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
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 {
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 {
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 */
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)
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 }
|
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
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 {
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 {
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 */
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)
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 }
|