diff --git a/README.md b/README.md index ee94865..de04241 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ There are some guidelines which will make applying PRs easier for us: Optional supplemental description. ``` + Make sure you have added the necessary tests (JUnit/IT) for your changes. -+ Run all the tests with `mvn -Prun-its verify` to assure nothing else was accidentally broken. ++ Run all the tests with `mvn -Prun-its verify` to assure nothing else was accidentally broken (you may need to run `mvn install` beforehand). + Submit a pull request to the repository in the Apache organization. + Update your JIRA ticket and include a link to the pull request in the ticket. diff --git a/pom.xml b/pom.xml index 18d9266..7b15b5a 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,18 @@ under the License. 2021-02-24T19:52:25Z + + + + org.junit + junit-bom + 5.10.1 + pom + import + + + + org.apache.maven @@ -88,6 +100,12 @@ under the License. maven-plugin-annotations provided + + + org.junit.jupiter + junit-jupiter + test + diff --git a/src/it/projects/java-script/pom.xml b/src/it/projects/java-script/pom.xml new file mode 100644 index 0000000..ae202ee --- /dev/null +++ b/src/it/projects/java-script/pom.xml @@ -0,0 +1,48 @@ + + + + + + 4.0.0 + + org.apache.maven.plugins.scripting.its + java-script + 1.0.0-SNAPSHOT + pom + + + + + org.apache.maven.plugins + maven-scripting-plugin + @project.version@ + + Maven-Scripting-Java-Engine + + + + + + diff --git a/src/it/projects/java-script/verify.bsh b/src/it/projects/java-script/verify.bsh new file mode 100644 index 0000000..0124f7a --- /dev/null +++ b/src/it/projects/java-script/verify.bsh @@ -0,0 +1,26 @@ +import java.io.*; +import java.nio.file.*; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +File file = new File( basedir, "build.log" ); +if ( ! new String( Files.readAllBytes( file.toPath() ) ).contains( "87java-script" ) ) { + throw new IllegalArgumentException( "invalid output" ); +} diff --git a/src/main/java/org/apache/maven/plugins/scripting/EvalMojo.java b/src/main/java/org/apache/maven/plugins/scripting/EvalMojo.java index 739a7a0..d166090 100644 --- a/src/main/java/org/apache/maven/plugins/scripting/EvalMojo.java +++ b/src/main/java/org/apache/maven/plugins/scripting/EvalMojo.java @@ -24,12 +24,16 @@ import java.io.File; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; +import org.apache.maven.settings.Settings; /** * Evaluate the specified script or scriptFile @@ -64,14 +68,30 @@ public class EvalMojo extends AbstractMojo { @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; + @Parameter(defaultValue = "${mojoExecution}", readonly = true) + MojoExecution mojoExecution; + + @Parameter(defaultValue = "${pluginDescriptor}", readonly = true) + private PluginDescriptor pluginDescriptor; + + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession session; + + @Parameter(defaultValue = "${settings}", readonly = true) + private Settings settings; + @Override public void execute() throws MojoExecutionException, MojoFailureException { try { AbstractScriptEvaluator execute = constructExecute(); Bindings bindings = new SimpleBindings(); + bindings.put("session", session); bindings.put("project", project); + bindings.put("pluginDescriptor", pluginDescriptor); bindings.put("log", getLog()); + bindings.put("mojoExecution", mojoExecution); + bindings.put("settings", settings); Object result = execute.eval(bindings); diff --git a/src/main/java/org/apache/maven/plugins/scripting/engine/JavaEngine.java b/src/main/java/org/apache/maven/plugins/scripting/engine/JavaEngine.java new file mode 100644 index 0000000..fc26af0 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/scripting/engine/JavaEngine.java @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.scripting.engine; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import javax.script.AbstractScriptEngine; +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; +import javax.script.SimpleBindings; +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.stream.Stream; + +import org.apache.maven.plugin.logging.Log; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +/** + * The java engine implementation. + */ +public class JavaEngine extends AbstractScriptEngine implements Compilable { + private final ScriptEngineFactory factory; + + public JavaEngine(ScriptEngineFactory factory) { + this.factory = factory; + } + + @Override + public CompiledScript compile(String script) throws ScriptException { + // plexus compiler is great but overkill there so don't bring it just for that + final JavaCompiler compiler = + requireNonNull(ToolProvider.getSystemJavaCompiler(), "you must run on a JDK to have a compiler"); + Path tmpDir = null; + try { + tmpDir = Files.createTempDirectory(getClass().getSimpleName()); + + final String packageName = getClass().getPackage().getName() + ".generated"; + final String className = "JavaCompiledScript_" + Math.abs(script.hashCode()); + final String source = toSource(packageName, className, script); + final Path src = tmpDir.resolve("sources"); + final Path bin = tmpDir.resolve("bin"); + final Path srcDir = src.resolve(packageName.replace('.', '/')); + Files.createDirectories(srcDir); + Files.createDirectories(bin); + final Path java = srcDir.resolve(className + ".java"); + try (Writer writer = Files.newBufferedWriter(java)) { + writer.write(source); + } + + // TODO: make it configurable from the project in subsequent releases + final String classpath = mavenClasspathPrefix() + + System.getProperty( + getClass().getName() + ".classpath", + System.getProperty("java.class.path", System.getProperty("surefire.real.class.path"))); + + // TODO: use a Logger in subsequent releases. Not very important as of now, so using std streams + final int run = compiler.run( + null, + System.out, + System.err, + Stream.of( + "-classpath", + classpath, + "-sourcepath", + src.toAbsolutePath().toString(), + "-d", + bin.toAbsolutePath().toString(), + java.toAbsolutePath().toString()) + .toArray(String[]::new)); + if (run != 0) { + throw new IllegalArgumentException( + "Can't compile the incoming script, here is the generated code: >\n" + source + "\n<\n"); + } + final URLClassLoader loader = new URLClassLoader( + new URL[] {bin.toUri().toURL()}, Thread.currentThread().getContextClassLoader()); + final Class loadClass = + loader.loadClass(packageName + '.' + className).asSubclass(CompiledScript.class); + return loadClass + .getConstructor(ScriptEngine.class, URLClassLoader.class) + .newInstance(this, loader); + } catch (Exception e) { + throw new ScriptException(e); + } finally { + if (tmpDir != null) { + try { + Files.walkFileTree(tmpDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + if (getLog() != null) { + getLog().debug(e); + } + } + } + } + } + + private String mavenClasspathPrefix() { + final String home = System.getProperty("maven.home"); + if (home == null) { + if (getLog() != null) { + getLog().debug("No maven.home set"); + } + return ""; + } + try (Stream files = Files.list(Paths.get(home).resolve("lib"))) { + return files.filter(it -> { + final String name = it.getFileName().toString(); + return name.startsWith("maven-"); + }) + .map(Path::toString) + .collect(joining(File.pathSeparator, "", File.pathSeparator)); + } catch (IOException e) { + if (getLog() != null) { + getLog().debug(e); + } + return ""; + } + } + + private String toSource(String pck, String name, String script) { + final String[] importsAndScript = splitImportsAndScript(script); + return "package " + pck + ";\n" + + "\n" + + "import java.io.*;\n" + + "import java.net.*;\n" + + "import java.util.*;\n" + + "import java.util.stream.*;\n" + + "import java.nio.file.*;\n" + + "import org.apache.maven.project.MavenProject;\n" + + "import org.apache.maven.plugin.logging.Log;\n" + + "\n" + + "import javax.script.Bindings;\n" + + "import javax.script.CompiledScript;\n" + + "import javax.script.ScriptContext;\n" + + "import javax.script.ScriptEngine;\n" + + "import javax.script.ScriptException;\n" + + "\n" + + importsAndScript[0] + '\n' + + "\n" + + "public class " + name + " extends CompiledScript implements AutoCloseable {\n" + + " private final ScriptEngine $engine;\n" + + " private final URLClassLoader $loader;\n" + + "\n" + + " public " + name + "( ScriptEngine engine, URLClassLoader loader) {\n" + + " this.$engine = engine;\n" + + " this.$loader = loader;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public Object eval( ScriptContext $context) throws ScriptException {\n" + + " final Thread $thread = Thread.currentThread();\n" + + " final ClassLoader $oldClassLoader = $thread.getContextClassLoader();\n" + + " $thread.setContextClassLoader($loader);\n" + + " try {\n" + + " final Bindings $bindings = $context.getBindings(ScriptContext.GLOBAL_SCOPE);\n" + + " final MavenProject $project = MavenProject.class.cast($bindings.get(\"project\"));\n" + + " final Log $log = Log.class.cast($bindings.get(\"log\"));\n" + + " " + importsAndScript[1] + "\n" + + " return null;\n" // assume the script doesn't return anything for now + + " } catch ( Exception e) {\n" + + " if (RuntimeException.class.isInstance(e)) {\n" + + " throw RuntimeException.class.cast(e);\n" + + " }\n" + + " throw new IllegalStateException(e);\n" + + " } finally {\n" + + " $thread.setContextClassLoader($oldClassLoader);\n" + + " }\n" + + " }\n" + + "\n" + + " @Override\n" + + " public ScriptEngine getEngine() {\n" + + " return $engine;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public void close() throws Exception {\n" + + " $loader.close();\n" + + " }\n" + + "}"; + } + + private String[] splitImportsAndScript(String script) { + final StringBuilder imports = new StringBuilder(); + final StringBuilder content = new StringBuilder(); + boolean useImport = true; + boolean inComment = false; + try (BufferedReader reader = new BufferedReader(new StringReader(script))) { + String line; + while ((line = reader.readLine()) != null) { + if (useImport) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + if (trimmed.startsWith("/*")) { + inComment = true; + continue; + } + if (trimmed.endsWith("*/") && inComment) { + inComment = false; + continue; + } + if (inComment) { + continue; + } + if (trimmed.startsWith("import ") && trimmed.endsWith(";")) { + imports.append(line).append('\n'); + continue; + } + useImport = false; + } + content.append(line).append('\n'); + } + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + return new String[] {imports.toString().trim(), content.toString().trim()}; + } + + @Override + public Object eval(String script, ScriptContext context) throws ScriptException { + final CompiledScript compile = compile(script); + try { + return compile.eval(context); + } finally { + doClose(compile); + } + } + + @Override + public Object eval(Reader reader, ScriptContext context) throws ScriptException { + return eval(load(reader), context); + } + + @Override + public CompiledScript compile(Reader script) throws ScriptException { + return compile(load(script)); + } + + @Override + public Bindings createBindings() { + return new SimpleBindings(); + } + + @Override + public ScriptEngineFactory getFactory() { + return factory; + } + + private Log getLog() { + return (Log) this.getContext().getAttribute("log"); + } + + private void doClose(final CompiledScript compile) { + if (!AutoCloseable.class.isInstance(compile)) { + return; + } + try { + AutoCloseable.class.cast(compile).close(); + } catch (Exception e) { + if (getLog() != null) { + getLog().debug(e); + } + } + } + + private String load(Reader reader) { + return new BufferedReader(reader).lines().collect(joining("\n")); + } +} diff --git a/src/main/java/org/apache/maven/plugins/scripting/engine/JavaEngineFactory.java b/src/main/java/org/apache/maven/plugins/scripting/engine/JavaEngineFactory.java new file mode 100644 index 0000000..7d57c68 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/scripting/engine/JavaEngineFactory.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.scripting.engine; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; + +import java.util.List; +import java.util.stream.Stream; + +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +/** + * A java engine factory to be able to script in plain java in the build. + */ +public class JavaEngineFactory implements ScriptEngineFactory { + @Override + public String getEngineName() { + return "Maven-Scripting-Java-Engine"; + } + + @Override + public String getEngineVersion() { + return "1.0"; + } + + @Override + public List getExtensions() { + return singletonList("java"); + } + + @Override + public List getMimeTypes() { + return singletonList("application/java"); + } + + @Override + public List getNames() { + return Stream.concat( + Stream.concat(getMimeTypes().stream(), getExtensions().stream()), + Stream.of(getEngineName(), getLanguageName())) + .distinct() + .collect(toList()); + } + + @Override + public String getLanguageName() { + return "java"; + } + + @Override + public String getLanguageVersion() { + return System.getProperty("java.version", "8"); + } + + @Override + public Object getParameter(String key) { + if (key.equals("javax.script.engine_version")) { + return getEngineVersion(); + } + if (key.equals("javax.script.engine")) { + return getEngineName(); + } + if (key.equals("javax.script.language")) { + return getLanguageName(); + } + if (key.equals("javax.script.language_version")) { + return getLanguageVersion(); + } + return null; + } + + @Override + public String getMethodCallSyntax(String obj, String m, String... args) { + return obj + "." + m + '(' + (args == null ? "" : String.join(", ", args)) + ')'; + } + + @Override + public String getOutputStatement(String toDisplay) { + return "System.out.println(" + toDisplay + ")"; + } + + @Override + public String getProgram(String... statements) { + return Stream.of(statements).collect(joining(";", "", ";")); + } + + @Override + public ScriptEngine getScriptEngine() { + return new JavaEngine(this); + } +} diff --git a/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory b/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory new file mode 100644 index 0000000..d4209f2 --- /dev/null +++ b/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +org.apache.maven.plugins.scripting.engine.JavaEngineFactory diff --git a/src/site/markdown/jsr223-script-engines.md.vm b/src/site/markdown/jsr223-script-engines.md.vm index b051a56..61ab1eb 100644 --- a/src/site/markdown/jsr223-script-engines.md.vm +++ b/src/site/markdown/jsr223-script-engines.md.vm @@ -20,4 +20,23 @@ under the License. | EngineName | Dependency | |------------|----------- | -| groovy | <dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-jsr223</artifactId>
  <version>...</version>
</dependency>
| \ No newline at end of file +| groovy | <dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-jsr223</artifactId>
  <version>...</version>
</dependency>
| +| java | built-in | + +## Java Engine + +The `java` engine enables to execute simple Java scripts. + +It imports by default `java.io`, `java.net`, `java.util`, `java.util.stream`, `java.nio.file`. + +It makes available the following contextual variables: + +* `$log`: maven logger +* `$project`: maven project + +The script is the content of a java method. Example: + +``` +$log.info($project.getArtifactId()); +``` + diff --git a/src/site/markdown/script-context.md b/src/site/markdown/script-context.md index 03ed850..5425422 100644 --- a/src/site/markdown/script-context.md +++ b/src/site/markdown/script-context.md @@ -20,5 +20,9 @@ under the License. The following variables are available in the script context + * `org.apache.maven.execution.MavenSession session` * `org.apache.maven.project.MavenProject project` - * `org.apache.maven.plugin.logging.Log log` \ No newline at end of file + * `org.apache.maven.plugin.descriptor.PluginDescriptor pluginDescriptor` + * `org.apache.maven.plugin.logging.Log log` + * `org.apache.maven.plugin.MojoExecution mojoExecution` + * `org.apache.maven.settings.Settings settings` diff --git a/src/test/java/org/apache/maven/plugins/scripting/engine/JavaEngineTest.java b/src/test/java/org/apache/maven/plugins/scripting/engine/JavaEngineTest.java new file mode 100644 index 0000000..a31404b --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/scripting/engine/JavaEngineTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.scripting.engine; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class JavaEngineTest { + @Test + public void run() throws ScriptException { + assertNull(System.getProperty("JavaEngineTest.run")); + new ScriptEngineManager() + .getEngineByExtension("java") + .eval("System.setProperty(\"JavaEngineTest.run\",\"yes\");"); + assertEquals("yes", System.getProperty("JavaEngineTest.run")); + System.clearProperty("JavaEngineTest.run"); + } +}