4343import java .io .Reader ;
4444import java .io .StringReader ;
4545import java .io .Writer ;
46+ import java .net .MalformedURLException ;
4647import java .net .URL ;
4748import java .net .URLClassLoader ;
4849import java .util .ArrayList ;
4950import java .util .List ;
51+ import java .util .jar .JarFile ;
52+ import java .util .jar .Manifest ;
53+ import java .util .jar .Attributes .Name ;
5054import java .util .regex .Matcher ;
5155import java .util .regex .Pattern ;
5256
@@ -92,7 +96,11 @@ public class JavaEngine extends AbstractScriptEngine {
9296 private final static String DEFAULT_GROUP_ID = "net.imagej" ;
9397 private final static String DEFAULT_VERSION = "1.0.0-SNAPSHOT" ;
9498
99+ /**
100+ * The key to specify how to indent the XML written out by Xalan.
101+ */
95102 private final static String XALAN_INDENT_AMOUNT = "{http://xml.apache.org/xslt}indent-amount" ;
103+
96104 {
97105 engineScopeBindings = new JavaEngineBindings ();
98106 }
@@ -106,11 +114,31 @@ public class JavaEngine extends AbstractScriptEngine {
106114 @ Parameter
107115 private JavaService javaService ;
108116
117+ /**
118+ * Compiles and runs the specified {@code .java} class.
119+ * <p>
120+ * The currently active {@link JavaService} is responsible for running the
121+ * class.
122+ * </p>
123+ *
124+ * @param script the source code for a Java class
125+ * @return null
126+ */
109127 @ Override
110128 public Object eval (String script ) throws ScriptException {
111129 return eval (new StringReader (script ));
112130 }
113131
132+ /**
133+ * Compiles and runs the specified {@code .java} class.
134+ * <p>
135+ * The currently active {@link JavaService} is responsible for running the
136+ * class.
137+ * </p>
138+ *
139+ * @param reader the reader producing the source code for a Java class
140+ * @return null
141+ */
114142 @ Override
115143 public Object eval (Reader reader ) throws ScriptException {
116144 final String path = (String )get (FILENAME );
@@ -140,7 +168,7 @@ public Object eval(Reader reader) throws ScriptException {
140168 urls [i ] = new URL ("file:" + paths [i ]
141169 + (paths [i ].endsWith (".jar" ) ? "" : "/" ));
142170 URLClassLoader classLoader = new URLClassLoader (urls ,
143- getClass ().getClassLoader ());
171+ Thread . currentThread ().getContextClassLoader ());
144172
145173 // needed for sezpoz
146174 Thread .currentThread ().setContextClassLoader (classLoader );
@@ -165,6 +193,12 @@ public Object eval(Reader reader) throws ScriptException {
165193 return null ;
166194 }
167195
196+ /**
197+ * Compiles the specified {@code .java} file.
198+ *
199+ * @param file the source code
200+ * @param errorWriter where to write the errors
201+ */
168202 public void compile (final File file , final Writer errorWriter ) {
169203 try {
170204 final Builder builder = new Builder (file , null , errorWriter );
@@ -203,6 +237,17 @@ public void makeJar(final File file, final boolean includeSources, final File ou
203237 }
204238 }
205239
240+ /**
241+ * Reports an exception.
242+ * <p>
243+ * If a writer for errors is specified (e.g. when being called from the script
244+ * editor), we should just print the error and return. Otherwise, we'll throw
245+ * the exception back at the caller.
246+ * </p>
247+ *
248+ * @param t the exception
249+ * @param errorWriter the error writer, or null
250+ */
206251 private void printOrThrow (Throwable t , Writer errorWriter ) {
207252 RuntimeException e = t instanceof RuntimeException ? (RuntimeException ) t
208253 : new RuntimeException (t );
@@ -214,6 +259,11 @@ private void printOrThrow(Throwable t, Writer errorWriter) {
214259 err .flush ();
215260 }
216261
262+ /**
263+ * A wrapper around a (possibly only temporary) project.
264+ *
265+ * @author Johannes Schindelin
266+ */
217267 private class Builder {
218268 private final PrintStream err ;
219269 private final File temporaryDirectory ;
@@ -276,6 +326,9 @@ public void println(final String line) throws IOException {
276326 }
277327 }
278328
329+ /**
330+ * Cleans up the project, if it was only temporary.
331+ */
279332 private void cleanup () {
280333 if (err != null )
281334 err .close ();
@@ -288,6 +341,24 @@ private void cleanup() {
288341 }
289342 }
290343
344+ /**
345+ * Returns a Maven POM associated with a {@code .java} file.
346+ * <p>
347+ * If the file is not part of a valid Maven project, one will be generated.
348+ * </p>
349+ *
350+ * @param env the {@link BuildEnvironment}
351+ * @param file the {@code .java} file
352+ * @param mainClass the name of the class to execute
353+ * @return the Maven POM
354+ * @throws IOException
355+ * @throws ParserConfigurationException
356+ * @throws SAXException
357+ * @throws ScriptException
358+ * @throws TransformerConfigurationException
359+ * @throws TransformerException
360+ * @throws TransformerFactoryConfigurationError
361+ */
291362 private MavenProject getMavenProject (final BuildEnvironment env ,
292363 final File file , final String mainClass ) throws IOException ,
293364 ParserConfigurationException , SAXException , ScriptException ,
@@ -306,6 +377,13 @@ private MavenProject getMavenProject(final BuildEnvironment env,
306377 return writeTemporaryProject (env , new FileReader (file ));
307378 }
308379
380+ /**
381+ * Determines the class name of a Java class given its source code.
382+ *
383+ * @param file the source code
384+ * @return the class name including the package
385+ * @throws IOException
386+ */
309387 private static String getFullClassName (final File file ) throws IOException {
310388 String name = file .getName ();
311389 if (!name .endsWith (".java" )) {
@@ -345,6 +423,19 @@ private static String getFullClassName(final File file) throws IOException {
345423 return packageName + name ; // the 'package' statement must be the first in the file
346424 }
347425
426+ /**
427+ * Makes a temporary Maven project for a virtual {@code .java} file.
428+ *
429+ * @param env the {@link BuildEnvironment} to store the generated Maven POM
430+ * @param reader the virtual {@code .java} file
431+ * @return the generated Maven POM
432+ * @throws IOException
433+ * @throws ParserConfigurationException
434+ * @throws SAXException
435+ * @throws TransformerConfigurationException
436+ * @throws TransformerException
437+ * @throws TransformerFactoryConfigurationError
438+ */
348439 private static MavenProject writeTemporaryProject (final BuildEnvironment env ,
349440 final Reader reader ) throws IOException , ParserConfigurationException ,
350441 SAXException , TransformerConfigurationException , TransformerException ,
@@ -378,6 +469,18 @@ private static MavenProject writeTemporaryProject(final BuildEnvironment env,
378469 return fakePOM (env , directory , artifactId , mainClass , true );
379470 }
380471
472+ /**
473+ * Fakes a sensible, valid {@code artifactId}.
474+ * <p>
475+ * Given a name for a project or {@code .java} file, this function generated a
476+ * proper {@code artifactId} for use in faked Maven POMs.
477+ * </p>
478+ *
479+ * @param env the associated {@link BuildEnvironment} (to avoid duplicate
480+ * {@code artifactId}s)
481+ * @param name the project name
482+ * @return the generated {@code artifactId}
483+ */
381484 private static String fakeArtifactId (final BuildEnvironment env , final String name ) {
382485 int dot = name .indexOf ('.' );
383486 final String prefix = dot < 0 ? name : dot == 0 ? "dependency" : name .substring (0 , dot );
@@ -392,6 +495,29 @@ private static String fakeArtifactId(final BuildEnvironment env, final String na
392495 }
393496 }
394497
498+ /**
499+ * Fakes a single Maven POM for a given dependency.
500+ * <p>
501+ * When discovering possible dependencies on the class path, we do not
502+ * necessarily deal with proper Maven-generated artifacts. To be able to use
503+ * them for single {@code .java} "scripts", we simply fake Maven POMs for
504+ * those files.
505+ * </p>
506+ *
507+ * @param env the {@link BuildEnvironment} to house the faked POM
508+ * @param directory the directory associated with the Maven project
509+ * @param artifactId the {@code artifactId} of the dependency
510+ * @param mainClass the main class, if any
511+ * @param writePOM whether to write the Maven POM as {@code pom.xml} into the
512+ * specified directory
513+ * @return the faked POM
514+ * @throws IOException
515+ * @throws ParserConfigurationException
516+ * @throws SAXException
517+ * @throws TransformerConfigurationException
518+ * @throws TransformerException
519+ * @throws TransformerFactoryConfigurationError
520+ */
395521 private static MavenProject fakePOM (final BuildEnvironment env ,
396522 final File directory , final String artifactId , final String mainClass , boolean writePOM )
397523 throws IOException , ParserConfigurationException , SAXException ,
@@ -449,29 +575,124 @@ private static MavenProject fakePOM(final BuildEnvironment env,
449575 return env .parse (new ByteArrayInputStream (out .toByteArray ()), directory , null , null );
450576 }
451577
578+ /**
579+ * Writes out the specified XML element.
580+ *
581+ * @param document the XML document
582+ * @param parent the parent node
583+ * @param tag the tag to append
584+ * @param content the content of the tag to append
585+ * @return the appended node
586+ */
452587 private static Element append (final Document document , final Element parent , final String tag , final String content ) {
453588 Element child = document .createElement (tag );
454589 if (content != null ) child .appendChild (document .createCDATASection (content ));
455590 parent .appendChild (child );
456591 return child ;
457592 }
458593
594+ /**
595+ * Discovers all current class path elements and offers them as faked Maven
596+ * POMs.
597+ * <p>
598+ * When constructing an in-memory Maven POM for a single {@code .java} file,
599+ * we need to make sure that all class path elements are available to the
600+ * compiler. Since we use MiniMaven to compile everything (in order to be
601+ * consistent, and also to be able to generate Maven projects conveniently, to
602+ * turn hacky projects into proper ones), we need to put all of that into the
603+ * Maven context, i.e. fake Maven POMs for all the dependencies.
604+ * </p>
605+ *
606+ * @param env the {@link BuildEnvironment} in which the faked POMs are stored
607+ * @return the list of dependencies, as {@link Coordinate}s
608+ */
459609 private static List <Coordinate > getAllDependencies (final BuildEnvironment env ) {
460610 final List <Coordinate > result = new ArrayList <Coordinate >();
461- for (ClassLoader loader = env .getClass ().getClassLoader (); loader != null ; loader = loader .getParent ()) {
611+ for (ClassLoader loader = Thread .currentThread ().getContextClassLoader ();
612+ loader != null ; loader = loader .getParent ()) {
462613 if (loader instanceof URLClassLoader ) {
463614 for (final URL url : ((URLClassLoader )loader ).getURLs ()) {
464615 if (url .getProtocol ().equals ("file" )) {
465616 final File file = new File (url .getPath ());
466- final String artifactId = fakeArtifactId (env , file .getName ());
467- Coordinate dependency = new Coordinate (DEFAULT_GROUP_ID , artifactId , "1.0.0" );
468- env .fakePOM (file , dependency );
469- result .add (dependency );
617+ if (url .toString ().matches (".*/target/surefire/surefirebooter[0-9]*\\ .jar" )) {
618+ getSurefireBooterURLs (file , url , env , result );
619+ continue ;
620+ }
621+ result .add (fakeDependency (env , file ));
470622 }
471623 }
472624 }
473625 }
474626 return result ;
475627 }
476628
629+ /**
630+ * Fakes a Maven POM in memory for a specified dependency.
631+ * <p>
632+ * When compiling bare {@code .java} files, we need to fake a full-blown Maven
633+ * project, including full-blown Maven dependencies for all of the files
634+ * present on the current class path.
635+ * </p>
636+ *
637+ * @param env the {@link BuildEnvironment} for storing the faked Maven POM
638+ * @param file the dependency
639+ * @return the {@link Coordinate} specifying the dependency
640+ */
641+ private static Coordinate fakeDependency (final BuildEnvironment env , final File file ) {
642+ final String artifactId = fakeArtifactId (env , file .getName ());
643+ Coordinate dependency = new Coordinate (DEFAULT_GROUP_ID , artifactId , "1.0.0" );
644+ env .fakePOM (file , dependency );
645+ return dependency ;
646+ }
647+
648+ /**
649+ * Figures out the class path given a {@code .jar} file generated by the
650+ * {@code maven-surefire-plugin}.
651+ * <p>
652+ * A little-known feature of JAR files is that their manifest can specify
653+ * additional class path elements in a {@code Class-Path} entry. The
654+ * {@code maven-surefire-plugin} makes extensive use of that: the URLs of the
655+ * of the active {@link URLClassLoader} will consist of only a single
656+ * {@code .jar} file that is empty except for a manifest whose sole purpose is
657+ * to specify the dependencies.
658+ * </p>
659+ * <p>
660+ * This method can be used to discover those additional class path elements.
661+ * </p>
662+ *
663+ * @param file the {@code .jar} file generated by the
664+ * {@code maven-surefire-plugin}
665+ * @param baseURL the {@link URL} of the {@code .jar} file, needed for class
666+ * path elements specified as relative paths
667+ * @param env the {@link BuildEnvironment}, to store the Maven POMs faked for
668+ * the class path elements
669+ * @param result the list of dependencies to which the discovered dependencies
670+ * are added
671+ */
672+ private static void getSurefireBooterURLs (final File file , final URL baseURL ,
673+ final BuildEnvironment env , final List <Coordinate > result )
674+ {
675+ try {
676+ final JarFile jar = new JarFile (file );
677+ Manifest manifest = jar .getManifest ();
678+ if (manifest != null ) {
679+ final String classPath =
680+ manifest .getMainAttributes ().getValue (Name .CLASS_PATH );
681+ if (classPath != null ) {
682+ for (final String element : classPath .split (" +" ))
683+ try {
684+ final File dependency = new File (new URL (baseURL , element ).getPath ());
685+ result .add (fakeDependency (env , dependency ));
686+ }
687+ catch (MalformedURLException e ) {
688+ e .printStackTrace ();
689+ }
690+ }
691+ }
692+ }
693+ catch (final IOException e ) {
694+ e .printStackTrace ();
695+ }
696+ }
697+
477698}
0 commit comments