Skip to content

Commit f16d707

Browse files
committed
Merge pull request #1 from scijava/surefire
Allow running inside Maven unit tests
2 parents b40b8dc + 5a2e628 commit f16d707

File tree

2 files changed

+229
-8
lines changed

2 files changed

+229
-8
lines changed

src/main/java/org/scijava/plugins/scripting/java/JavaEngine.java

Lines changed: 227 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@
4343
import java.io.Reader;
4444
import java.io.StringReader;
4545
import java.io.Writer;
46+
import java.net.MalformedURLException;
4647
import java.net.URL;
4748
import java.net.URLClassLoader;
4849
import java.util.ArrayList;
4950
import java.util.List;
51+
import java.util.jar.JarFile;
52+
import java.util.jar.Manifest;
53+
import java.util.jar.Attributes.Name;
5054
import java.util.regex.Matcher;
5155
import 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
}

src/test/java/org/scijava/plugins/scripting/java/JavaEngineTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,9 @@
4848
import org.junit.Test;
4949
import org.scijava.Context;
5050
import org.scijava.object.ObjectService;
51-
import org.scijava.plugins.scripting.java.JavaScriptLanguage;
5251
import org.scijava.script.ScriptLanguage;
5352
import org.scijava.script.ScriptService;
5453
import org.scijava.test.TestUtils;
55-
import org.scijava.util.FileUtils;
5654

5755
/**
5856
* Tests the Java 'scripting' backend.
@@ -139,8 +137,10 @@ public void minimalProjectFromSource() throws Exception {
139137
public void testEvalReader() throws Exception {
140138
final String source = "" + //
141139
"package pinky.brain;\n" + //
140+
"import org.scijava.util.AppUtils;\n" + //
142141
"public class TakeOverTheWorld {\n" + //
143142
"\tpublic static void main(final String[] arguments) {\n" + //
143+
"\t\tSystem.err.println(\"main class: \" + AppUtils.getMainClass());\n" + //
144144
"\t\tthrow new RuntimeException(\"Egads!\");\n" + //
145145
"\t}\n" + //
146146
"}";

0 commit comments

Comments
 (0)