View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.scripting.engine;
20  
21  /*
22   * Licensed to the Apache Software Foundation (ASF) under one
23   * or more contributor license agreements.  See the NOTICE file
24   * distributed with this work for additional information
25   * regarding copyright ownership.  The ASF licenses this file
26   * to you under the Apache License, Version 2.0 (the
27   * "License"); you may not use this file except in compliance
28   * with the License.  You may obtain a copy of the License at
29   *
30   *   http://www.apache.org/licenses/LICENSE-2.0
31   *
32   * Unless required by applicable law or agreed to in writing,
33   * software distributed under the License is distributed on an
34   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
35   * KIND, either express or implied.  See the License for the
36   * specific language governing permissions and limitations
37   * under the License.
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   * The java engine implementation.
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          // plexus compiler is great but overkill there so don't bring it just for that
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             // TODO: make it configurable from the project in subsequent releases
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             // TODO: use a Logger in subsequent releases. Not very important as of now, so using std streams
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" // assume the script doesn't return anything for now
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 }