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