1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package jdk.tools.jaotc.collect;
  25 
  26 import jdk.tools.jaotc.LogPrinter;
  27 import jdk.tools.jaotc.Main;
  28 
  29 import java.io.File;
  30 import java.io.IOException;
  31 import java.net.*;
  32 import java.nio.file.*;
  33 import java.nio.file.attribute.BasicFileAttributes;
  34 import java.util.*;
  35 
  36 import static java.nio.file.FileVisitResult.CONTINUE;
  37 
  38 public class ClassCollector {
  39     private final Main.Options options;
  40     private final LogPrinter log;
  41 
  42     public ClassCollector(Main.Options options, LogPrinter log) {
  43         this.options = options;
  44         this.log = log;
  45     }
  46 
  47     /**
  48      * Collect all class names passed by the user.
  49      *
  50      * @return array list of classes
  51      */
  52     public Set<Class<?>> collectClassesToCompile() {
  53         Set<Class<?>> classes = new HashSet<>();
  54         List<String> filesToScan = new LinkedList<>(options.files);
  55 
  56         if (options.module != null) {
  57             classes.addAll(scanModule(filesToScan));
  58         }
  59 
  60         classes.addAll(scanFiles(filesToScan));
  61         return classes;
  62     }
  63 
  64     private Set<Class<?>> scanModule(List<String> filesToScan) {
  65         String module = options.module;
  66         // Search module in standard JDK installation.
  67         Path dir = getModuleDirectory(options.modulepath, module);
  68 
  69         if (Files.isDirectory(dir)) {
  70             return loadFromModuleDirectory(dir);
  71         } else {
  72             findFilesToScan(filesToScan, module);
  73             return new HashSet<>();
  74         }
  75     }
  76 
  77     private Set<Class<?>> loadFromModuleDirectory(Path dir) {
  78         log.printInfo("Scanning module: " + dir + " ...");
  79         log.printlnVerbose(" "); // Break line
  80 
  81         FileSystemFinder finder = new FileSystemFinder(dir, pathname -> entryIsClassFile(pathname.toString()));
  82         Set<Class<?>> cls = loadWithClassLoader(() -> ClassLoader.getSystemClassLoader(), dir, finder);
  83         log.printlnInfo(" " + cls.size() + " classes loaded.");
  84         return cls;
  85     }
  86 
  87     private void findFilesToScan(List<String> filesToScan, String module) {
  88         // Try to search regular directory, .jar or .class files
  89         Path path = Paths.get(options.modulepath, module);
  90 
  91         if (Files.isDirectory(path)) {
  92             filesToScan.add(".");
  93             options.classpath = path.toString();
  94         } else if (path.endsWith(".jar") || path.endsWith(".class")) {
  95             filesToScan.add(path.toString());
  96         } else {
  97             path = Paths.get(options.modulepath, module + ".jar");
  98             if (Files.exists(path)) {
  99                 filesToScan.add(path.toString());
 100             } else {
 101                 path = Paths.get(options.modulepath, module + ".class");
 102                 if (Files.exists(path)) {
 103                     filesToScan.add(path.toString());
 104                 } else {
 105                     throw new InternalError("Expecting a .class, .jar or directory: " + path);
 106                 }
 107             }
 108         }
 109     }
 110 
 111     private boolean entryIsClassFile(String entry) {
 112         return entry.endsWith(".class") && !entry.endsWith("module-info.class");
 113     }
 114 
 115     private Set<Class<?>> scanFiles(List<String> filesToScan) {
 116         Set<Class<?>> classes = new HashSet<>();
 117         for (String fileName : filesToScan) {
 118             Set<Class<?>> loaded = scanFile(fileName);
 119             log.printlnInfo(" " + loaded.size() + " classes loaded.");
 120             classes.addAll(loaded);
 121         }
 122         return classes;
 123     }
 124 
 125     interface ClassLoaderFactory {
 126         ClassLoader create() throws IOException;
 127     }
 128 
 129     private Set<Class<?>> loadWithClassLoader(ClassLoaderFactory factory, Path root, FileSystemFinder finder) {
 130         ClassLoader loader = null;
 131         try {
 132             loader = factory.create();
 133             return loadClassFiles(root, finder, loader);
 134         } catch (IOException e) {
 135             throw new InternalError(e);
 136         } finally {
 137             if (loader instanceof AutoCloseable) {
 138                 try {
 139                     ((AutoCloseable) loader).close();
 140                 } catch (Exception e) {
 141                     throw new InternalError(e);
 142                 }
 143             }
 144         }
 145     }
 146 
 147     private Set<Class<?>> scanFile(String fileName) {
 148         log.printInfo("Scanning: " + fileName + " ...");
 149         log.printlnVerbose(" "); // Break line
 150 
 151         if (fileName.endsWith(".jar")) {
 152             return loadFromJarFile(fileName);
 153         } else if (fileName.endsWith(".class")) {
 154             Set<Class<?>> classes = new HashSet<>();
 155             loadFromClassFile(fileName, classes);
 156             return classes;
 157         } else {
 158             return scanClassPath(fileName);
 159         }
 160     }
 161 
 162     private Set<Class<?>> loadFromJarFile(String fileName) {
 163         FileSystem fs = makeFileSystem(fileName);
 164         FileSystemFinder finder = new FileSystemFinder(fs.getPath("/"), pathname -> entryIsClassFile(pathname.toString()));
 165         return loadWithClassLoader(() -> URLClassLoader.newInstance(buildUrls(fileName)), fs.getPath("/"), finder);
 166     }
 167 
 168     private void loadFromClassFile(String fileName, Set<Class<?>> classes) {
 169         Class<?> result;
 170         File file = new File(options.classpath);
 171         try (URLClassLoader loader = URLClassLoader.newInstance(buildUrls(file))) {
 172             result = loadClassFile(loader, fileName);
 173         } catch (IOException e) {
 174             throw new InternalError(e);
 175         }
 176         Class<?> c = result;
 177         addClass(classes, fileName, c);
 178     }
 179 
 180     private Set<Class<?>> scanClassPath(String fileName) {
 181         Path classPath = Paths.get(options.classpath);
 182         if (!Files.exists(classPath)) {
 183             throw new InternalError("Path does not exist: " + classPath);
 184         }
 185         if (!Files.isDirectory(classPath)) {
 186             throw new InternalError("Path must be a directory: " + classPath);
 187         }
 188 
 189         // Combine class path and file name and see what it is.
 190         Path combinedPath = Paths.get(options.classpath + File.separator + fileName);
 191         if (combinedPath.endsWith(".class")) {
 192             throw new InternalError("unimplemented");
 193         } else if (Files.isDirectory(combinedPath)) {
 194             return scanDirectory(classPath, combinedPath);
 195         } else {
 196             throw new InternalError("Expecting a .class, .jar or directory: " + fileName);
 197         }
 198     }
 199 
 200     private FileSystem makeFileSystem(String fileName) {
 201         try {
 202             return FileSystems.newFileSystem(makeJarFileURI(fileName), new HashMap<>());
 203         } catch (IOException e) {
 204             throw new InternalError(e);
 205         }
 206     }
 207 
 208     private URI makeJarFileURI(String fileName) {
 209         String name = Paths.get(fileName).toAbsolutePath().toString();
 210         name = name.replace('\\', '/');
 211         try {
 212             return new URI("jar:file:///" + name + "!/");
 213         } catch (URISyntaxException e) {
 214             throw new InternalError(e);
 215         }
 216     }
 217 
 218     private PathMatcher combine(PathMatcher m1, PathMatcher m2) {
 219         return path -> m1.matches(path) && m2.matches(path);
 220     }
 221 
 222     private Set<Class<?>> scanDirectory(Path classPath, Path combinedPath) {
 223         String dir = options.classpath;
 224 
 225         FileSystem fileSystem = FileSystems.getDefault();
 226         PathMatcher matcher = fileSystem.getPathMatcher("glob:" + "*.class");
 227         FileSystemFinder finder = new FileSystemFinder(combinedPath,
 228             combine(matcher, pathname -> entryIsClassFile(pathname.toString())));
 229 
 230         File file = new File(dir);
 231         try (URLClassLoader loader = URLClassLoader.newInstance(buildUrls(file))) {
 232             return loadClassFiles(classPath, finder, loader);
 233         } catch (IOException e) {
 234             throw new InternalError(e);
 235         }
 236     }
 237 
 238     private Set<Class<?>> loadClassFiles(Path root, FileSystemFinder finder, ClassLoader loader) {
 239         Set<Class<?>> classes = new HashSet<>();
 240         for (Path name : finder.done()) {
 241             // Now relativize to the class path so we get the actual class names.
 242             String entry = root.relativize(name).normalize().toString();
 243             Class<?> c = loadClassFile(loader, entry);
 244             addClass(classes, entry, c);
 245         }
 246         return classes;
 247     }
 248 
 249     private void addClass(Set<Class<?>> classes, String name, Class<?> c) {
 250         if (c != null) {
 251             classes.add(c);
 252             log.printlnVerbose(" loaded " + name);
 253         }
 254     }
 255 
 256     private URL[] buildUrls(String fileName) throws MalformedURLException {
 257         String name = Paths.get(fileName).toAbsolutePath().toString();
 258         name = name.replace('\\', '/');
 259         return new URL[]{ new URL("jar:file:///" + name + "!/") };
 260     }
 261 
 262     private URL[] buildUrls(File file) throws MalformedURLException {
 263         return new URL[] {file.toURI().toURL() };
 264     }
 265 
 266     private Path getModuleDirectory(String modulepath, String module) {
 267         FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
 268         return fs.getPath(modulepath, module);
 269     }
 270 
 271     /**
 272      * Loads a class with the given file name from the specified {@link URLClassLoader}.
 273      */
 274     private Class<?> loadClassFile(final ClassLoader loader, final String fileName) {
 275         int start = 0;
 276         if (fileName.startsWith("/")) {
 277             start = 1;
 278         }
 279         String className = fileName.substring(start, fileName.length() - ".class".length());
 280         className = className.replace('/', '.');
 281         className = className.replace('\\', '.');
 282         try {
 283             return loader.loadClass(className);
 284         } catch (Throwable e) {
 285             // If we are running in JCK mode we ignore all exceptions.
 286             if (options.ignoreClassLoadingErrors) {
 287                 log.printError(className + ": " + e);
 288                 return null;
 289             }
 290             throw new InternalError(e);
 291         }
 292     }
 293 
 294     /**
 295      * {@link FileVisitor} implementation to find class files recursively.
 296      */
 297     private static class FileSystemFinder extends SimpleFileVisitor<Path> {
 298         private final ArrayList<Path> fileNames = new ArrayList<>();
 299         private final PathMatcher filter;
 300 
 301         FileSystemFinder(Path combinedPath, PathMatcher filter) {
 302             this.filter = filter;
 303             try {
 304                 Files.walkFileTree(combinedPath, this);
 305             } catch (IOException e) {
 306                 throw new InternalError(e);
 307             }
 308         }
 309 
 310         /**
 311          * Compares the glob pattern against the file name.
 312          */
 313         void find(Path file) {
 314             Path name = file.getFileName();
 315             if (name != null && filter.matches(name)) {
 316                 fileNames.add(file);
 317             }
 318         }
 319 
 320         List<Path> done() {
 321             return fileNames;
 322         }
 323 
 324         @Override
 325         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
 326             find(file);
 327             return CONTINUE;
 328         }
 329 
 330         @Override
 331         public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
 332             find(dir);
 333             return CONTINUE;
 334         }
 335 
 336     }
 337 }