8210839: Improve interaction between source launcher and classpath
authorjjg
Tue, 25 Sep 2018 10:30:32 -0700
changeset 51871 8f66a57054b7
parent 51870 cdfabab3413f
child 51872 a6bdb6d5f167
8210839: Improve interaction between source launcher and classpath Reviewed-by: alanb, mchung
src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java
src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties
test/langtools/tools/javac/launcher/SourceLauncherTest.java
test/langtools/tools/javac/launcher/src/CLTest.java
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java	Tue Sep 25 09:34:51 2018 -0700
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java	Tue Sep 25 10:30:32 2018 -0700
@@ -50,8 +50,6 @@
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -63,6 +61,8 @@
 import java.util.MissingResourceException;
 import java.util.NoSuchElementException;
 import java.util.ResourceBundle;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.NestingKind;
@@ -399,9 +399,6 @@
         ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader());
         try {
             Class<?> appClass = Class.forName(mainClassName, true, cl);
-            if (appClass.getClassLoader() != cl) {
-                throw new Fault(Errors.UnexpectedClass(mainClassName));
-            }
             Method main = appClass.getDeclaredMethod("main", String[].class);
             int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC;
             if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) {
@@ -535,26 +532,103 @@
     }
 
     /**
-     * An in-memory classloader, that uses an in-memory cache written by {@link MemoryFileManager}.
+     * An in-memory classloader, that uses an in-memory cache of classes written by
+     * {@link MemoryFileManager}.
      *
-     * <p>The classloader uses the standard parent-delegation model, just providing
-     * {@code findClass} to find classes in the in-memory cache.
+     * <p>The classloader inverts the standard parent-delegation model, giving preference
+     * to classes defined in the source file before classes known to the parent (such
+     * as any like-named classes that might be found on the application class path.)
      */
     private static class MemoryClassLoader extends ClassLoader {
         /**
-         * The map of classes known to this class loader, indexed by
+         * The map of all classes found in the source file, indexed by
          * {@link ClassLoader#name binary name}.
          */
-        private final Map<String, byte[]> map;
+        private final Map<String, byte[]> sourceFileClasses;
+
+        MemoryClassLoader(Map<String, byte[]> sourceFileClasses, ClassLoader parent) {
+            super(parent);
+            this.sourceFileClasses = sourceFileClasses;
+        }
+
+        /**
+         * Override loadClass to check for classes defined in the source file
+         * before checking for classes in the parent class loader,
+         * including those on the classpath.
+         *
+         * {@code loadClass(String name)} calls this method, and so will have the same behavior.
+         *
+         * @param name the name of the class to load
+         * @param resolve whether or not to resolve the class
+         * @return the class
+         * @throws ClassNotFoundException if the class is not found
+         */
+        @Override
+        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+            synchronized (getClassLoadingLock(name)) {
+                Class<?> c = findLoadedClass(name);
+                if (c == null) {
+                    if (sourceFileClasses.containsKey(name)) {
+                        c = findClass(name);
+                    } else {
+                        c = getParent().loadClass(name);
+                    }
+                    if (resolve) {
+                        resolveClass(c);
+                    }
+                }
+                return c;
+            }
+        }
+
 
