1 /*
   2  * Copyright (c) 2016, 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         try {
 210             return new URI("jar:file:" + Paths.get(fileName).toAbsolutePath() + "!/");
 211         } catch (URISyntaxException e) {
 212             throw new InternalError(e);
 213         }
 214     }
 215 
 216     private PathMatcher combine(PathMatcher m1, PathMatcher m2) {
 217         return path -> m1.matches(path) && m2.matches(path);
 218     }
 219 
 220     private Set<Class<?>> scanDirectory(Path classPath, Path combinedPath) {
 221         String dir = options.classpath;
 222 
 223         FileSystem fileSystem = FileSystems.getDefault();
 224         PathMatcher matcher = fileSystem.getPathMatcher("glob:" + "*.class");
 225         FileSystemFinder finder = new FileSystemFinder(combinedPath,
 226             combine(matcher, pathname -> entryIsClassFile(pathname.toString())));
 227 
 228         File file = new File(dir);
 229         try (URLClassLoader loader = URLClassLoader.newInstance(buildUrls(file))) {
 230             return loadClassFiles(classPath, finder, loader);
 231         } catch (IOException e) {
 232             throw new InternalError(e);
 233         }
 234     }
 235 
 236     private Set<Class<?>> loadClassFiles(Path root, FileSystemFinder finder, ClassLoader loader) {
 237         Set<Class<?>> classes = new HashSet<>();
 238         for (Path name : finder.done()) {
 239             // Now relativize to the class path so we get the actual class names.
 240             String entry = root.relativize(name).normalize().toString();
 241             Class<?> c = loadClassFile(loader, entry);
 242             addClass(classes, entry, c);
 243         }
 244         return classes;
 245     }
 246 
 247     private void addClass(Set<Class<?>> classes, String name, Class<?> c) {
 248         if (c != null) {
 249             classes.add(c);
 250             log.printlnVerbose(" loaded " + name);
 251         }
 252     }
 253 
 254     private URL[] buildUrls(String fileName) throws MalformedURLException {
 255         return new URL[]{ new URL("jar:file:" + Paths.get(fileName).toAbsolutePath() + "!/") };
 256     }
 257 
 258     private URL[] buildUrls(File file) throws MalformedURLException {
 259         return new URL[] {file.toURI().toURL() };
 260     }
 261 
 262     private Path getModuleDirectory(String modulepath, String module) {
 263         FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
 264         return fs.getPath(modulepath, module);
 265     }
 266 
 267     /**
 268      * Loads a class with the given file name from the specified {@link URLClassLoader}.
 269      */
 270     private Class<?> loadClassFile(final ClassLoader loader, final String fileName) {
 271         int start = 0;
 272         if (fileName.startsWith("/")) {
 273             start = 1;
 274         }
 275         String className = fileName.substring(start, fileName.length() - ".class".length());
 276         className = className.replace('/', '.');
 277         try {
 278             return loader.loadClass(className);
 279         } catch (Throwable e) {
 280             // If we are running in JCK mode we ignore all exceptions.
 281             if (options.ignoreClassLoadingErrors) {
 282                 log.printError(className + ": " + e);
 283                 return null;
 284             }
 285             throw new InternalError(e);
 286         }
 287     }
 288 
 289     /**
 290      * {@link FileVisitor} implementation to find class files recursively.
 291      */
 292     private static class FileSystemFinder extends SimpleFileVisitor<Path> {
 293         private final ArrayList<Path> fileNames = new ArrayList<>();
 294         private final PathMatcher filter;
 295 
 296         FileSystemFinder(Path combinedPath, PathMatcher filter) {
 297             this.filter = filter;
 298             try {
 299                 Files.walkFileTree(combinedPath, this);
 300             } catch (IOException e) {
 301                 throw new InternalError(e);
 302             }
 303         }
 304 
 305         /**
 306          * Compares the glob pattern against the file name.
 307          */
 308         void find(Path file) {
 309             Path name = file.getFileName();
 310             if (name != null && filter.matches(name)) {
 311                 fileNames.add(file);
 312             }
 313         }
 314 
 315         List<Path> done() {
 316             return fileNames;
 317         }
 318 
 319         @Override
 320         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
 321             find(file);
 322             return CONTINUE;
 323         }
 324 
 325         @Override
 326         public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
 327             find(dir);
 328             return CONTINUE;
 329         }
 330 
 331     }
 332 }