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