From def5f24cafa95fdd91c8675f1bd6b7ff2b30a204 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 18 Jun 2014 16:07:46 -0500 Subject: [PATCH 1/5] Allow running inside Maven unit tests When Maven's surefire plugin runs unit tests, the URLClassLoader does actually not list the URLs corresponding to the dependencies. Instead, surefire tries to be clever and hides those dependencies inside a single, empty .jar file's manifest's Class-Path: entry. We are not dumb, so surefire loses: we get the URLs anyway. Hah! Take this, surefire! *shakes-fist* Signed-off-by: Johannes Schindelin --- .../plugins/scripting/java/JavaEngine.java | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java b/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java index ed66b5c..e56c30f 100644 --- a/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java +++ b/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java @@ -43,10 +43,14 @@ import java.io.Reader; import java.io.StringReader; import java.io.Writer; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.jar.Attributes.Name; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -463,10 +467,11 @@ private static List getAllDependencies(final BuildEnvironment env) { for (final URL url : ((URLClassLoader)loader).getURLs()) { if (url.getProtocol().equals("file")) { final File file = new File(url.getPath()); - final String artifactId = fakeArtifactId(env, file.getName()); - Coordinate dependency = new Coordinate(DEFAULT_GROUP_ID, artifactId, "1.0.0"); - env.fakePOM(file, dependency); - result.add(dependency); + if (url.toString().matches(".*/target/surefire/surefirebooter[0-9]*\\.jar")) { + getSurefireBooterURLs(file, url, env, result); + continue; + } + result.add(fakeDependency(env, file)); } } } @@ -474,4 +479,37 @@ private static List getAllDependencies(final BuildEnvironment env) { return result; } + private static Coordinate fakeDependency(final BuildEnvironment env, final File file) { + final String artifactId = fakeArtifactId(env, file.getName()); + Coordinate dependency = new Coordinate(DEFAULT_GROUP_ID, artifactId, "1.0.0"); + env.fakePOM(file, dependency); + return dependency; + } + + private static void getSurefireBooterURLs(final File file, final URL baseURL, + final BuildEnvironment env, final List result) + { + try { + final JarFile jar = new JarFile(file); + Manifest manifest = jar.getManifest(); + if (manifest != null) { + final String classPath = + manifest.getMainAttributes().getValue(Name.CLASS_PATH); + if (classPath != null) { + for (final String element : classPath.split(" +")) + try { + final File dependency = new File(new URL(baseURL, element).getPath()); + result.add(fakeDependency(env, dependency)); + } + catch (MalformedURLException e) { + e.printStackTrace(); + } + } + } + } + catch (final IOException e) { + e.printStackTrace(); + } + } + } From f6dd3f421a6e293b029cfc192be69141974bfafc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 18 Jun 2014 17:46:49 -0500 Subject: [PATCH 2/5] Verify that bare .java 'scripts' find all dependencies ... even when run via surefire... To that end, we piggy-back on the already existing test for bare .java "scripts": as scijava-common is a dependency of scripting-java, let's just use one of scijava-common's utility classes in the bare .java "script". That way, the test will only pass when scijava-common's class is found on the compiler class path. Signed-off-by: Johannes Schindelin --- .../java/org/scijava/plugins/scripting/java/JavaEngineTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java b/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java index 08cadb1..863bdd1 100644 --- a/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java +++ b/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java @@ -139,8 +139,10 @@ public void minimalProjectFromSource() throws Exception { public void testEvalReader() throws Exception { final String source = "" + // "package pinky.brain;\n" + // + "import org.scijava.util.AppUtils;\n" + // "public class TakeOverTheWorld {\n" + // "\tpublic static void main(final String[] arguments) {\n" + // + "\t\tSystem.err.println(\"main class: \" + AppUtils.getMainClass());\n" + // "\t\tthrow new RuntimeException(\"Egads!\");\n" + // "\t}\n" + // "}"; From d7f00fc2c112381a7ebad7bff7c5364f1d91f127 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 19 Jun 2014 09:28:31 -0500 Subject: [PATCH 3/5] JavaEngine: Add all missing Javadocs Signed-off-by: Johannes Schindelin --- .../plugins/scripting/java/JavaEngine.java | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java b/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java index e56c30f..f543e4d 100644 --- a/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java +++ b/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java @@ -96,7 +96,11 @@ public class JavaEngine extends AbstractScriptEngine { private final static String DEFAULT_GROUP_ID = "net.imagej"; private final static String DEFAULT_VERSION = "1.0.0-SNAPSHOT"; + /** + * The key to specify how to indent the XML written out by Xalan. + */ private final static String XALAN_INDENT_AMOUNT = "{http://xml.apache.org/xslt}indent-amount"; + { engineScopeBindings = new JavaEngineBindings(); } @@ -110,11 +114,31 @@ public class JavaEngine extends AbstractScriptEngine { @Parameter private JavaService javaService; + /** + * Compiles and runs the specified {@code .java} class. + *

+ * The currently active {@link JavaService} is responsible for running the + * class. + *

+ * + * @param script the source code for a Java class + * @return null + */ @Override public Object eval(String script) throws ScriptException { return eval(new StringReader(script)); } + /** + * Compiles and runs the specified {@code .java} class. + *

+ * The currently active {@link JavaService} is responsible for running the + * class. + *

+ * + * @param reader the reader producing the source code for a Java class + * @return null + */ @Override public Object eval(Reader reader) throws ScriptException { final String path = (String)get(FILENAME); @@ -169,6 +193,12 @@ public Object eval(Reader reader) throws ScriptException { return null; } + /** + * Compiles the specified {@code .java} file. + * + * @param file the source code + * @param errorWriter where to write the errors + */ public void compile(final File file, final Writer errorWriter) { try { final Builder builder = new Builder(file, null, errorWriter); @@ -207,6 +237,17 @@ public void makeJar(final File file, final boolean includeSources, final File ou } } + /** + * Reports an exception. + *

+ * If a writer for errors is specified (e.g. when being called from the script + * editor), we should just print the error and return. Otherwise, we'll throw + * the exception back at the caller. + *

+ * + * @param t the exception + * @param errorWriter the error writer, or null + */ private void printOrThrow(Throwable t, Writer errorWriter) { RuntimeException e = t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); @@ -218,6 +259,11 @@ private void printOrThrow(Throwable t, Writer errorWriter) { err.flush(); } + /** + * A wrapper around a (possibly only temporary) project. + * + * @author Johannes Schindelin + */ private class Builder { private final PrintStream err; private final File temporaryDirectory; @@ -280,6 +326,9 @@ public void println(final String line) throws IOException { } } + /** + * Cleans up the project, if it was only temporary. + */ private void cleanup() { if (err != null) err.close(); @@ -292,6 +341,24 @@ private void cleanup() { } } + /** + * Returns a Maven POM associated with a {@code .java} file. + *

+ * If the file is not part of a valid Maven project, one will be generated. + *

+ * + * @param env the {@link BuildEnvironment} + * @param file the {@code .java} file + * @param mainClass the name of the class to execute + * @return the Maven POM + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException + * @throws ScriptException + * @throws TransformerConfigurationException + * @throws TransformerException + * @throws TransformerFactoryConfigurationError + */ private MavenProject getMavenProject(final BuildEnvironment env, final File file, final String mainClass) throws IOException, ParserConfigurationException, SAXException, ScriptException, @@ -310,6 +377,13 @@ private MavenProject getMavenProject(final BuildEnvironment env, return writeTemporaryProject(env, new FileReader(file)); } + /** + * Determines the class name of a Java class given its source code. + * + * @param file the source code + * @return the class name including the package + * @throws IOException + */ private static String getFullClassName(final File file) throws IOException { String name = file.getName(); if (!name.endsWith(".java")) { @@ -349,6 +423,19 @@ private static String getFullClassName(final File file) throws IOException { return packageName + name; // the 'package' statement must be the first in the file } + /** + * Makes a temporary Maven project for a virtual {@code .java} file. + * + * @param env the {@link BuildEnvironment} to store the generated Maven POM + * @param reader the virtual {@code .java} file + * @return the generated Maven POM + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException + * @throws TransformerConfigurationException + * @throws TransformerException + * @throws TransformerFactoryConfigurationError + */ private static MavenProject writeTemporaryProject(final BuildEnvironment env, final Reader reader) throws IOException, ParserConfigurationException, SAXException, TransformerConfigurationException, TransformerException, @@ -382,6 +469,18 @@ private static MavenProject writeTemporaryProject(final BuildEnvironment env, return fakePOM(env, directory, artifactId, mainClass, true); } + /** + * Fakes a sensible, valid {@code artifactId}. + *

+ * Given a name for a project or {@code .java} file, this function generated a + * proper {@code artifactId} for use in faked Maven POMs. + *

+ * + * @param env the associated {@link BuildEnvironment} (to avoid duplicate + * {@code artifactId}s) + * @param name the project name + * @return the generated {@code artifactId} + */ private static String fakeArtifactId(final BuildEnvironment env, final String name) { int dot = name.indexOf('.'); final String prefix = dot < 0 ? name : dot == 0 ? "dependency" : name.substring(0, dot); @@ -396,6 +495,29 @@ private static String fakeArtifactId(final BuildEnvironment env, final String na } } + /** + * Fakes a single Maven POM for a given dependency. + *

+ * When discovering possible dependencies on the class path, we do not + * necessarily deal with proper Maven-generated artifacts. To be able to use + * them for single {@code .java} "scripts", we simply fake Maven POMs for + * those files. + *

+ * + * @param env the {@link BuildEnvironment} to house the faked POM + * @param directory the directory associated with the Maven project + * @param artifactId the {@code artifactId} of the dependency + * @param mainClass the main class, if any + * @param writePOM whether to write the Maven POM as {@code pom.xml} into the + * specified directory + * @return the faked POM + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException + * @throws TransformerConfigurationException + * @throws TransformerException + * @throws TransformerFactoryConfigurationError + */ private static MavenProject fakePOM(final BuildEnvironment env, final File directory, final String artifactId, final String mainClass, boolean writePOM) throws IOException, ParserConfigurationException, SAXException, @@ -453,6 +575,15 @@ private static MavenProject fakePOM(final BuildEnvironment env, return env.parse(new ByteArrayInputStream(out.toByteArray()), directory, null, null); } + /** + * Writes out the specified XML element. + * + * @param document the XML document + * @param parent the parent node + * @param tag the tag to append + * @param content the content of the tag to append + * @return the appended node + */ private static Element append(final Document document, final Element parent, final String tag, final String content) { Element child = document.createElement(tag); if (content != null) child.appendChild(document.createCDATASection(content)); @@ -460,6 +591,21 @@ private static Element append(final Document document, final Element parent, fin return child; } + /** + * Discovers all current class path elements and offers them as faked Maven + * POMs. + *

+ * When constructing an in-memory Maven POM for a single {@code .java} file, + * we need to make sure that all class path elements are available to the + * compiler. Since we use MiniMaven to compile everything (in order to be + * consistent, and also to be able to generate Maven projects conveniently, to + * turn hacky projects into proper ones), we need to put all of that into the + * Maven context, i.e. fake Maven POMs for all the dependencies. + *

+ * + * @param env the {@link BuildEnvironment} in which the faked POMs are stored + * @return the list of dependencies, as {@link Coordinate}s + */ private static List getAllDependencies(final BuildEnvironment env) { final List result = new ArrayList(); for (ClassLoader loader = env.getClass().getClassLoader(); loader != null; loader = loader.getParent()) { @@ -479,6 +625,18 @@ private static List getAllDependencies(final BuildEnvironment env) { return result; } + /** + * Fakes a Maven POM in memory for a specified dependency. + *

+ * When compiling bare {@code .java} files, we need to fake a full-blown Maven + * project, including full-blown Maven dependencies for all of the files + * present on the current class path. + *

+ * + * @param env the {@link BuildEnvironment} for storing the faked Maven POM + * @param file the dependency + * @return the {@link Coordinate} specifying the dependency + */ private static Coordinate fakeDependency(final BuildEnvironment env, final File file) { final String artifactId = fakeArtifactId(env, file.getName()); Coordinate dependency = new Coordinate(DEFAULT_GROUP_ID, artifactId, "1.0.0"); @@ -486,6 +644,30 @@ private static Coordinate fakeDependency(final BuildEnvironment env, final File return dependency; } + /** + * Figures out the class path given a {@code .jar} file generated by the + * {@code maven-surefire-plugin}. + *

+ * A little-known feature of JAR files is that their manifest can specify + * additional class path elements in a {@code Class-Path} entry. The + * {@code maven-surefire-plugin} makes extensive use of that: the URLs of the + * of the active {@link URLClassLoader} will consist of only a single + * {@code .jar} file that is empty except for a manifest whose sole purpose is + * to specify the dependencies. + *

+ *

+ * This method can be used to discover those additional class path elements. + *

+ * + * @param file the {@code .jar} file generated by the + * {@code maven-surefire-plugin} + * @param baseURL the {@link URL} of the {@code .jar} file, needed for class + * path elements specified as relative paths + * @param env the {@link BuildEnvironment}, to store the Maven POMs faked for + * the class path elements + * @param result the list of dependencies to which the discovered dependencies + * are added + */ private static void getSurefireBooterURLs(final File file, final URL baseURL, final BuildEnvironment env, final List result) { From 5216a8d8966a2cca81e58ee7eddf8c586bac98b7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 19 Jun 2014 09:28:51 -0500 Subject: [PATCH 4/5] *Always* use the context class loader If we ever limit which class loader we use as parent or to discover the available dependencies by using the class loader that happened to load MiniMaven, we will be in for a *lot* of trouble in the future, e.g. with ImageJ 1.x' PluginClassLoader. So let's not do that. Let's use the current thread's context class loader, like everywhere else we need a class loader. Signed-off-by: Johannes Schindelin --- .../java/org/scijava/plugins/scripting/java/JavaEngine.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java b/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java index f543e4d..1686d50 100644 --- a/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java +++ b/src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java @@ -168,7 +168,7 @@ public Object eval(Reader reader) throws ScriptException { urls[i] = new URL("file:" + paths[i] + (paths[i].endsWith(".jar") ? "" : "/")); URLClassLoader classLoader = new URLClassLoader(urls, - getClass().getClassLoader()); + Thread.currentThread().getContextClassLoader()); // needed for sezpoz Thread.currentThread().setContextClassLoader(classLoader); @@ -608,7 +608,8 @@ private static Element append(final Document document, final Element parent, fin */ private static List getAllDependencies(final BuildEnvironment env) { final List result = new ArrayList(); - for (ClassLoader loader = env.getClass().getClassLoader(); loader != null; loader = loader.getParent()) { + for (ClassLoader loader = Thread.currentThread().getContextClassLoader(); + loader != null; loader = loader.getParent()) { if (loader instanceof URLClassLoader) { for (final URL url : ((URLClassLoader)loader).getURLs()) { if (url.getProtocol().equals("file")) { From 5a2e628962faa0727ac327cc0491f5f3eae6df1b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 19 Jun 2014 14:57:46 -0500 Subject: [PATCH 5/5] Remove unused imports Signed-off-by: Johannes Schindelin --- .../java/org/scijava/plugins/scripting/java/JavaEngineTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java b/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java index 863bdd1..ad8f8c9 100644 --- a/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java +++ b/src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java @@ -48,11 +48,9 @@ import org.junit.Test; import org.scijava.Context; import org.scijava.object.ObjectService; -import org.scijava.plugins.scripting.java.JavaScriptLanguage; import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; import org.scijava.test.TestUtils; -import org.scijava.util.FileUtils; /** * Tests the Java 'scripting' backend.