1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.scripting.engine;
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 import javax.script.AbstractScriptEngine;
41 import javax.script.Bindings;
42 import javax.script.Compilable;
43 import javax.script.CompiledScript;
44 import javax.script.ScriptContext;
45 import javax.script.ScriptEngine;
46 import javax.script.ScriptEngineFactory;
47 import javax.script.ScriptException;
48 import javax.script.SimpleBindings;
49 import javax.tools.JavaCompiler;
50 import javax.tools.ToolProvider;
51
52 import java.io.BufferedReader;
53 import java.io.File;
54 import java.io.IOException;
55 import java.io.Reader;
56 import java.io.StringReader;
57 import java.io.Writer;
58 import java.net.URL;
59 import java.net.URLClassLoader;
60 import java.nio.file.FileVisitResult;
61 import java.nio.file.Files;
62 import java.nio.file.Path;
63 import java.nio.file.Paths;
64 import java.nio.file.SimpleFileVisitor;
65 import java.nio.file.attribute.BasicFileAttributes;
66 import java.util.stream.Stream;
67
68 import org.apache.maven.plugin.logging.Log;
69
70 import static java.util.Objects.requireNonNull;
71 import static java.util.stream.Collectors.joining;
72
73
74
75
76 public class JavaEngine extends AbstractScriptEngine implements Compilable, ContextAwareEngine {
77 private final ScriptEngineFactory factory;
78
79 private Log log;
80
81 public JavaEngine(ScriptEngineFactory factory) {
82 this.factory = factory;
83 }
84
85 @Override
86 public void setLog(Log log) {
87 this.log = log;
88 }
89
90 @Override
91 public CompiledScript compile(String script) throws ScriptException {
92
93 final JavaCompiler compiler =
94 requireNonNull(ToolProvider.getSystemJavaCompiler(), "you must run on a JDK to have a compiler");
95 Path tmpDir = null;
96 try {
97 tmpDir = Files.createTempDirectory(getClass().getSimpleName());
98
99 final String packageName = getClass().getPackage().getName() + ".generated";
100 final String className = "JavaCompiledScript_" + Math.abs(script.hashCode());
101 final String source = toSource(packageName, className, script);
102 final Path src = tmpDir.resolve("sources");
103 final Path bin = tmpDir.resolve("bin");
104 final Path srcDir = src.resolve(packageName.replace('.', '/'));
105 Files.createDirectories(srcDir);
106 Files.createDirectories(bin);
107 final Path java = srcDir.resolve(className + ".java");
108 try (Writer writer = Files.newBufferedWriter(java)) {
109 writer.write(source);
110 }
111
112
113 final String classpath = mavenClasspathPrefix()
114 + System.getProperty(
115 getClass().getName() + ".classpath",
116 System.getProperty("java.class.path", System.getProperty("surefire.real.class.path")));
117
118
119 final int run = compiler.run(
120 null,
121 System.out,
122 System.err,
123 Stream.of(
124 "-classpath",
125 classpath,
126 "-sourcepath",
127 src.toAbsolutePath().toString(),
128 "-d",
129 bin.toAbsolutePath().toString(),
130 java.toAbsolutePath().toString())
131 .toArray(String[]::new));
132 if (run != 0) {
133 throw new IllegalArgumentException(
134 "Can't compile the incoming script, here is the generated code: >\n" + source + "\n<\n");
135 }
136 final URLClassLoader loader = new URLClassLoader(
137 new URL[] {bin.toUri().toURL()}, Thread.currentThread().getContextClassLoader());
138 final Class<? extends CompiledScript> loadClass =
139 loader.loadClass(packageName + '.' + className).asSubclass(CompiledScript.class);
140 return loadClass
141 .getConstructor(ScriptEngine.class, URLClassLoader.class)
142 .newInstance(this, loader);
143 } catch (Exception e) {
144 throw new ScriptException(e);
145 } finally {
146 if (tmpDir != null) {
147 try {
148 Files.walkFileTree(tmpDir, new SimpleFileVisitor<Path>() {
149 @Override
150 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
151 Files.delete(file);
152 return FileVisitResult.CONTINUE;
153 }
154
155 @Override
156 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
157 Files.delete(dir);
158 return FileVisitResult.CONTINUE;
159 }
160 });
161 } catch (IOException e) {
162 if (log != null) {
163 log.debug(e);
164 }
165 }
166 }
167 }
168 }
169
170 private String mavenClasspathPrefix() {
171 final String home = System.getProperty("maven.home");
172 if (home == null) {
173 if (log != null) {
174 log.debug("No maven.home set");
175 }
176 return "";
177 }
178 try (Stream<Path> files = Files.list(Paths.get(home).resolve("lib"))) {
179 return files.filter(it -> {
180 final String name = it.getFileName().toString();
181 return name.startsWith("maven-");
182 })
183 .map(Path::toString)
184 .collect(joining(File.pathSeparator, "", File.pathSeparator));
185 } catch (IOException e) {
186 if (log != null) {
187 log.debug(e);
188 }
189 return "";
190 }
191 }
192
193 private String toSource(String pck, String name, String script) {
194 final String[] importsAndScript = splitImportsAndScript(script);
195 return "package " + pck + ";\n"
196 + "\n"
197 + "import java.io.*;\n"
198 + "import java.net.*;\n"
199 + "import java.util.*;\n"
200 + "import java.util.stream.*;\n"
201 + "import java.nio.file.*;\n"
202 + "import org.apache.maven.project.MavenProject;\n"
203 + "import org.apache.maven.plugin.logging.Log;\n"
204 + "\n"
205 + "import javax.script.Bindings;\n"
206 + "import javax.script.CompiledScript;\n"
207 + "import javax.script.ScriptContext;\n"
208 + "import javax.script.ScriptEngine;\n"
209 + "import javax.script.ScriptException;\n"
210 + "\n"
211 + importsAndScript[0] + '\n'
212 + "\n"
213 + "public class " + name + " extends CompiledScript implements AutoCloseable {\n"
214 + " private final ScriptEngine $engine;\n"
215 + " private final URLClassLoader $loader;\n"
216 + "\n"
217 + " public " + name + "( ScriptEngine engine, URLClassLoader loader) {\n"
218 + " this.$engine = engine;\n"
219 + " this.$loader = loader;\n"
220 + " }\n"
221 + "\n"
222 + " @Override\n"
223 + " public Object eval( ScriptContext $context) throws ScriptException {\n"
224 + " final Thread $thread = Thread.currentThread();\n"
225 + " final ClassLoader $oldClassLoader = $thread.getContextClassLoader();\n"
226 + " $thread.setContextClassLoader($loader);\n"
227 + " try {\n"
228 + " final Bindings $bindings = $context.getBindings(ScriptContext.GLOBAL_SCOPE);\n"
229 + " final MavenProject $project = MavenProject.class.cast($bindings.get(\"project\"));\n"
230 + " final Log $log = Log.class.cast($bindings.get(\"log\"));\n"
231 + " " + importsAndScript[1] + "\n"
232 + " return null;\n"
233 + " } catch ( Exception e) {\n"
234 + " if (RuntimeException.class.isInstance(e)) {\n"
235 + " throw RuntimeException.class.cast(e);\n"
236 + " }\n"
237 + " throw new IllegalStateException(e);\n"
238 + " } finally {\n"
239 + " $thread.setContextClassLoader($oldClassLoader);\n"
240 + " }\n"
241 + " }\n"
242 + "\n"
243 + " @Override\n"
244 + " public ScriptEngine getEngine() {\n"
245 + " return $engine;\n"
246 + " }\n"
247 + "\n"
248 + " @Override\n"
249 + " public void close() throws Exception {\n"
250 + " $loader.close();\n"
251 + " }\n"
252 + "}";
253 }
254
255 private String[] splitImportsAndScript(String script) {
256 final StringBuilder imports = new StringBuilder();
257 final StringBuilder content = new StringBuilder();
258 boolean useImport = true;
259 boolean inComment = false;
260 try (BufferedReader reader = new BufferedReader(new StringReader(script))) {
261 String line;
262 while ((line = reader.readLine()) != null) {
263 if (useImport) {
264 String trimmed = line.trim();
265 if (trimmed.isEmpty()) {
266 continue;
267 }
268 if (trimmed.startsWith("/*")) {
269 inComment = true;
270 continue;
271 }
272 if (trimmed.endsWith("*/") && inComment) {
273 inComment = false;
274 continue;
275 }
276 if (inComment) {
277 continue;
278 }
279 if (trimmed.startsWith("import ") && trimmed.endsWith(";")) {
280 imports.append(line).append('\n');
281 continue;
282 }
283 useImport = false;
284 }
285 content.append(line).append('\n');
286 }
287 } catch (IOException ioe) {
288 throw new IllegalStateException(ioe);
289 }
290 return new String[] {imports.toString().trim(), content.toString().trim()};
291 }
292
293 @Override
294 public Object eval(String script, ScriptContext context) throws ScriptException {
295 final CompiledScript compile = compile(script);
296 try {
297 return compile.eval(context);
298 } finally {
299 doClose(compile);
300 }
301 }
302
303 @Override
304 public Object eval(Reader reader, ScriptContext context) throws ScriptException {
305 return eval(load(reader), context);
306 }
307
308 @Override
309 public CompiledScript compile(Reader script) throws ScriptException {
310 return compile(load(script));
311 }
312
313 @Override
314 public Bindings createBindings() {
315 return new SimpleBindings();
316 }
317
318 @Override
319 public ScriptEngineFactory getFactory() {
320 return factory;
321 }
322
323 private void doClose(final CompiledScript compile) {
324 if (!AutoCloseable.class.isInstance(compile)) {
325 return;
326 }
327 try {
328 AutoCloseable.class.cast(compile).close();
329 } catch (Exception e) {
330 if (log != null) {
331 log.debug(e);
332 }
333 }
334 }
335
336 private String load(Reader reader) {
337 return new BufferedReader(reader).lines().collect(joining("\n"));
338 }
339 }