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