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 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                     // does not exist or unable to determine so assume a
 199                     // directory of modules
 200                     return scanDirectory(entry);
 201                 }
 202             }
 203 
 204             // packaged or exploded module
 205             ModuleReference mref = readModule(entry, attrs);
 206             if (mref != null) {
 207                 String name = mref.descriptor().name();
 208                 return Collections.singletonMap(name, mref);
 209             } else {
 210                 // skipped
 211                 return Collections.emptyMap();
 212             }
 213 
 214         } catch (IOException ioe) {
 215             throw new FindException(ioe);
 216         }
 217     }
 218 
 219 
 220     /**
 221      * Scans the given directory for packaged or exploded modules.
 222      *
 223      * @return a map of module name to ModuleReference for the modules found
 224      *         in the directory
 225      *
 226      * @throws IOException if an I/O error occurs
 227      * @throws FindException if an error occurs scanning the entry or the
 228      *         directory contains two or more modules with the same name
 229      */
 230     private Map<String, ModuleReference> scanDirectory(Path dir)
 231         throws IOException
 232     {
 233         // The map of name -> mref of modules found in this directory.
 234         Map<String, ModuleReference> nameToReference = new HashMap<>();
 235 
 236         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
 237             for (Path entry : stream) {
 238                 BasicFileAttributes attrs;
 239                 try {
 240                     attrs = Files.readAttributes(entry, BasicFileAttributes.class);
 241                 } catch (NoSuchFileException ignore) {
 242                     // file has been removed or moved, ignore for now
 243                     continue;
 244                 }
 245 
 246                 ModuleReference mref = readModule(entry, attrs);
 247 
 248                 // module found
 249                 if (mref != null) {
 250                     // can have at most one version of a module in the directory
 251                     String name = mref.descriptor().name();
 252                     ModuleReference previous = nameToReference.put(name, mref);
 253                     if (previous != null) {
 254                         String fn1 = fileName(mref);
 255                         String fn2 = fileName(previous);
 256                         throw new FindException("Two versions of module "
 257                                                  + name + " found in " + dir
 258                                                  + " (" + fn1 + " and " + fn2 + ")");
 259                     }
 260                 }
 261             }
 262         }
 263 
 264         return nameToReference;
 265     }
 266 
 267 
 268     /**
 269      * Locates a packaged or exploded module, returning a {@code ModuleReference}
 270      * to the module. Returns {@code null} if the entry is skipped because it is
 271      * to a directory that does not contain a module-info.class or it's a hidden
 272      * file.
 273      *
 274      * @throws IOException if an I/O error occurs
 275      * @throws FindException if the file is not recognized as a module or an
 276      *         error occurs parsing its module descriptor
 277      */
 278     private ModuleReference readModule(Path entry, BasicFileAttributes attrs)
 279         throws IOException
 280     {
 281         try {
 282 
 283             if (attrs.isDirectory()) {
 284                 return readExplodedModule(entry); // may return null
 285             }
 286 
 287             String fn = entry.getFileName().toString();
 288             if (attrs.isRegularFile()) {
 289                 if (fn.endsWith(".jar")) {
 290                     return readJar(entry);
 291                 } else if (fn.endsWith(".jmod")) {
 292                     if (isLinkPhase)
 293                         return readJMod(entry);
 294                     throw new FindException("JMOD files not supported: " + entry);
 295                 }
 296             }
 297 
 298             // skip hidden files
 299             if (fn.startsWith(".") || Files.isHidden(entry)) {
 300                 return null;
 301             } else {
 302                 throw new FindException("Unrecognized module: " + entry);
 303             }
 304 
 305         } catch (InvalidModuleDescriptorException e) {
 306             throw new FindException("Error reading module: " + entry, e);
 307         }
 308     }
 309 
 310 
 311     /**
 312      * Returns a string with the file name of the module if possible.
 313      * If the module location is not a file URI then return the URI
 314      * as a string.
 315      */
 316     private String fileName(ModuleReference mref) {
 317         URI uri = mref.location().orElse(null);
 318         if (uri != null) {
 319             if (uri.getScheme().equalsIgnoreCase("file")) {
 320                 Path file = Paths.get(uri);
 321                 return file.getFileName().toString();
 322             } else {
 323                 return uri.toString();
 324             }
 325         } else {
 326             return "<unknown>";
 327         }
 328     }
 329 
 330     // -- jmod files --
 331 
 332     private Set<String> jmodPackages(JmodFile jf) {
 333         return jf.stream()
 334             .filter(e -> e.section() == Section.CLASSES)
 335             .map(JmodFile.Entry::name)
 336             .map(this::toPackageName)
 337             .flatMap(Optional::stream)
 338             .collect(Collectors.toSet());
 339     }
 340 
 341     /**
 342      * Returns a {@code ModuleReference} to a module in jmod file on the
 343      * file system.
 344      *
 345      * @throws IOException
 346      * @throws InvalidModuleDescriptorException
 347      */
 348     private ModuleReference readJMod(Path file) throws IOException {
 349         try (JmodFile jf = new JmodFile(file)) {
 350             ModuleInfo.Attributes attrs;
 351             try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) {
 352                 attrs  = ModuleInfo.read(in, () -> jmodPackages(jf));
 353             }
 354             return ModuleReferences.newJModModule(attrs, file);
 355         }
 356     }
 357 
 358 
 359     // -- JAR files --
 360 
 361     private static final String SERVICES_PREFIX = "META-INF/services/";
 362 
 363     /**
 364      * Returns the service type corresponding to the name of a services
 365      * configuration file if it is a valid Java identifier.
 366      *
 367      * For example, if called with "META-INF/services/p.S" then this method
 368      * returns a container with the value "p.S".
 369      */
 370     private Optional<String> toServiceName(String cf) {
 371         assert cf.startsWith(SERVICES_PREFIX);
 372         int index = cf.lastIndexOf("/") + 1;
 373         if (index < cf.length()) {
 374             String prefix = cf.substring(0, index);
 375             if (prefix.equals(SERVICES_PREFIX)) {
 376                 String sn = cf.substring(index);
 377                 if (Checks.isJavaIdentifier(sn))
 378                     return Optional.of(sn);
 379             }
 380         }
 381         return Optional.empty();
 382     }
 383 
 384     /**
 385      * Reads the next line from the given reader and trims it of comments and
 386      * leading/trailing white space.
 387      *
 388      * Returns null if the reader is at EOF.
 389      */
 390     private String nextLine(BufferedReader reader) throws IOException {
 391         String ln = reader.readLine();
 392         if (ln != null) {
 393             int ci = ln.indexOf('#');
 394             if (ci >= 0)
 395                 ln = ln.substring(0, ci);
 396             ln = ln.trim();
 397         }
 398         return ln;
 399     }
 400 
 401     /**
 402      * Treat the given JAR file as a module as follows:
 403      *
 404      * 1. The module name (and optionally the version) is derived from the file
 405      *    name of the JAR file
 406      * 2. All packages are exported and open
 407      * 3. It has no non-exported/non-open packages
 408      * 4. The contents of any META-INF/services configuration files are mapped
 409      *    to "provides" declarations
 410      * 5. The Main-Class attribute in the main attributes of the JAR manifest
 411      *    is mapped to the module descriptor mainClass
 412      */
 413     private ModuleDescriptor deriveModuleDescriptor(JarFile jf)
 414         throws IOException
 415     {
 416         // Derive module name and version from JAR file name
 417 
 418         String fn = jf.getName();
 419         int i = fn.lastIndexOf(File.separator);
 420         if (i != -1)
 421             fn = fn.substring(i+1);
 422 
 423         // drop .jar
 424         String mn = fn.substring(0, fn.length()-4);
 425         String vs = null;
 426 
 427         // find first occurrence of -${NUMBER}. or -${NUMBER}$
 428         Matcher matcher = Patterns.DASH_VERSION.matcher(mn);
 429         if (matcher.find()) {
 430             int start = matcher.start();
 431 
 432             // attempt to parse the tail as a version string
 433             try {
 434                 String tail = mn.substring(start+1);
 435                 ModuleDescriptor.Version.parse(tail);
 436                 vs = tail;
 437             } catch (IllegalArgumentException ignore) { }
 438 
 439             mn = mn.substring(0, start);
 440         }
 441 
 442         // finally clean up the module name
 443         mn = cleanModuleName(mn);
 444 
 445         // Builder throws IAE if module name is empty or invalid
 446         ModuleDescriptor.Builder builder
 447             = ModuleDescriptor.automaticModule(mn)
 448                 .requires(Set.of(Requires.Modifier.MANDATED), "java.base");
 449         if (vs != null)
 450             builder.version(vs);
 451 
 452         // scan the names of the entries in the JAR file
 453         Map<Boolean, Set<String>> map = VersionedStream.stream(jf)
 454                 .filter(e -> !e.isDirectory())
 455                 .map(JarEntry::getName)
 456                 .collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX),
 457                                                    Collectors.toSet()));
 458 
 459         Set<String> resources = map.get(Boolean.FALSE);
 460         Set<String> configFiles = map.get(Boolean.TRUE);
 461         // all packages are exported and open
 462         resources.stream()
 463                 .map(this::toPackageName)
 464                 .flatMap(Optional::stream)
 465                 .distinct()
 466                 .forEach(pn -> builder.exports(pn).opens(pn));
 467 
 468         // map names of service configuration files to service names
 469         Set<String> serviceNames = configFiles.stream()
 470                 .map(this::toServiceName)
 471                 .flatMap(Optional::stream)
 472                 .collect(Collectors.toSet());
 473 
 474         // parse each service configuration file
 475         for (String sn : serviceNames) {
 476             JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);
 477             List<String> providerClasses = new ArrayList<>();
 478             try (InputStream in = jf.getInputStream(entry)) {
 479                 BufferedReader reader
 480                     = new BufferedReader(new InputStreamReader(in, "UTF-8"));
 481                 String cn;
 482                 while ((cn = nextLine(reader)) != null) {
 483                     if (cn.length() > 0) {
 484                         providerClasses.add(cn);
 485                     }
 486                 }
 487             }
 488             if (!providerClasses.isEmpty())
 489                 builder.provides(sn, providerClasses);
 490         }
 491 
 492         // Main-Class attribute if it exists
 493         Manifest man = jf.getManifest();
 494         if (man != null) {
 495             Attributes attrs = man.getMainAttributes();
 496             String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
 497             if (mainClass != null)
 498                 builder.mainClass(mainClass.replace("/", "."));
 499         }
 500 
 501         return builder.build();
 502     }
 503 
 504     /**
 505      * Patterns used to derive the module name from a JAR file name.
 506      */
 507     private static class Patterns {
 508         static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
 509         static final Pattern TRAILING_VERSION = Pattern.compile("(\\.|\\d)*$");
 510         static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
 511         static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");
 512         static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
 513         static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");
 514     }
 515 
 516     /**
 517      * Clean up candidate module name derived from a JAR file name.
 518      */
 519     private static String cleanModuleName(String mn) {
 520         // drop trailing version from name
 521         mn = Patterns.TRAILING_VERSION.matcher(mn).replaceAll("");
 522 
 523         // replace non-alphanumeric
 524         mn = Patterns.NON_ALPHANUM.matcher(mn).replaceAll(".");
 525 
 526         // collapse repeating dots
 527         mn = Patterns.REPEATING_DOTS.matcher(mn).replaceAll(".");
 528 
 529         // drop leading dots
 530         if (mn.length() > 0 && mn.charAt(0) == '.')
 531             mn = Patterns.LEADING_DOTS.matcher(mn).replaceAll("");
 532 
 533         // drop trailing dots
 534         int len = mn.length();
 535         if (len > 0 && mn.charAt(len-1) == '.')
 536             mn = Patterns.TRAILING_DOTS.matcher(mn).replaceAll("");
 537 
 538         return mn;
 539     }
 540 
 541     private Set<String> jarPackages(JarFile jf) {
 542         return VersionedStream.stream(jf)
 543                 .filter(e -> !e.isDirectory())
 544                 .map(JarEntry::getName)
 545                 .map(this::toPackageName)
 546                 .flatMap(Optional::stream)
 547                 .collect(Collectors.toSet());
 548     }
 549 
 550     /**
 551      * Returns a {@code ModuleReference} to a module in modular JAR file on
 552      * the file system.
 553      *
 554      * @throws IOException
 555      * @throws FindException
 556      * @throws InvalidModuleDescriptorException
 557      */
 558     private ModuleReference readJar(Path file) throws IOException {
 559         try (JarFile jf = new JarFile(file.toFile(),
 560                                       true,               // verify
 561                                       ZipFile.OPEN_READ,
 562                                       releaseVersion))
 563         {
 564             ModuleInfo.Attributes attrs;
 565             JarEntry entry = jf.getJarEntry(MODULE_INFO);
 566             if (entry == null) {
 567 
 568                 // no module-info.class so treat it as automatic module
 569                 try {
 570                     ModuleDescriptor md = deriveModuleDescriptor(jf);
 571                     attrs = new ModuleInfo.Attributes(md, null, null);
 572                 } catch (IllegalArgumentException iae) {
 573                     throw new FindException(
 574                         "Unable to derive module descriptor for: "
 575                         + jf.getName(), iae);
 576                 }
 577 
 578             } else {
 579                 attrs = ModuleInfo.read(jf.getInputStream(entry),
 580                                         () -> jarPackages(jf));
 581             }
 582 
 583             return ModuleReferences.newJarModule(attrs, file);
 584         }
 585     }
 586 
 587 
 588     // -- exploded directories --
 589 
 590     private Set<String> explodedPackages(Path dir) {
 591         try {
 592             return Files.find(dir, Integer.MAX_VALUE,
 593                               ((path, attrs) -> attrs.isRegularFile()))
 594                     .map(path -> dir.relativize(path))
 595                     .map(this::toPackageName)
 596                     .flatMap(Optional::stream)
 597                     .collect(Collectors.toSet());
 598         } catch (IOException x) {
 599             throw new UncheckedIOException(x);
 600         }
 601     }
 602 
 603     /**
 604      * Returns a {@code ModuleReference} to an exploded module on the file
 605      * system or {@code null} if {@code module-info.class} not found.
 606      *
 607      * @throws IOException
 608      * @throws InvalidModuleDescriptorException
 609      */
 610     private ModuleReference readExplodedModule(Path dir) throws IOException {
 611         Path mi = dir.resolve(MODULE_INFO);
 612         ModuleInfo.Attributes attrs;
 613         try (InputStream in = Files.newInputStream(mi)) {
 614             attrs = ModuleInfo.read(new BufferedInputStream(in),
 615                                     () -> explodedPackages(dir));
 616         } catch (NoSuchFileException e) {
 617             // for now
 618             return null;
 619         }
 620         return ModuleReferences.newExplodedModule(attrs, dir);
 621     }
 622 
 623     /**
 624      * Maps the name of an entry in a JAR or ZIP file to a package name.
 625      *
 626      * @throws IllegalArgumentException if the name is a class file in
 627      *         the top-level directory of the JAR/ZIP file (and it's
 628      *         not module-info.class)
 629      */
 630     private Optional<String> toPackageName(String name) {
 631         assert !name.endsWith("/");
 632 
 633         int index = name.lastIndexOf("/");
 634         if (index == -1) {
 635             if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {
 636                 throw new IllegalArgumentException(name
 637                         + " found in top-level directory:"
 638                         + " (unnamed package not allowed in module)");
 639             }
 640             return Optional.empty();
 641         }
 642 
 643         String pn = name.substring(0, index).replace('/', '.');
 644         if (Checks.isJavaIdentifier(pn)) {
 645             return Optional.of(pn);
 646         } else {
 647             // not a valid package name
 648             return Optional.empty();
 649         }
 650     }
 651 
 652     /**
 653      * Maps the relative path of an entry in an exploded module to a package
 654      * name.
 655      *
 656      * @throws IllegalArgumentException if the name is a class file in
 657      *         the top-level directory (and it's not module-info.class)
 658      */
 659     private Optional<String> toPackageName(Path file) {
 660         assert file.getRoot() == null;
 661 
 662         Path parent = file.getParent();
 663         if (parent == null) {
 664             String name = file.toString();
 665             if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {
 666                 throw new IllegalArgumentException(name
 667                         + " found in in top-level directory"
 668                         + " (unnamed package not allowed in module)");
 669             }
 670             return Optional.empty();
 671         }
 672 
 673         String pn = parent.toString().replace(File.separatorChar, '.');
 674         if (Checks.isJavaIdentifier(pn)) {
 675             return Optional.of(pn);
 676         } else {
 677             // not a valid package name
 678             return Optional.empty();
 679         }
 680     }
 681 
 682     private static final PerfCounter scanTime
 683         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.scanTime");
 684     private static final PerfCounter moduleCount
 685         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.modules");
 686 }