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