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