-        MemoryClassLoader(Map<String, byte[]> map, ClassLoader parent) {
-            super(parent);
-            this.map = map;
+        /**
+         * Override getResource to check for resources (i.e. class files) defined in the
+         * source file before checking resources in the parent class loader,
+         * including those on the class path.
+         *
+         * {@code getResourceAsStream(String name)} calls this method,
+         * and so will have the same behavior.
+         *
+         * @param name the name of the resource
+         * @return a URL for the resource, or null if not found
+         */
+        @Override
+        public URL getResource(String name) {
+            if (sourceFileClasses.containsKey(toBinaryName(name))) {
+                return findResource(name);
+            } else {
+                return getParent().getResource(name);
+            }
+        }
+
+        /**
+         * Override getResources to check for resources (i.e. class files) defined in the
+         * source file before checking resources in the parent class loader,
+         * including those on the class path.
+         *
+         * @param name the name of the resource
+         * @return an enumeration of the resources in this loader and in the application class loader
+         */
+        @Override
+        public Enumeration<URL> getResources(String name) throws IOException {
+            URL u = findResource(name);
+            Enumeration<URL> e = getParent().getResources(name);
+            if (u == null) {
+                return e;
+            } else {
+                List<URL> list = new ArrayList<>();
+                list.add(u);
+                while (e.hasMoreElements()) {
+                    list.add(e.nextElement());
+                }
+                return Collections.enumeration(list);
+            }
         }
 
         @Override
         protected Class<?> findClass(String name) throws ClassNotFoundException {
-            byte[] bytes = map.get(name);
+            byte[] bytes = sourceFileClasses.get(name);
             if (bytes == null) {
                 throw new ClassNotFoundException(name);
             }
@@ -564,7 +638,7 @@
         @Override
         public URL findResource(String name) {
             String binaryName = toBinaryName(name);
-            if (binaryName == null || map.get(binaryName) == null) {
+            if (binaryName == null || sourceFileClasses.get(binaryName) == null) {
                 return null;
             }
 
@@ -628,7 +702,7 @@
                 if (!u.getProtocol().equalsIgnoreCase(PROTOCOL)) {
                     throw new IllegalArgumentException(u.toString());
                 }
-                return new MemoryURLConnection(u, map.get(toBinaryName(u.getPath())));
+                return new MemoryURLConnection(u, sourceFileClasses.get(toBinaryName(u.getPath())));
             }
 
         }
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties	Tue Sep 25 09:34:51 2018 -0700
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties	Tue Sep 25 10:30:32 2018 -0700
@@ -109,10 +109,6 @@
     can''t find class: {0}
 
 # 0: string
-launcher.err.unexpected.class=\
-    class found on application class path: {0}
-
-# 0: string
 launcher.err.cant.find.main.method=\
     can''t find main(String[]) method in class: {0}
 
--- a/test/langtools/tools/javac/launcher/SourceLauncherTest.java	Tue Sep 25 09:34:51 2018 -0700
+++ b/test/langtools/tools/javac/launcher/SourceLauncherTest.java	Tue Sep 25 10:30:32 2018 -0700
@@ -47,6 +47,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Properties;
+import java.util.regex.Pattern;
 
 import com.sun.tools.javac.launcher.Main;
 
@@ -233,22 +234,98 @@
     }
 
     @Test
-    public void testWrongClass(Path base) throws IOException {
+    public void testLoadClass(Path base) throws IOException {
+        Path src1 = base.resolve("src1");
+        Path file1 = src1.resolve("LoadClass.java");
+        tb.writeJavaFiles(src1,
+                "class LoadClass {\n"
+                + "    public static void main(String... args) {\n"
+                + "        System.out.println(\"on classpath\");\n"
+                + "    };\n"
+                + "}\n");
+        Path classes1 = Files.createDirectories(base.resolve("classes"));
+        new JavacTask(tb)
+                .outdir(classes1)
+                .files(file1)
+                .run();
+        String log1 = new JavaTask(tb)
+                .classpath(classes1.toString())
+                .className("LoadClass")
+                .run(Task.Expect.SUCCESS)
+                .getOutput(Task.OutputKind.STDOUT);
+        checkEqual("stdout", log1.trim(),
+                "on classpath");
+
+        Path src2 = base.resolve("src2");
+        Path file2 = src2.resolve("LoadClass.java");
+        tb.writeJavaFiles(src2,
+                "class LoadClass {\n"
+                + "    public static void main(String... args) {\n"
+                + "        System.out.println(\"in source file\");\n"
+                + "    };\n"
+                + "}\n");
+        String log2 = new JavaTask(tb)
+                .classpath(classes1.toString())
+                .className(file2.toString())
+                .run(Task.Expect.SUCCESS)
+                .getOutput(Task.OutputKind.STDOUT);
+        checkEqual("stdout", log2.trim(),
+                "in source file");
+    }
+
+    @Test
+    public void testGetResource(Path base) throws IOException {
         Path src = base.resolve("src");
-        Path file = src.resolve("WrongClass.java");
-        tb.writeJavaFiles(src, "class WrongClass { }");
+        Path file = src.resolve("GetResource.java");
+        tb.writeJavaFiles(src,
+                "class GetResource {\n"
+                + "    public static void main(String... args) {\n"
+                + "        System.out.println(GetResource.class.getClassLoader().getResource(\"GetResource.class\"));\n"
+                + "    };\n"
+                + "}\n");
         Path classes = Files.createDirectories(base.resolve("classes"));
         new JavacTask(tb)
                 .outdir(classes)
                 .files(file)
                 .run();
+
         String log = new JavaTask(tb)
                 .classpath(classes.toString())
                 .className(file.toString())
-                .run(Task.Expect.FAIL)
-                .getOutput(Task.OutputKind.STDERR);
-        checkEqual("stderr", log.trim(),
-                "error: class found on application class path: WrongClass");
+                .run(Task.Expect.SUCCESS)
+                .getOutput(Task.OutputKind.STDOUT);
+        checkMatch("stdout", log.trim(),
+                Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResource.class"));
+    }
+
+    @Test
+    public void testGetResources(Path base) throws IOException {
+        Path src = base.resolve("src");
+        Path file = src.resolve("GetResources.java");
+        tb.writeJavaFiles(src,
+                "import java.io.*; import java.net.*; import java.util.*;\n"
+                + "class GetResources {\n"
+                + "    public static void main(String... args) throws IOException {\n"
+                + "        Enumeration<URL> e =\n"
+                + "            GetResources.class.getClassLoader().getResources(\"GetResources.class\");\n"
+                + "        while (e.hasMoreElements()) System.out.println(e.nextElement());\n"
+                + "    };\n"
+                + "}\n");
+        Path classes = Files.createDirectories(base.resolve("classes"));
+        new JavacTask(tb)
+                .outdir(classes)
+                .files(file)
+                .run();
+
+        List<String> log = new JavaTask(tb)
+                .classpath(classes.toString())
+                .className(file.toString())
+                .run(Task.Expect.SUCCESS)
+                .getOutputLines(Task.OutputKind.STDOUT);
+        checkMatch("stdout:0", log.get(0).trim(),
+                Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResources.class"));
+        checkMatch("stdout:1", log.get(1).trim(),
+                Pattern.compile("file:/.*/testGetResources/classes/GetResources.class"));
     }
 
     @Test
@@ -294,7 +371,37 @@
         checkEmpty("stdout", r.stdOut);
         checkEqual("stderr", r.stdErr, expectStdErr);
         checkFault("exception", r.exception, "error: compilation failed");
+    }
 
+    @Test
+    public void testClassNotFound(Path base) throws IOException {
+        Path src = base.resolve("src");
+        Path file = src.resolve("ClassNotFound.java");
+        tb.writeJavaFiles(src,
+                "class ClassNotFound {\n"
+                + "    public static void main(String... args) {\n"
+                + "        try {\n"
+                + "            Class.forName(\"NoSuchClass\");\n"
+                + "            System.out.println(\"no exception\");\n"
+                + "            System.exit(1);\n"
+                + "        } catch (ClassNotFoundException e) {\n"
+                + "            System.out.println(\"Expected exception thrown: \" + e);\n"
+                + "        }\n"
+                + "    };\n"
+                + "}\n");
+        Path classes = Files.createDirectories(base.resolve("classes"));
+        new JavacTask(tb)
+                .outdir(classes)
+                .files(file)
+                .run();
+
+        String log = new JavaTask(tb)
+                .classpath(classes.toString())
+                .className(file.toString())
+                .run(Task.Expect.SUCCESS)
+                .getOutput(Task.OutputKind.STDOUT);
+        checkEqual("stdout", log.trim(),
+                "Expected exception thrown: java.lang.ClassNotFoundException: NoSuchClass");
     }
 
     // For any source file that is invoked through the OS shebang mechanism, invalid shebang
@@ -471,12 +578,18 @@
     void checkEqual(String name, String found, String expect) {
         expect = expect.replace("\n", tb.lineSeparator);
         out.println(name + ": " + found);
-        out.println(name + ": " + found);
         if (!expect.equals(found)) {
             error("Unexpected output; expected: " + expect);
         }
     }
 
+    void checkMatch(String name, String found, Pattern expect) {
+        out.println(name + ": " + found);
+        if (!expect.matcher(found).matches()) {
+            error("Unexpected output; expected match for: " + expect);
+        }
+    }
+
     void checkEmpty(String name, String found) {
         out.println(name + ": " + found);
         if (!found.isEmpty()) {
--- a/test/langtools/tools/javac/launcher/src/CLTest.java	Tue Sep 25 09:34:51 2018 -0700
+++ b/test/langtools/tools/javac/launcher/src/CLTest.java	Tue Sep 25 10:30:32 2018 -0700
@@ -43,38 +43,38 @@
 import com.sun.tools.classfile.ClassFile;
 
 public class CLTest {
-        public static void main(String... args) throws Exception {
-                try {
-                        new CLTest().run();
-                } catch (Throwable t) {
-                        t.printStackTrace();
-                        System.exit(1);
-                }
+    public static void main(String... args) throws Exception {
+        try {
+            new CLTest().run();
+        } catch (Throwable t) {
+            t.printStackTrace();
+            System.exit(1);
+        }
+    }
+
+    void run() throws Exception {
+        String[] names = {
+                "p/q/CLTest.class",
+                "p/q/CLTest$Inner.class",
+                "p/q/CLTest2.class",
+                "java/lang/Object.class",
+                "UNKNOWN.class",
+                "UNKNOWN"
+        };
+
+        for (String name : names) {
+            testGetResource(name);
+            testGetResources(name);
+            testGetResourceAsStream(name);
         }
 
-        void run() throws Exception {
-                String[] names = {
-                                "p/q/CLTest.class",
-                                "p/q/CLTest$Inner.class",
-                                "p/q/CLTest2.class",
-                                "java/lang/Object.class",
-                                "UNKNOWN.class",
-                                "UNKNOWN"
-                };
-
-                for (String name : names) {
-                        testGetResource(name);
-                        testGetResources(name);
-                        testGetResourceAsStream(name);
-                }
-
-                if (errors > 0) {
-                        throw new Exception(errors + " errors found");
-                }
+        if (errors > 0) {
+            throw new Exception(errors + " errors found");
         }
+    }
 
     void testGetResource(String name) {
-                System.err.println("testGetResource: " + name);
+        System.err.println("testGetResource: " + name);
         try {
             ClassLoader cl = getClass().getClassLoader();
             URL u = cl.getResource(name);
@@ -95,7 +95,7 @@
     }
 
     void testGetResources(String name) {
-                System.err.println("testGetResources: " + name);
+        System.err.println("testGetResources: " + name);
         try {
             ClassLoader cl = getClass().getClassLoader();
             Enumeration<URL> e = cl.getResources(name);