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()
+        '''
+    }
+}

Reply via email to