1 /*
   2  * Copyright (c) 2014, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package java.lang.module;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.BufferedReader;
  30 import java.io.File;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.InputStreamReader;
  34 import java.io.UncheckedIOException;
  35 import java.lang.module.ModuleDescriptor.Requires;
  36 import java.nio.file.DirectoryStream;
  37 import java.nio.file.Files;
  38 import java.nio.file.NoSuchFileException;
  39 import java.nio.file.Path;
  40 import java.nio.file.attribute.BasicFileAttributes;
  41 import java.util.Collections;
  42 import java.util.HashMap;
  43 import java.util.HashSet;
  44 import java.util.Map;
  45 import java.util.Objects;
  46 import java.util.Optional;
  47 import java.util.Set;
  48 import java.util.jar.Attributes;
  49 import java.util.jar.JarEntry;
  50 import java.util.jar.JarFile;
  51 import java.util.jar.Manifest;
  52 import java.util.regex.Matcher;
  53 import java.util.regex.Pattern;
  54 import java.util.stream.Collectors;
  55 import java.util.stream.Stream;
  56 import java.util.zip.ZipEntry;
  57 import java.util.zip.ZipFile;
  58 
  59 import jdk.internal.module.Checks;
  60 import jdk.internal.module.ConfigurableModuleFinder;
  61 import jdk.internal.perf.PerfCounter;
  62 
  63 
  64 /**
  65  * A {@code ModuleFinder} that locates modules on the file system by searching
  66  * a sequence of directories or packaged modules.
  67  *
  68  * The {@code ModuleFinder} can be configured to work in either the run-time
  69  * or link-time phases. In both cases it locates modular JAR and exploded
  70  * modules. When configured for link-time then it additionally locates
  71  * modules in JMOD files.
  72  */
  73 
  74 class ModulePath implements ConfigurableModuleFinder {
  75     private static final String MODULE_INFO = "module-info.class";
  76 
  77     // the entries on this module path
  78     private final Path[] entries;
  79     private int next;
  80 
  81     // true if in the link phase
  82     private boolean isLinkPhase;
  83 
  84     // map of module name to module reference map for modules already located
  85     private final Map<String, ModuleReference> cachedModules = new HashMap<>();
  86 
  87     ModulePath(Path... entries) {
  88         this.entries = entries.clone();
  89         for (Path entry : this.entries) {
  90             Objects.requireNonNull(entry);
  91         }
  92     }
  93 
  94     @Override
  95     public void configurePhase(Phase phase) {
  96         isLinkPhase = (phase == Phase.LINK_TIME);
  97     }
  98 
  99     @Override
 100     public Optional<ModuleReference> find(String name) {
 101         Objects.requireNonNull(name);
 102 
 103         // try cached modules
 104         ModuleReference m = cachedModules.get(name);
 105         if (m != null)
 106             return Optional.of(m);
 107 
 108         // the module may not have been encountered yet
 109         while (hasNextEntry()) {
 110             scanNextEntry();
 111             m = cachedModules.get(name);
 112             if (m != null)
 113                 return Optional.of(m);
 114         }
 115         return Optional.empty();
 116     }
 117 
 118     @Override
 119     public Set<ModuleReference> findAll() {
 120         // need to ensure that all entries have been scanned
 121         while (hasNextEntry()) {
 122             scanNextEntry();
 123         }
 124         return cachedModules.values().stream().collect(Collectors.toSet());
 125     }
 126 
 127     /**
 128      * Returns {@code true} if there are additional entries to scan
 129      */
 130     private boolean hasNextEntry() {
 131         return next < entries.length;
 132     }
 133 
 134     /**
 135      * Scans the next entry on the module path. A no-op if all entries have
 136      * already been scanned.
 137      *
 138      * @throws FindException if an error occurs scanning the next entry
 139      */
 140     private void scanNextEntry() {
 141         if (hasNextEntry()) {
 142 
 143             long t0 = System.nanoTime();
 144 
 145             Path entry = entries[next];
 146             Map<String, ModuleReference> modules = scan(entry);
 147             next++;
 148 
 149             // update cache, ignoring duplicates
 150             int initialSize = cachedModules.size();
 151             for (Map.Entry<String, ModuleReference> e : modules.entrySet()) {
 152                 cachedModules.putIfAbsent(e.getKey(), e.getValue());
 153             }
 154 
 155             // update counters
 156             int added = cachedModules.size() - initialSize;
 157             moduleCount.add(added);
 158 
 159             scanTime.addElapsedTimeFrom(t0);
 160         }
 161     }
 162 
 163 
 164     /**
 165      * Scan the given module path entry. If the entry is a directory then it is
 166      * a directory of modules or an exploded module. If the entry is a regular
 167      * file then it is assumed to be a packaged module.
 168      *
 169      * @throws FindException if an error occurs scanning the entry
 170      */
 171     private Map<String, ModuleReference> scan(Path entry) {
 172 
 173         BasicFileAttributes attrs;
 174         try {
 175             attrs = Files.readAttributes(entry, BasicFileAttributes.class);
 176         } catch (NoSuchFileException e) {
 177             return Collections.emptyMap();
 178         } catch (IOException ioe) {
 179             throw new FindException(ioe);
 180         }
 181 
 182         try {
 183 
 184             if (attrs.isDirectory()) {
 185                 Path mi = entry.resolve(MODULE_INFO);
 186                 if (!Files.exists(mi)) {
 187                     // does not exist or unable to determine so assume a
 188                     // directory of modules
 189                     return scanDirectory(entry);
 190                 }
 191             }
 192 
 193             if (attrs.isRegularFile() || attrs.isDirectory()) {
 194                 // packaged or exploded module
 195                 ModuleReference mref = readModule(entry, attrs);
 196                 if (mref != null) {
 197                     String name = mref.descriptor().name();
 198                     return Collections.singletonMap(name, mref);
 199                 }
 200             }
 201 
 202             // not recognized
 203             throw new FindException("Unrecognized module: " + entry);
 204 
 205         } catch (IOException ioe) {
 206             throw new FindException(ioe);
 207         }
 208     }
 209 
 210 
 211     /**
 212      * Scans the given directory for packaged or exploded modules.
 213      *
 214      * @return a map of module name to ModuleReference for the modules found
 215      *         in the directory
 216      *
 217      * @throws IOException if an I/O error occurs
 218      * @throws FindException if an error occurs scanning the entry or the
 219      *         directory contains two or more modules with the same name
 220      */
 221     private Map<String, ModuleReference> scanDirectory(Path dir)
 222         throws IOException
 223     {
 224         // The map of name -> mref of modules found in this directory.
 225         Map<String, ModuleReference> nameToReference = new HashMap<>();
 226 
 227         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
 228             for (Path entry : stream) {
 229                 BasicFileAttributes attrs;
 230                 try {
 231                     attrs = Files.readAttributes(entry, BasicFileAttributes.class);
 232                 } catch (NoSuchFileException ignore) {
 233                     // file has been removed or moved, ignore for now
 234                     continue;
 235                 }
 236 
 237                 ModuleReference mref = readModule(entry, attrs);
 238 
 239                 // module found
 240                 if (mref != null) {
 241 
 242                     // can have at most one version of a module in the directory
 243                     String name = mref.descriptor().name();
 244                     if (nameToReference.put(name, mref) != null) {
 245                         throw new FindException("Two versions of module "
 246                                 + name + " found in " + dir);
 247                     }
 248 
 249                 }
 250 
 251             }
 252         }
 253 
 254         return nameToReference;
 255     }
 256 
 257 
 258     /**
 259      * Locates a packaged or exploded module, returning a {@code ModuleReference}
 260      * to the module. Returns {@code null} if the module is not recognized
 261      * as a packaged or exploded module.
 262      *
 263      * @throws IOException if an I/O error occurs
 264      * @throws FindException if an error occurs parsing the module descriptor
 265      */
 266     private ModuleReference readModule(Path entry, BasicFileAttributes attrs)
 267         throws IOException
 268     {
 269         try {
 270 
 271             ModuleReference mref = null;
 272             if (attrs.isDirectory()) {
 273                 mref = readExplodedModule(entry);
 274             } if (attrs.isRegularFile()) {
 275                 if (entry.toString().endsWith(".jar")) {
 276                     mref = readJar(entry);
 277                 } else if (isLinkPhase && entry.toString().endsWith(".jmod")) {
 278                     mref = readJMod(entry);
 279                 }
 280             }
 281             return mref;
 282 
 283         } catch (InvalidModuleDescriptorException e) {
 284             throw new FindException("Error reading module: " + entry, e);
 285         }
 286     }
 287 
 288 
 289     // -- jmod files --
 290 
 291     private Set<String> jmodPackages(ZipFile zf) {
 292         return zf.stream()
 293             .filter(e -> e.getName().startsWith("classes/") &&
 294                     e.getName().endsWith(".class"))
 295             .map(e -> toPackageName(e))
 296             .filter(pkg -> pkg.length() > 0) // module-info
 297             .distinct()
 298             .collect(Collectors.toSet());
 299     }
 300 
 301     /**
 302      * Returns a {@code ModuleReference} to a module in jmod file on the
 303      * file system.
 304      */
 305     private ModuleReference readJMod(Path file) throws IOException {
 306         try (ZipFile zf = new ZipFile(file.toString())) {
 307             ZipEntry ze = zf.getEntry("classes/" + MODULE_INFO);
 308             if (ze == null) {
 309                 throw new IOException(MODULE_INFO + " is missing: " + file);
 310             }
 311             ModuleDescriptor md;
 312             try (InputStream in = zf.getInputStream(ze)) {
 313                 md = ModuleDescriptor.read(in, () -> jmodPackages(zf));
 314             }
 315             return ModuleReferences.newJModModule(md, file);
 316         }
 317     }
 318 
 319 
 320     // -- JAR files --
 321 
 322     private static final String SERVICES_PREFIX = "META-INF/services/";
 323 
 324     /**
 325      * Returns a container with the service type corresponding to the name of
 326      * a services configuration file.
 327      *
 328      * For example, if called with "META-INF/services/p.S" then this method
 329      * returns a container with the value "p.S".
 330      */
 331     private Optional<String> toServiceName(String cf) {
 332         assert cf.startsWith(SERVICES_PREFIX);
 333         int index = cf.lastIndexOf("/") + 1;
 334         if (index < cf.length()) {
 335             String prefix = cf.substring(0, index);
 336             if (prefix.equals(SERVICES_PREFIX)) {
 337                 String sn = cf.substring(index);
 338                 if (Checks.isJavaIdentifier(sn))
 339                     return Optional.of(sn);
 340             }
 341         }
 342         return Optional.empty();
 343     }
 344 
 345     /**
 346      * Reads the next line from the given reader and trims it of comments and
 347      * leading/trailing white space.
 348      *
 349      * Returns null if the reader is at EOF.
 350      */
 351     private String nextLine(BufferedReader reader) throws IOException {
 352         String ln = reader.readLine();
 353         if (ln != null) {
 354             int ci = ln.indexOf('#');
 355             if (ci >= 0)
 356                 ln = ln.substring(0, ci);
 357             ln = ln.trim();
 358         }
 359         return ln;
 360     }
 361 
 362     /**
 363      * Treat the given JAR file as a module as follows:
 364      *
 365      * 1. The module name (and optionally the version) is derived from the file
 366      *    name of the JAR file
 367      * 2. The packages of all .class files in the JAR file are exported
 368      * 3. It has no module-private/concealed packages
 369      * 4. The contents of any META-INF/services configuration files are mapped
 370      *    to "provides" declarations
 371      * 5. The Main-Class attribute in the main attributes of the JAR manifest
 372      *    is mapped to the module descriptor mainClass
 373      *
 374      * @apiNote This needs to move to somewhere where it can be used by tools,
 375      * maybe even a standard API if automatic modules are a Java SE feature.
 376      */
 377     private ModuleDescriptor deriveModuleDescriptor(JarFile jf)
 378         throws IOException
 379     {
 380         // Derive module name and version from JAR file name
 381 
 382         String fn = jf.getName();
 383         int i = fn.lastIndexOf(File.separator);
 384         if (i != -1)
 385             fn = fn.substring(i+1);
 386 
 387         // drop .jar
 388         String mn = fn.substring(0, fn.length()-4);
 389         String vs = null;
 390 
 391         // find first occurrence of -${NUMBER}. or -${NUMBER}$
 392         Matcher matcher = Pattern.compile("-(\\d+(\\.|$))").matcher(mn);
 393         if (matcher.find()) {
 394             int start = matcher.start();
 395 
 396             // attempt to parse the tail as a version string
 397             try {
 398                 String tail = mn.substring(start+1);
 399                 ModuleDescriptor.Version.parse(tail);
 400                 vs = tail;
 401             } catch (IllegalArgumentException ignore) { }
 402 
 403             mn = mn.substring(0, start);
 404         }
 405 
 406         // finally clean up the module name
 407         mn =  mn.replaceAll("[^A-Za-z0-9]", ".")  // replace non-alphanumeric
 408                 .replaceAll("(\\.)(\\1)+", ".")   // collapse repeating dots
 409                 .replaceAll("^\\.", "")           // drop leading dots
 410                 .replaceAll("\\.$", "");          // drop trailing dots
 411 
 412 
 413         // Builder throws IAE if module name is empty or invalid
 414         ModuleDescriptor.Builder builder
 415             = new ModuleDescriptor.Builder(mn, true)
 416                 .requires(Requires.Modifier.MANDATED, "java.base");
 417         if (vs != null)
 418             builder.version(vs);
 419 
 420         // scan the entries in the JAR file to locate the .class and service
 421         // configuration file
 422         Stream<String> stream = jf.stream()
 423             .map(e -> e.getName())
 424             .filter(e -> (e.endsWith(".class") || e.startsWith(SERVICES_PREFIX)))
 425             .distinct();
 426         Map<Boolean, Set<String>> map
 427             = stream.collect(Collectors.partitioningBy(s -> s.endsWith(".class"),
 428                              Collectors.toSet()));
 429         Set<String> classFiles = map.get(Boolean.TRUE);
 430         Set<String> configFiles = map.get(Boolean.FALSE);
 431 
 432         // all packages are exported
 433         classFiles.stream()
 434             .map(c -> toPackageName(c))
 435             .distinct()
 436             .forEach(p -> builder.exports(p));
 437 
 438         // map names of service configuration files to service names
 439         Set<String> serviceNames = configFiles.stream()
 440             .map(this::toServiceName)
 441             .filter(Optional::isPresent)
 442             .map(Optional::get)
 443             .collect(Collectors.toSet());
 444 
 445         // parse each service configuration file
 446         for (String sn : serviceNames) {
 447             JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);
 448             Set<String> providerClasses = new HashSet<>();
 449             try (InputStream in = jf.getInputStream(entry)) {
 450                 BufferedReader reader
 451                     = new BufferedReader(new InputStreamReader(in, "UTF-8"));
 452                 String cn;
 453                 while ((cn = nextLine(reader)) != null) {
 454                     if (Checks.isJavaIdentifier(cn)) {
 455                         providerClasses.add(cn);
 456                     }
 457                 }
 458             }
 459             if (!providerClasses.isEmpty())
 460                 builder.provides(sn, providerClasses);
 461         }
 462 
 463         // Main-Class attribute if it exists
 464         Manifest man = jf.getManifest();
 465         if (man != null) {
 466             Attributes attrs = man.getMainAttributes();
 467             String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
 468             if (mainClass != null)
 469                 builder.mainClass(mainClass);
 470         }
 471 
 472         return builder.build();
 473     }
 474 
 475     private Set<String> jarPackages(JarFile jf) {
 476         return jf.stream()
 477             .filter(e -> e.getName().endsWith(".class"))
 478             .map(e -> toPackageName(e))
 479             .filter(pkg -> pkg.length() > 0)   // module-info
 480             .distinct()
 481             .collect(Collectors.toSet());
 482     }
 483 
 484     /**
 485      * Returns a {@code ModuleReference} to a module in modular JAR file on
 486      * the file system.
 487      */
 488     private ModuleReference readJar(Path file) throws IOException {
 489         try (JarFile jf = new JarFile(file.toString())) {
 490 
 491             ModuleDescriptor md;
 492             JarEntry entry = jf.getJarEntry(MODULE_INFO);
 493             if (entry == null) {
 494 
 495                 // no module-info.class so treat it as automatic module
 496                 try {
 497                     md = deriveModuleDescriptor(jf);
 498                 } catch (IllegalArgumentException iae) {
 499                     throw new FindException(
 500                         "Unable to derive module descriptor for: "
 501                         + jf.getName(), iae);
 502                 }
 503 
 504             } else {
 505                 md = ModuleDescriptor.read(jf.getInputStream(entry),
 506                                            () -> jarPackages(jf));
 507             }
 508 
 509             return ModuleReferences.newJarModule(md, file);
 510         }
 511     }
 512 
 513 
 514     // -- exploded directories --
 515 
 516     private Set<String> explodedPackages(Path dir) {
 517         try {
 518             return Files.find(dir, Integer.MAX_VALUE,
 519                               ((path, attrs) -> attrs.isRegularFile() &&
 520                                path.toString().endsWith(".class")))
 521                 .map(path -> toPackageName(dir.relativize(path)))
 522                 .filter(pkg -> pkg.length() > 0)   // module-info
 523                 .distinct()
 524                 .collect(Collectors.toSet());
 525         } catch (IOException x) {
 526             throw new UncheckedIOException(x);
 527         }
 528     }
 529 
 530     /**
 531      * Returns a {@code ModuleReference} to an exploded module on the file
 532      * system or {@code null} if {@code module-info.class} not found.
 533      */
 534     private ModuleReference readExplodedModule(Path dir) throws IOException {
 535         Path mi = dir.resolve(MODULE_INFO);
 536         ModuleDescriptor md;
 537         try (InputStream in = Files.newInputStream(mi)) {
 538             md = ModuleDescriptor.read(new BufferedInputStream(in),
 539                                        () -> explodedPackages(dir));
 540         } catch (NoSuchFileException e) {
 541             // for now
 542             return null;
 543         }
 544         return ModuleReferences.newExplodedModule(md, dir);
 545     }
 546 
 547 
 548     //
 549 
 550     // p/q/T.class => p.q
 551     private String toPackageName(String cn) {
 552         assert cn.endsWith(".class");
 553         int start = 0;
 554         int index = cn.lastIndexOf("/");
 555         if (index > start) {
 556             return cn.substring(start, index).replace('/', '.');
 557         } else {
 558             return "";
 559         }
 560     }
 561 
 562     private String toPackageName(ZipEntry entry) {
 563         String name = entry.getName();
 564         assert name.endsWith(".class");
 565         // jmod classes in classes/, jar in /
 566         int start = name.startsWith("classes/") ? 8 : 0;
 567         int index = name.lastIndexOf("/");
 568         if (index > start) {
 569             return name.substring(start, index).replace('/', '.');
 570         } else {
 571             return "";
 572         }
 573     }
 574 
 575     private String toPackageName(Path path) {
 576         String name = path.toString();
 577         assert name.endsWith(".class");
 578         int index = name.lastIndexOf(File.separatorChar);
 579         if (index != -1) {
 580             return name.substring(0, index).replace(File.separatorChar, '.');
 581         } else {
 582             return "";
 583         }
 584     }
 585 
 586     private static final PerfCounter scanTime
 587         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.scanTime");
 588     private static final PerfCounter moduleCount
 589         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.modules");
 590 }