This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new aa274f8 GROOVY-9365: Implement JavaShell to run Java code
aa274f8 is described below
commit aa274f8e582834fe3cd970e4fd1e687a5198ee9a
Author: Daniel.Sun <[email protected]>
AuthorDate: Fri Jan 17 08:07:51 2020 +0800
GROOVY-9365: Implement JavaShell to run Java code
---
.../java/org/apache/groovy/util/JavaShell.java | 241 +++++++++++++++++++++
.../groovy/tools/javac/MemJavaFileObject.java | 28 ++-
.../org/apache/groovy/util/JavaShellTest.groovy | 99 +++++++++
3 files changed, 357 insertions(+), 11 deletions(-)
diff --git a/src/main/java/org/apache/groovy/util/JavaShell.java
b/src/main/java/org/apache/groovy/util/JavaShell.java
new file mode 100644
index 0000000..ee1bd21
--- /dev/null
+++ b/src/main/java/org/apache/groovy/util/JavaShell.java
@@ -0,0 +1,241 @@
+/*
+ * 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.groovy.util;
+
+import groovy.lang.GroovyRuntimeException;
+import org.apache.groovy.io.StringBuilderWriter;
+import org.apache.groovy.lang.annotation.Incubating;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.tools.javac.MemJavaFileObject;
+
+import javax.tools.FileObject;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A shell for compiling or running pure Java code
+ */
+@Incubating
+public class JavaShell {
+ private static final String MAIN_METHOD_NAME = "main";
+ private static final URL[] EMPTY_URL_ARRAY = new URL[0];
+ private final JavaShellClassLoader jscl;
+ private final Locale locale;
+ private final Charset charset;
+
+ /**
+ * Initializes a newly created {@code JavaShell} object
+ */
+ public JavaShell() {
+ this(null);
+ }
+
+ /**
+ * Initializes a newly created {@code JavaShell} object
+ *
+ * @param parentClassLoader the parent class loader for delegation
+ */
+ public JavaShell(ClassLoader parentClassLoader) {
+ jscl = new JavaShellClassLoader(
+ EMPTY_URL_ARRAY,
+ null == parentClassLoader
+ ? JavaShell.class.getClassLoader()
+ : parentClassLoader
+ );
+
+ locale = Locale.ENGLISH;
+ charset =
Charset.forName(CompilerConfiguration.DEFAULT.getSourceEncoding());
+ }
+
+ /**
+ * Run main method
+ *
+ * @param className the main class name
+ * @param src the source code
+ * @param args arguments for main method
+ * @throws Throwable
+ */
+ public void runMain(String className, String src, String... args) throws
Throwable {
+ Class<?> c = compile(className, src);
+ Method mainMethod = c.getMethod(MAIN_METHOD_NAME, String[].class);
+ mainMethod.invoke(null, (Object) args);
+ }
+
+ /**
+ * Compile and return the main class
+ * @param className the main class name
+ * @param src the source code
+ * @return the main class
+ * @throws IOException
+ * @throws ClassNotFoundException
+ */
+ public Class<?> compile(final String className, String src) throws
IOException, ClassNotFoundException {
+ return compileAll(className, src).get(className);
+ }
+
+ /**
+ * Compile and return all classes
+ *
+ * @param className the main class name
+ * @param src the source code
+ * @return all classes
+ * @throws IOException
+ * @throws ClassNotFoundException
+ */
+ public Map<String, Class<?>> compileAll(final String className, String
src) throws IOException, ClassNotFoundException {
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ try (BytesJavaFileManager bjfm = new
BytesJavaFileManager(compiler.getStandardFileManager(null, locale, charset))) {
+ StringBuilderWriter out = new StringBuilderWriter();
+ JavaCompiler.CompilationTask task =
+ compiler.getTask(
+ out,
+ bjfm,
+ null,
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.singletonList(
+ new MemJavaFileObject(className, src)
+ )
+ );
+
+ task.call();
+
+ if (bjfm.isEmpty()) {
+ throw new GroovyRuntimeException(out.toString());
+ }
+
+ final Map<String, byte[]> classMap = bjfm.getClassMap();
+
+ jscl.setClassMap(classMap);
+
+ Map<String, Class<?>> classes = new LinkedHashMap<>();
+ for (String cn : classMap.keySet()) {
+ Class<?> c = jscl.findClass(cn);
+ classes.put(cn, c);
+ }
+
+ return classes;
+ }
+ }
+
+ /**
+ * When and only when {@link #compile(String, String)} or {@link
#compileAll(String, String)} is invoked,
+ * returned class loader will reference the compiled classes.
+ */
+ public JavaShellClassLoader getClassLoader() {
+ return jscl;
+ }
+
+ private static final class JavaShellClassLoader extends URLClassLoader {
+ private Map<String, byte[]> classMap = Collections.emptyMap();
+ private final Map<String, Class<?>> classCache = new
ConcurrentHashMap<>();
+
+ public JavaShellClassLoader(URL[] urls, ClassLoader parent) {
+ super(urls, parent);
+ }
+
+ @Override
+ public Class<?> findClass(String name) throws ClassNotFoundException {
+ final byte[] bytes = classMap.get(name);
+
+ if (null != bytes) {
+ return classCache.computeIfAbsent(name, n -> defineClass(n,
bytes, 0, bytes.length));
+ }
+
+ return super.findClass(name);
+ }
+
+ public Map<String, byte[]> getClassMap() {
+ return classMap;
+ }
+
+ public void setClassMap(Map<String, byte[]> classMap) {
+ this.classMap = classMap;
+ }
+ }
+
+ private static final class BytesJavaFileManager extends
ForwardingJavaFileManager<StandardJavaFileManager> {
+ private final Map<String, BytesJavaFileObject> fileObjectMap = new
HashMap<>();
+ private Map<String, byte[]> classMap;
+
+ public BytesJavaFileManager(StandardJavaFileManager sjfm) {
+ super(sjfm);
+ }
+
+ public boolean isEmpty() {
+ return fileObjectMap.isEmpty();
+ }
+
+ @Override
+ public JavaFileObject getJavaFileForOutput(
+ JavaFileManager.Location location,
+ String className,
+ JavaFileObject.Kind kind,
+ FileObject sibling) {
+ BytesJavaFileObject bjfo = new BytesJavaFileObject(className,
kind);
+ fileObjectMap.put(className, bjfo);
+ return bjfo;
+ }
+
+ public Map<String, byte[]> getClassMap() {
+ if (classMap != null) return classMap;
+
+ classMap = new LinkedHashMap<>();
+ fileObjectMap.forEach((key, value) -> classMap.put(key,
value.getBytes()));
+
+ return Collections.unmodifiableMap(classMap);
+ }
+ }
+
+ private static class BytesJavaFileObject extends SimpleJavaFileObject {
+ private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ public BytesJavaFileObject(String name, Kind kind) {
+ super(URI.create("string:///" + name.replace('.', '/') +
kind.extension), kind);
+ }
+
+ @Override
+ public OutputStream openOutputStream() {
+ return baos;
+ }
+
+ public byte[] getBytes() {
+ return baos.toByteArray();
+ }
+ }
+}
diff --git
a/src/main/java/org/codehaus/groovy/tools/javac/MemJavaFileObject.java
b/src/main/java/org/codehaus/groovy/tools/javac/MemJavaFileObject.java
index 3ae8fbd..7705248 100644
--- a/src/main/java/org/codehaus/groovy/tools/javac/MemJavaFileObject.java
+++ b/src/main/java/org/codehaus/groovy/tools/javac/MemJavaFileObject.java
@@ -21,7 +21,6 @@ package org.codehaus.groovy.tools.javac;
import groovy.lang.GroovyRuntimeException;
import org.codehaus.groovy.ast.ClassNode;
-import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import java.net.URI;
import java.net.URISyntaxException;
@@ -36,23 +35,30 @@ public class MemJavaFileObject extends SimpleJavaFileObject
{
private final String src;
/**
- * Construct a MemJavaFileObject instance with given groovy class node and
stub source code
+ * Construct a MemJavaFileObject instance with given class node and source
code
*
- * @param classNode the groovy class node
- * @param src the stub source code
+ * @param classNode the class node
+ * @param src the source code
*/
public MemJavaFileObject(ClassNode classNode, String src) {
- super(createURI(classNode), JavaFileObject.Kind.SOURCE);
- this.className = classNode.getName();
+ this(classNode.getName(), src);
+ }
+
+ /**
+ * Construct a MemJavaFileObject instance with given class name and source
code
+ *
+ * @param className the class name
+ * @param src the source code
+ */
+ public MemJavaFileObject(String className, String src) {
+ super(createURI(className), Kind.SOURCE);
+ this.className = className;
this.src = src;
}
- private static URI createURI(ClassNode classNode) {
+ private static URI createURI(String className) {
try {
- String packageName = classNode.getPackageName();
- String className = classNode.getNameWithoutPackage();
-
- return new URI("string:///" + (null == packageName ? "" :
(packageName.replace('.', '/') + "/")) + className + ".java");
+ return new URI("string:///" + className.replace('.', '/') +
Kind.SOURCE.extension);
} catch (URISyntaxException e) {
throw new GroovyRuntimeException(e);
}
diff --git a/src/test/org/apache/groovy/util/JavaShellTest.groovy
b/src/test/org/apache/groovy/util/JavaShellTest.groovy
new file mode 100644
index 0000000..70fa388
--- /dev/null
+++ b/src/test/org/apache/groovy/util/JavaShellTest.groovy
@@ -0,0 +1,99 @@
+/*
+ * 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.groovy.util
+
+import org.junit.Test
+
+class JavaShellTest {
+ @Test
+ void compileAll() {
+ JavaShell js = new JavaShell()
+ final mcn = "tests.Test1"
+ final cn = "tests.TestHelper"
+ Map<String, Class<?>> classes = js.compileAll(mcn, '''
+ package tests;
+ public class Test1 {}
+ class TestHelper {}
+ ''')
+
+ assert 2 == classes.size()
+ assert mcn == classes.get(mcn).getName()
+ assert cn == classes.get(cn).getName()
+ }
+
+ @Test
+ void compile() {
+ JavaShell js = new JavaShell()
+ final mcn = "tests.Test1"
+ Class<?> c = js.compile(mcn, '''
+ package tests;
+ public class Test1 {
+ public static String test() { return "Hello"; }
+ }
+ ''')
+
+ Object result = c.getDeclaredMethod("test").invoke(null)
+ assert "Hello" == result
+ }
+
+ @Test
+ void runMain() {
+ JavaShell js = new JavaShell()
+ final mcn = "tests.Test1"
+ try {
+ js.runMain(mcn, '''
+ package tests;
+ public class Test1 {
+ public static void main(String[] args) {
+ throw new RuntimeException(TestHelper.msg());
+ }
+ }
+ class TestHelper {
+ static String msg() { return "Boom"; }
+ }
+ ''')
+ } catch (Throwable t) {
+ assert t.getCause().getMessage().contains("Boom")
+ }
+ }
+
+ @Test
+ void getClassLoader() {
+ JavaShell js = new JavaShell()
+ final mcn = "tests.Test1"
+ js.compile(mcn, '''
+ package tests;
+ public class Test1 {
+ public static String test() { return TestHelper.msg(); }
+ }
+
+ class TestHelper {
+ public static String msg() {
+ return "Hello, " +
groovy.lang.GString.class.getSimpleName();
+ }
+ }
+ ''')
+
+ new GroovyShell(js.getClassLoader()).evaluate '''
+ import tests.Test1
+
+ assert 'Hello, GString' == Test1.test()
+ '''
+ }
+}