diff --git a/jdk/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java b/jdk/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java index e76c2f59200..60aeea6d85d 100644 --- a/jdk/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java +++ b/jdk/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java @@ -31,6 +31,7 @@ import java.io.PrintStream; import java.io.UncheckedIOException; import java.net.URI; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -1997,6 +1998,13 @@ public class ModuleDescriptor public Optional hashes(ModuleDescriptor descriptor) { return descriptor.hashes(); } + + @Override + public ModuleFinder newModulePath(Runtime.Version version, + boolean isLinkPhase, + Path... entries) { + return new ModulePath(version, isLinkPhase, entries); + } }); } diff --git a/jdk/src/java.base/share/classes/java/lang/module/ModuleFinder.java b/jdk/src/java.base/share/classes/java/lang/module/ModuleFinder.java index 9dd5b54c8cc..4e72a52cf22 100644 --- a/jdk/src/java.base/share/classes/java/lang/module/ModuleFinder.java +++ b/jdk/src/java.base/share/classes/java/lang/module/ModuleFinder.java @@ -228,7 +228,7 @@ public interface ModuleFinder { * *
  • If the name matches the regular expression {@code * "-(\\d+(\\.|$))"} then the module name will be derived from the - * subsequence proceeding the hyphen of the first occurrence. The + * subsequence preceding the hyphen of the first occurrence. The * subsequence after the hyphen is parsed as a {@link * ModuleDescriptor.Version} and ignored if it cannot be parsed as * a {@code Version}.

  • @@ -248,18 +248,23 @@ public interface ModuleFinder { *
  • It {@link ModuleDescriptor#requires() requires} {@code * java.base}.

  • * - *
  • All entries in the JAR file with names ending with {@code - * .class} are assumed to be class files where the name corresponds - * to the fully qualified name of the class. The packages of all - * classes are {@link ModuleDescriptor#exports() exported}.

  • + *
  • The set of packages in the module is derived from the names + * of non-directory entries in the JAR file. A candidate package name + * is derived from an entry using the characters up to, but not + * including, the last forward slash. All remaining forward slashes are + * replaced with dot ({@code "."}). If the resulting string is a valid + * Java identifier then it is assumed to be a package name. For example, + * if the JAR file contains an entry "{@code p/q/Foo.class}" then the + * package name derived is "{@code p.q}". All packages are {@link + * ModuleDescriptor#exports() exported}.

  • * - *
  • The contents of all entries starting with {@code + *

  • The contents of entries starting with {@code * META-INF/services/} are assumed to be service configuration files - * (see {@link java.util.ServiceLoader}). The name of the file - * (that follows {@code META-INF/services/}) is assumed to be the - * fully-qualified binary name of a service type. The entries in the - * file are assumed to be the fully-qualified binary names of - * provider classes.

  • + * (see {@link java.util.ServiceLoader}). If the name of a file + * (that follows {@code META-INF/services/}) is a legal Java identifier + * then it is assumed to be the fully-qualified binary name of a + * service type. The entries in the file are assumed to be the + * fully-qualified binary names of provider classes.

    * *
  • If the JAR file has a {@code Main-Class} attribute in its * main manifest then its value is the {@link @@ -271,8 +276,8 @@ public interface ModuleFinder { * {@link ModuleDescriptor.Builder ModuleDescriptor.Builder} API) for an * automatic module then {@code FindException} is thrown. This can arise, * for example, when a legal Java identifier name cannot be derived from - * the file name of the JAR file or where a package name derived from an - * entry ending with {@code .class} is not a legal Java identifier.

    + * the file name of the JAR file or where the JAR file contains a {@code + * .class} in the top-level directory of the JAR file.

    * *

    In addition to JAR files, an implementation may also support modules * that are packaged in other implementation specific module formats. When @@ -283,8 +288,10 @@ public interface ModuleFinder { * *

    As with automatic modules, the contents of a packaged or exploded * module may need to be scanned in order to determine the packages - * in the module. If a {@code .class} file that corresponds to a class in an - * unnamed package is encountered then {@code FindException} is thrown.

    + * in the module. If a {@code .class} file (other than {@code + * module-info.class}) is found in the top-level directory then it is + * assumed to be a class in the unnamed package and so {@code FindException} + * is thrown.

    * *

    Finders created by this method are lazy and do not eagerly check * that the given file paths are directories or packaged modules. @@ -341,7 +348,7 @@ public interface ModuleFinder { * @return A {@code ModuleFinder} that composes a sequence of module finders */ static ModuleFinder compose(ModuleFinder... finders) { - // copy the list, also checking for nulls + // copy the list and check for nulls final List finderList = List.of(finders); return new ModuleFinder() { diff --git a/jdk/src/java.base/share/classes/java/lang/module/ModulePath.java b/jdk/src/java.base/share/classes/java/lang/module/ModulePath.java index cad2812ab64..fa40f3b7958 100644 --- a/jdk/src/java.base/share/classes/java/lang/module/ModulePath.java +++ b/jdk/src/java.base/share/classes/java/lang/module/ModulePath.java @@ -33,10 +33,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.lang.module.ModuleDescriptor.Requires; +import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collections; import java.util.HashMap; @@ -52,49 +54,53 @@ import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import jdk.internal.jmod.JmodFile; import jdk.internal.jmod.JmodFile.Section; -import jdk.internal.module.ConfigurableModuleFinder; +import jdk.internal.module.Checks; import jdk.internal.perf.PerfCounter; +import jdk.internal.util.jar.VersionedStream; /** * A {@code ModuleFinder} that locates modules on the file system by searching * a sequence of directories or packaged modules. * - * The {@code ModuleFinder} can be configured to work in either the run-time + * The {@code ModuleFinder} can be created to work in either the run-time * or link-time phases. In both cases it locates modular JAR and exploded - * modules. When configured for link-time then it additionally locates + * modules. When created for link-time then it additionally locates * modules in JMOD files. */ -class ModulePath implements ConfigurableModuleFinder { +class ModulePath implements ModuleFinder { private static final String MODULE_INFO = "module-info.class"; + // the version to use for multi-release modular JARs + private final Runtime.Version releaseVersion; + + // true for the link phase (supports modules packaged in JMOD format) + private final boolean isLinkPhase; + // the entries on this module path private final Path[] entries; private int next; - // true if in the link phase - private boolean isLinkPhase; - // map of module name to module reference map for modules already located private final Map cachedModules = new HashMap<>(); - ModulePath(Path... entries) { + ModulePath(Runtime.Version version, boolean isLinkPhase, Path... entries) { + this.releaseVersion = version; + this.isLinkPhase = isLinkPhase; this.entries = entries.clone(); for (Path entry : this.entries) { Objects.requireNonNull(entry); } } - @Override - public void configurePhase(Phase phase) { - isLinkPhase = (phase == Phase.LINK_TIME); + ModulePath(Path... entries) { + this(JarFile.runtimeVersion(), false, entries); } @Override @@ -239,9 +245,13 @@ class ModulePath implements ConfigurableModuleFinder { if (mref != null) { // can have at most one version of a module in the directory String name = mref.descriptor().name(); - if (nameToReference.put(name, mref) != null) { + ModuleReference previous = nameToReference.put(name, mref); + if (previous != null) { + String fn1 = fileName(mref); + String fn2 = fileName(previous); throw new FindException("Two versions of module " - + name + " found in " + dir); + + name + " found in " + dir + + " (" + fn1 + " and " + fn2 + ")"); } } } @@ -294,6 +304,25 @@ class ModulePath implements ConfigurableModuleFinder { } + /** + * Returns a string with the file name of the module if possible. + * If the module location is not a file URI then return the URI + * as a string. + */ + private String fileName(ModuleReference mref) { + URI uri = mref.location().orElse(null); + if (uri != null) { + if (uri.getScheme().equalsIgnoreCase("file")) { + Path file = Paths.get(uri); + return file.getFileName().toString(); + } else { + return uri.toString(); + } + } else { + return ""; + } + } + // -- jmod files -- private Set jmodPackages(JmodFile jf) { @@ -301,7 +330,7 @@ class ModulePath implements ConfigurableModuleFinder { .filter(e -> e.section() == Section.CLASSES) .map(JmodFile.Entry::name) .map(this::toPackageName) - .filter(pkg -> pkg.length() > 0) // module-info + .flatMap(Optional::stream) .collect(Collectors.toSet()); } @@ -328,8 +357,8 @@ class ModulePath implements ConfigurableModuleFinder { private static final String SERVICES_PREFIX = "META-INF/services/"; /** - * Returns a container with the service type corresponding to the name of - * a services configuration file. + * Returns the service type corresponding to the name of a services + * configuration file if it is a valid Java identifier. * * For example, if called with "META-INF/services/p.S" then this method * returns a container with the value "p.S". @@ -341,7 +370,8 @@ class ModulePath implements ConfigurableModuleFinder { String prefix = cf.substring(0, index); if (prefix.equals(SERVICES_PREFIX)) { String sn = cf.substring(index); - return Optional.of(sn); + if (Checks.isJavaIdentifier(sn)) + return Optional.of(sn); } } return Optional.empty(); @@ -416,28 +446,28 @@ class ModulePath implements ConfigurableModuleFinder { if (vs != null) builder.version(vs); - // scan the entries in the JAR file to locate the .class and service - // configuration file - Map> map = - versionedStream(jf) - .map(JarEntry::getName) - .filter(s -> (s.endsWith(".class") ^ s.startsWith(SERVICES_PREFIX))) - .collect(Collectors.partitioningBy(s -> s.endsWith(".class"), - Collectors.toSet())); - Set classFiles = map.get(Boolean.TRUE); - Set configFiles = map.get(Boolean.FALSE); + // scan the names of the entries in the JAR file + Map> map = VersionedStream.stream(jf) + .filter(e -> !e.isDirectory()) + .map(JarEntry::getName) + .collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX), + Collectors.toSet())); + + Set resources = map.get(Boolean.FALSE); + Set configFiles = map.get(Boolean.TRUE); // all packages are exported - classFiles.stream() - .map(c -> toPackageName(c)) - .distinct() - .forEach(builder::exports); + resources.stream() + .map(this::toPackageName) + .flatMap(Optional::stream) + .distinct() + .forEach(builder::exports); // map names of service configuration files to service names Set serviceNames = configFiles.stream() - .map(this::toServiceName) - .flatMap(Optional::stream) - .collect(Collectors.toSet()); + .map(this::toServiceName) + .flatMap(Optional::stream) + .collect(Collectors.toSet()); // parse each service configuration file for (String sn : serviceNames) { @@ -502,25 +532,13 @@ class ModulePath implements ConfigurableModuleFinder { return mn; } - private Stream versionedStream(JarFile jf) { - if (jf.isMultiRelease()) { - // a stream of JarEntries whose names are base names and whose - // contents are from the corresponding versioned entries in - // a multi-release jar file - return jf.stream().map(JarEntry::getName) - .filter(name -> !name.startsWith("META-INF/versions/")) - .map(jf::getJarEntry); - } else { - return jf.stream(); - } - } - private Set jarPackages(JarFile jf) { - return versionedStream(jf) - .filter(e -> e.getName().endsWith(".class")) - .map(e -> toPackageName(e.getName())) - .filter(pkg -> pkg.length() > 0) // module-info - .collect(Collectors.toSet()); + return VersionedStream.stream(jf) + .filter(e -> !e.isDirectory()) + .map(JarEntry::getName) + .map(this::toPackageName) + .flatMap(Optional::stream) + .collect(Collectors.toSet()); } /** @@ -535,7 +553,7 @@ class ModulePath implements ConfigurableModuleFinder { try (JarFile jf = new JarFile(file.toFile(), true, // verify ZipFile.OPEN_READ, - JarFile.runtimeVersion())) + releaseVersion)) { ModuleDescriptor md; JarEntry entry = jf.getJarEntry(MODULE_INFO); @@ -565,11 +583,11 @@ class ModulePath implements ConfigurableModuleFinder { private Set explodedPackages(Path dir) { try { return Files.find(dir, Integer.MAX_VALUE, - ((path, attrs) -> attrs.isRegularFile() && - path.toString().endsWith(".class"))) - .map(path -> toPackageName(dir.relativize(path))) - .filter(pkg -> pkg.length() > 0) // module-info - .collect(Collectors.toSet()); + ((path, attrs) -> attrs.isRegularFile())) + .map(path -> dir.relativize(path)) + .map(this::toPackageName) + .flatMap(Optional::stream) + .collect(Collectors.toSet()); } catch (IOException x) { throw new UncheckedIOException(x); } @@ -595,29 +613,62 @@ class ModulePath implements ConfigurableModuleFinder { return ModuleReferences.newExplodedModule(md, dir); } + /** + * Maps the name of an entry in a JAR or ZIP file to a package name. + * + * @throws IllegalArgumentException if the name is a class file in + * the top-level directory of the JAR/ZIP file (and it's + * not module-info.class) + */ + private Optional toPackageName(String name) { + assert !name.endsWith("/"); - // + int index = name.lastIndexOf("/"); + if (index == -1) { + if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { + throw new IllegalArgumentException(name + + " found in top-level directory:" + + " (unnamed package not allowed in module)"); + } + return Optional.empty(); + } - // p/q/T.class => p.q - private String toPackageName(String cn) { - assert cn.endsWith(".class"); - int start = 0; - int index = cn.lastIndexOf("/"); - if (index > start) { - return cn.substring(start, index).replace('/', '.'); + String pn = name.substring(0, index).replace('/', '.'); + if (Checks.isJavaIdentifier(pn)) { + return Optional.of(pn); } else { - return ""; + // not a valid package name + return Optional.empty(); } } - private String toPackageName(Path path) { - String name = path.toString(); - assert name.endsWith(".class"); - int index = name.lastIndexOf(File.separatorChar); - if (index != -1) { - return name.substring(0, index).replace(File.separatorChar, '.'); + /** + * Maps the relative path of an entry in an exploded module to a package + * name. + * + * @throws IllegalArgumentException if the name is a class file in + * the top-level directory (and it's not module-info.class) + */ + private Optional toPackageName(Path file) { + assert file.getRoot() == null; + + Path parent = file.getParent(); + if (parent == null) { + String name = file.toString(); + if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { + throw new IllegalArgumentException(name + + " found in in top-level directory" + + " (unnamed package not allowed in module)"); + } + return Optional.empty(); + } + + String pn = parent.toString().replace(File.separatorChar, '.'); + if (Checks.isJavaIdentifier(pn)) { + return Optional.of(pn); } else { - return ""; + // not a valid package name + return Optional.empty(); } } diff --git a/jdk/src/java.base/share/classes/java/lang/module/ModuleReader.java b/jdk/src/java.base/share/classes/java/lang/module/ModuleReader.java index 4d3d2bf15a0..d79d210edd0 100644 --- a/jdk/src/java.base/share/classes/java/lang/module/ModuleReader.java +++ b/jdk/src/java.base/share/classes/java/lang/module/ModuleReader.java @@ -32,6 +32,7 @@ import java.net.URI; import java.nio.ByteBuffer; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; /** @@ -44,6 +45,11 @@ import java.util.Optional; * module. A module reader is also intended to be used by {@code ClassLoader} * implementations that load classes and resources from modules.

    * + *

    A resource in a module is identified by a name that is a + * '{@code /}'-separated path string. For example, module {@code java.base} may + * have a resource "{@code java/lang/Object.class}" that, by convention, is the + * class file for {@code java.lang.Object}.

    + * *

    A {@code ModuleReader} is {@linkplain ModuleReference#open open} upon * creation and is closed by invoking the {@link #close close} method. Failure * to close a module reader may result in a resource leak. The {@code @@ -52,8 +58,8 @@ import java.util.Optional; * *

    A {@code ModuleReader} implementation may require permissions to access * resources in the module. Consequently the {@link #find find}, {@link #open - * open} and {@link #read read} methods may throw {@code SecurityException} if - * access is denied by the security manager.

    + * open}, {@link #read read}, and {@link #list list} methods may throw {@code + * SecurityException} if access is denied by the security manager.

    * * @see ModuleReference * @since 9 @@ -84,6 +90,9 @@ public interface ModuleReader extends Closeable { * Opens a resource, returning an input stream to read the resource in * the module. * + *

    The behavior of the input stream when used after the module reader + * is closed is implementation specific and therefore not specified.

    + * * @implSpec The default implementation invokes the {@link #find(String) * find} method to get a URI to the resource. If found, then it attempts * to construct a {@link java.net.URL URL} and open a connection to the @@ -171,18 +180,38 @@ public interface ModuleReader extends Closeable { Objects.requireNonNull(bb); } + /** + * Lists the contents of the module, returning a stream of elements that + * are the names of all resources in the module. + * + *

    In lazy implementations then an {@code IOException} may be thrown + * when using the stream to list the module contents. If this occurs then + * the {@code IOException} will be wrapped in an {@link + * java.io.UncheckedIOException} and thrown from the method that caused the + * access to be attempted. {@code SecurityException} may also be thrown + * when using the stream to list the module contents and access is denied + * by the security manager.

    + * + *

    The behavior of the stream when used after the module reader is + * closed is implementation specific and therefore not specified.

    + * + * @return A stream of elements that are the names of all resources + * in the module + * + * @throws IOException + * If an I/O error occurs or the module reader is closed + * @throws SecurityException + * If denied by the security manager + */ + Stream list() throws IOException; + /** * Closes the module reader. Once closed then subsequent calls to locate or - * read a resource will fail by returning {@code Optional.empty()} or - * throwing {@code IOException}. + * read a resource will fail by throwing {@code IOException}. * *

    A module reader is not required to be asynchronously closeable. If a * thread is reading a resource and another thread invokes the close method, - * then the second thread may block until the read operation is complete. - * - *

    The behavior of {@code InputStream}s obtained using the {@link - * #open(String) open} method and used after the module reader is closed - * is implementation specific and therefore not specified. + * then the second thread may block until the read operation is complete.

    */ @Override void close() throws IOException; diff --git a/jdk/src/java.base/share/classes/java/lang/module/ModuleReferences.java b/jdk/src/java.base/share/classes/java/lang/module/ModuleReferences.java index de245656364..8393bd0f223 100644 --- a/jdk/src/java.base/share/classes/java/lang/module/ModuleReferences.java +++ b/jdk/src/java.base/share/classes/java/lang/module/ModuleReferences.java @@ -35,6 +35,7 @@ import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.locks.Lock; @@ -43,14 +44,17 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.zip.ZipEntry; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipFile; +import jdk.internal.jmod.JmodFile; import jdk.internal.misc.JavaLangAccess; import jdk.internal.misc.SharedSecrets; import jdk.internal.module.ModuleHashes; import jdk.internal.module.ModuleHashes.HashSupplier; import jdk.internal.module.ModulePatcher; +import jdk.internal.util.jar.VersionedStream; import sun.net.www.ParseUtil; @@ -139,6 +143,13 @@ class ModuleReferences { */ abstract Optional implOpen(String name) throws IOException; + /** + * Returns a stream of the names of resources in the module. This + * method is invoked by the list method to do the actual work of + * creating the stream. + */ + abstract Stream implList() throws IOException; + /** * Closes the module reader. This method is invoked by close to do the * actual work of closing the module reader. @@ -175,7 +186,21 @@ class ModuleReferences { } @Override - public void close() throws IOException { + public final Stream list() throws IOException { + readLock.lock(); + try { + if (!closed) { + return implList(); + } else { + throw new IOException("ModuleReader is closed"); + } + } finally { + readLock.unlock(); + } + } + + @Override + public final void close() throws IOException { writeLock.lock(); try { if (!closed) { @@ -240,6 +265,16 @@ class ModuleReferences { } } + @Override + Stream implList() throws IOException { + // take snapshot to avoid async close + List names = VersionedStream.stream(jf) + .filter(e -> !e.isDirectory()) + .map(JarEntry::getName) + .collect(Collectors.toList()); + return names.stream(); + } + @Override void implClose() throws IOException { jf.close(); @@ -251,30 +286,31 @@ class ModuleReferences { * A ModuleReader for a JMOD file. */ static class JModModuleReader extends SafeCloseModuleReader { - private final ZipFile zf; + private final JmodFile jf; private final URI uri; - static ZipFile newZipFile(Path path) { + static JmodFile newJmodFile(Path path) { try { - return new ZipFile(path.toFile()); + return new JmodFile(path); } catch (IOException ioe) { throw new UncheckedIOException(ioe); } } JModModuleReader(Path path, URI uri) { - this.zf = newZipFile(path); + this.jf = newJmodFile(path); this.uri = uri; } - private ZipEntry getEntry(String name) { - return zf.getEntry("classes/" + Objects.requireNonNull(name)); + private JmodFile.Entry getEntry(String name) { + Objects.requireNonNull(name); + return jf.getEntry(JmodFile.Section.CLASSES, name); } @Override Optional implFind(String name) { - ZipEntry ze = getEntry(name); - if (ze != null) { + JmodFile.Entry je = getEntry(name); + if (je != null) { String encodedPath = ParseUtil.encodePath(name, false); String uris = "jmod:" + uri + "!/" + encodedPath; return Optional.of(URI.create(uris)); @@ -285,17 +321,27 @@ class ModuleReferences { @Override Optional implOpen(String name) throws IOException { - ZipEntry ze = getEntry(name); - if (ze != null) { - return Optional.of(zf.getInputStream(ze)); + JmodFile.Entry je = getEntry(name); + if (je != null) { + return Optional.of(jf.getInputStream(je)); } else { return Optional.empty(); } } + @Override + Stream implList() throws IOException { + // take snapshot to avoid async close + List names = jf.stream() + .filter(e -> e.section() == JmodFile.Section.CLASSES) + .map(JmodFile.Entry::name) + .collect(Collectors.toList()); + return names.stream(); + } + @Override void implClose() throws IOException { - zf.close(); + jf.close(); } } @@ -377,6 +423,17 @@ class ModuleReferences { } } + @Override + public Stream list() throws IOException { + ensureOpen(); + // sym links not followed + return Files.find(dir, Integer.MAX_VALUE, + (path, attrs) -> attrs.isRegularFile()) + .map(f -> dir.relativize(f) + .toString() + .replace(File.separatorChar, '/')); + } + @Override public void close() { closed = true; diff --git a/jdk/src/java.base/share/classes/java/lang/module/SystemModuleFinder.java b/jdk/src/java.base/share/classes/java/lang/module/SystemModuleFinder.java index 3e49c93f132..9fae75cb382 100644 --- a/jdk/src/java.base/share/classes/java/lang/module/SystemModuleFinder.java +++ b/jdk/src/java.base/share/classes/java/lang/module/SystemModuleFinder.java @@ -32,14 +32,21 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.URLConnection; import java.nio.ByteBuffer; +import java.util.ArrayDeque; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.Spliterator; +import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import jdk.internal.jimage.ImageLocation; import jdk.internal.jimage.ImageReader; @@ -62,6 +69,8 @@ import jdk.internal.perf.PerfCounter; class SystemModuleFinder implements ModuleFinder { + private static final JavaNetUriAccess JNUA = SharedSecrets.getJavaNetUriAccess(); + private static final PerfCounter initTime = PerfCounter.newPerfCounter("jdk.module.finder.jimage.initTime"); private static final PerfCounter moduleCount @@ -73,8 +82,6 @@ class SystemModuleFinder implements ModuleFinder { // ImageReader used to access all modules in the image private static final ImageReader imageReader; - private static final JavaNetUriAccess jnua = SharedSecrets.getJavaNetUriAccess(); - // the set of modules in the run-time image private static final Set modules; @@ -170,8 +177,7 @@ class SystemModuleFinder implements ModuleFinder { HashSupplier hash) { String mn = md.name(); - - URI uri = jnua.create("jrt", "/".concat(mn)); + URI uri = JNUA.create("jrt", "/".concat(mn)); Supplier readerSupplier = new Supplier<>() { @Override @@ -331,6 +337,15 @@ class SystemModuleFinder implements ModuleFinder { ImageReader.releaseByteBuffer(bb); } + @Override + public Stream list() throws IOException { + if (closed) + throw new IOException("ModuleReader is closed"); + + Spliterator s = new ModuleContentSpliterator(module); + return StreamSupport.stream(s, false); + } + @Override public void close() { // nothing else to do @@ -338,4 +353,86 @@ class SystemModuleFinder implements ModuleFinder { } } + /** + * A Spliterator for traversing the resources of a module linked into the + * run-time image. + */ + static class ModuleContentSpliterator implements Spliterator { + final String moduleRoot; + final Deque stack; + Iterator iterator; + + ModuleContentSpliterator(String module) throws IOException { + moduleRoot = "/modules/" + module; + stack = new ArrayDeque<>(); + + // push the root node to the stack to get started + ImageReader.Node dir = imageReader.findNode(moduleRoot); + if (dir == null || !dir.isDirectory()) + throw new IOException(moduleRoot + " not a directory"); + stack.push(dir); + iterator = Collections.emptyIterator(); + } + + /** + * Returns the name of the next non-directory node or {@code null} if + * there are no remaining nodes to visit. + */ + private String next() throws IOException { + for (;;) { + while (iterator.hasNext()) { + ImageReader.Node node = iterator.next(); + String name = node.getName(); + if (node.isDirectory()) { + // build node + ImageReader.Node dir = imageReader.findNode(name); + assert dir.isDirectory(); + stack.push(dir); + } else { + // strip /modules/$MODULE/ prefix + return name.substring(moduleRoot.length() + 1); + } + } + + if (stack.isEmpty()) { + return null; + } else { + ImageReader.Node dir = stack.poll(); + assert dir.isDirectory(); + iterator = dir.getChildren().iterator(); + } + } + } + + @Override + public boolean tryAdvance(Consumer action) { + String next; + try { + next = next(); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + if (next != null) { + action.accept(next); + return true; + } else { + return false; + } + } + + @Override + public Spliterator trySplit() { + return null; + } + + @Override + public int characteristics() { + return Spliterator.DISTINCT + Spliterator.NONNULL + Spliterator.IMMUTABLE; + } + + @Override + public long estimateSize() { + return Long.MAX_VALUE; + } + } } diff --git a/jdk/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java b/jdk/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java index f64ccf2aa4a..df6724bec7b 100644 --- a/jdk/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java +++ b/jdk/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java @@ -175,6 +175,16 @@ public class JmodFile implements AutoCloseable { this.zipfile = new ZipFile(file.toFile()); } + /** + * Returns the {@code Entry} for a resource in a JMOD file section + * or {@code null} if not found. + */ + public Entry getEntry(Section section, String name) { + String entry = section.jmodDir() + "/" + name; + ZipEntry ze = zipfile.getEntry(entry); + return (ze != null) ? new Entry(ze) : null; + } + /** * Opens an {@code InputStream} for reading the named entry of the given * section in this jmod file. @@ -185,7 +195,6 @@ public class JmodFile implements AutoCloseable { public InputStream getInputStream(Section section, String name) throws IOException { - String entry = section.jmodDir() + "/" + name; ZipEntry e = zipfile.getEntry(entry); if (e == null) { @@ -194,6 +203,15 @@ public class JmodFile implements AutoCloseable { return zipfile.getInputStream(e); } + /** + * Opens an {@code InputStream} for reading an entry in the JMOD file. + * + * @throws IOException if an I/O error occurs + */ + public InputStream getInputStream(Entry entry) throws IOException { + return zipfile.getInputStream(entry.zipEntry()); + } + /** * Returns a stream of non-directory entries in this jmod file. */ diff --git a/jdk/src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java b/jdk/src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java index b6e58fb932b..75d509606d2 100644 --- a/jdk/src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java +++ b/jdk/src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java @@ -53,6 +53,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.Attributes; import java.util.jar.Manifest; +import java.util.stream.Stream; import jdk.internal.module.ModulePatcher.PatchedModuleReader; import jdk.internal.misc.VM; @@ -749,6 +750,10 @@ public class BuiltinClassLoader return Optional.empty(); } @Override + public Stream list() { + return Stream.empty(); + } + @Override public void close() { throw new InternalError("Should not get here"); } diff --git a/jdk/src/java.base/share/classes/jdk/internal/loader/Loader.java b/jdk/src/java.base/share/classes/jdk/internal/loader/Loader.java index 21900b5731e..8b2e257d7b8 100644 --- a/jdk/src/java.base/share/classes/jdk/internal/loader/Loader.java +++ b/jdk/src/java.base/share/classes/jdk/internal/loader/Loader.java @@ -53,6 +53,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; /** @@ -534,6 +535,10 @@ public final class Loader extends SecureClassLoader { return Optional.empty(); } @Override + public Stream list() { + return Stream.empty(); + } + @Override public void close() { throw new InternalError("Should not get here"); } diff --git a/jdk/src/java.base/share/classes/jdk/internal/misc/JavaLangModuleAccess.java b/jdk/src/java.base/share/classes/jdk/internal/misc/JavaLangModuleAccess.java index cf0e8f797d9..d19579212dd 100644 --- a/jdk/src/java.base/share/classes/jdk/internal/misc/JavaLangModuleAccess.java +++ b/jdk/src/java.base/share/classes/jdk/internal/misc/JavaLangModuleAccess.java @@ -39,6 +39,7 @@ import java.util.Collection; import java.lang.module.ModuleReader; import java.lang.module.ModuleReference; import java.net.URI; +import java.nio.file.Path; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -102,6 +103,11 @@ public interface JavaLangModuleAccess { Set packages, ModuleHashes hashes); + /** + * Returns the object with the hashes of other modules + */ + Optional hashes(ModuleDescriptor descriptor); + /** * Resolves a collection of root modules, with service binding * and the empty configuration as the parent. The post resolution @@ -120,8 +126,10 @@ public interface JavaLangModuleAccess { Supplier readerSupplier); /** - * Returns the object with the hashes of other modules + * Creates a ModuleFinder for a module path. */ - Optional hashes(ModuleDescriptor descriptor); + ModuleFinder newModulePath(Runtime.Version version, + boolean isLinkPhase, + Path... entries); } diff --git a/jdk/src/java.base/share/classes/jdk/internal/module/ConfigurableModuleFinder.java b/jdk/src/java.base/share/classes/jdk/internal/module/ConfigurableModuleFinder.java deleted file mode 100644 index 914f870044e..00000000000 --- a/jdk/src/java.base/share/classes/jdk/internal/module/ConfigurableModuleFinder.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package jdk.internal.module; - -import java.lang.module.ModuleFinder; - -/** - * A ModuleFinder that may be configured to work at either run-time - * or link-time. - */ - -public interface ConfigurableModuleFinder extends ModuleFinder { - - public static enum Phase { - RUN_TIME, - LINK_TIME - } - - /** - * Configures this finder to work in the given phase. - */ - void configurePhase(Phase phase); - -} diff --git a/jdk/src/java.base/share/classes/jdk/internal/module/ModuleInfoWriter.java b/jdk/src/java.base/share/classes/jdk/internal/module/ModuleInfoWriter.java index c86416e1dab..a8dda9b8d57 100644 --- a/jdk/src/java.base/share/classes/jdk/internal/module/ModuleInfoWriter.java +++ b/jdk/src/java.base/share/classes/jdk/internal/module/ModuleInfoWriter.java @@ -57,7 +57,15 @@ public final class ModuleInfoWriter { cw.visit(Opcodes.V1_9, ACC_MODULE, name, null, null, null); cw.visitAttribute(new ModuleAttribute(md)); - cw.visitAttribute(new ConcealedPackagesAttribute(md.conceals())); + + // for tests: write the ConcealedPackages attribute when there are non-exported packages + long nExportedPackages = md.exports().stream() + .map(ModuleDescriptor.Exports::source) + .distinct() + .count(); + if (md.packages().size() > nExportedPackages) + cw.visitAttribute(new ConcealedPackagesAttribute(md.packages())); + md.version().ifPresent(v -> cw.visitAttribute(new VersionAttribute(v))); md.mainClass().ifPresent(mc -> cw.visitAttribute(new MainClassAttribute(mc))); diff --git a/jdk/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java b/jdk/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java index ebc7d85a5e8..afee8bf3c36 100644 --- a/jdk/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java +++ b/jdk/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java @@ -50,6 +50,7 @@ import java.util.Optional; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.stream.Stream; import jdk.internal.loader.Resource; import jdk.internal.misc.JavaLangModuleAccess; @@ -159,21 +160,19 @@ public final class ModulePatcher { // is not supported by the boot class loader try (JarFile jf = new JarFile(file.toFile())) { jf.stream() - .filter(e -> e.getName().endsWith(".class")) .map(e -> toPackageName(file, e)) - .filter(pn -> pn.length() > 0) + .filter(Checks::isJavaIdentifier) .forEach(packages::add); } } else if (Files.isDirectory(file)) { - // exploded directory + // exploded directory without following sym links Path top = file; Files.find(top, Integer.MAX_VALUE, - ((path, attrs) -> attrs.isRegularFile() && - path.toString().endsWith(".class"))) + ((path, attrs) -> attrs.isRegularFile())) .map(path -> toPackageName(top, path)) - .filter(pn -> pn.length() > 0) + .filter(Checks::isJavaIdentifier) .forEach(packages::add); } @@ -380,6 +379,15 @@ public final class ModulePatcher { } } + @Override + public Stream list() throws IOException { + Stream s = delegate().list(); + for (ResourceFinder finder : finders) { + s = Stream.concat(s, finder.list()); + } + return s.distinct(); + } + @Override public void close() throws IOException { closeAll(finders); @@ -393,6 +401,7 @@ public final class ModulePatcher { */ private static interface ResourceFinder extends Closeable { Resource find(String name) throws IOException; + Stream list() throws IOException; } @@ -453,6 +462,13 @@ public final class ModulePatcher { } }; } + + @Override + public Stream list() throws IOException { + return jf.stream() + .filter(e -> !e.isDirectory()) + .map(JarEntry::getName); + } } @@ -527,6 +543,15 @@ public final class ModulePatcher { } }; } + + @Override + public Stream list() throws IOException { + return Files.find(dir, Integer.MAX_VALUE, + (path, attrs) -> attrs.isRegularFile()) + .map(f -> dir.relativize(f) + .toString() + .replace(File.separatorChar, '/')); + } } @@ -537,7 +562,7 @@ public final class ModulePatcher { Path entry = top.relativize(file); Path parent = entry.getParent(); if (parent == null) { - return warnUnnamedPackage(top, entry.toString()); + return warnIfModuleInfo(top, entry.toString()); } else { return parent.toString().replace(File.separatorChar, '.'); } @@ -557,14 +582,15 @@ public final class ModulePatcher { String name = entry.getName(); int index = name.lastIndexOf("/"); if (index == -1) { - return warnUnnamedPackage(file, name); + return warnIfModuleInfo(file, name); } else { return name.substring(0, index).replace('/', '.'); } } - private static String warnUnnamedPackage(Path file, String e) { - System.err.println("WARNING: " + e + " not allowed in patch: " + file); + private static String warnIfModuleInfo(Path file, String e) { + if (e.equals("module-info.class")) + System.err.println("WARNING: " + e + " ignored in patch: " + file); return ""; } diff --git a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java index 1d5530cb040..a51b5328a1e 100644 --- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java +++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java @@ -42,8 +42,6 @@ import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; -import jdk.internal.module.ConfigurableModuleFinder; -import jdk.internal.module.ConfigurableModuleFinder.Phase; import jdk.tools.jlink.internal.TaskHelper.BadArgs; import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE; import jdk.tools.jlink.internal.Jlink.JlinkConfiguration; @@ -54,6 +52,7 @@ import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider; import jdk.tools.jlink.plugin.PluginException; import jdk.tools.jlink.builder.DefaultImageBuilder; import jdk.tools.jlink.plugin.Plugin; +import jdk.internal.misc.SharedSecrets; /** * Implementation for the jlink tool. @@ -252,8 +251,9 @@ public class JlinkTask { throw new Exception("Empty module paths"); } - ModuleFinder finder - = newModuleFinder(config.getModulepaths(), config.getLimitmods(), config.getModules()); + ModuleFinder finder = newModuleFinder(config.getModulepaths(), + config.getLimitmods(), + config.getModules()); // First create the image provider ImageProvider imageProvider @@ -328,24 +328,41 @@ public class JlinkTask { return addMods; } - public static ModuleFinder newModuleFinder(List paths, - Set limitMods, - Set addMods) - { - ModuleFinder finder = ModuleFinder.of(paths.toArray(new Path[0])); + /** + * Returns a module finder to find the observable modules specified in + * the --module-path and --limit-modules options + */ + private ModuleFinder modulePathFinder() { + Path[] entries = options.modulePath.toArray(new Path[0]); + ModuleFinder finder = SharedSecrets.getJavaLangModuleAccess() + .newModulePath(Runtime.version(), true, entries); - // jmods are located at link-time - if (finder instanceof ConfigurableModuleFinder) { - ((ConfigurableModuleFinder) finder).configurePhase(Phase.LINK_TIME); - } - - // if limitmods is specified then limit the universe - if (!limitMods.isEmpty()) { - finder = limitFinder(finder, limitMods, addMods); + if (!options.limitMods.isEmpty()) { + finder = limitFinder(finder, options.limitMods, Collections.emptySet()); } return finder; } + /** + * Returns a module finder of the given module path that limits + * the observable modules to those in the transitive closure of + * the modules specified in {@code limitMods} plus other modules + * specified in the {@code roots} set. + */ + public static ModuleFinder newModuleFinder(List paths, + Set limitMods, + Set roots) + { + Path[] entries = paths.toArray(new Path[0]); + ModuleFinder finder = SharedSecrets.getJavaLangModuleAccess() + .newModulePath(Runtime.version(), true, entries); + + // if limitmods is specified then limit the universe + if (!limitMods.isEmpty()) { + finder = limitFinder(finder, limitMods, roots); + } + return finder; + } private static Path toPathLocation(ResolvedModule m) { Optional ouri = m.reference().location(); diff --git a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java index 3b0f9b8a4e4..79fd4411550 100644 --- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java +++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java @@ -47,8 +47,6 @@ import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; -import jdk.internal.module.ConfigurableModuleFinder; -import jdk.internal.module.ConfigurableModuleFinder.Phase; import jdk.tools.jlink.internal.plugins.ExcludeFilesPlugin; import jdk.tools.jlink.internal.plugins.ExcludeJmodSectionPlugin; import jdk.tools.jlink.plugin.Plugin; @@ -60,6 +58,7 @@ import jdk.tools.jlink.internal.Jlink.PluginsConfiguration; import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin; import jdk.tools.jlink.internal.plugins.StripDebugPlugin; +import jdk.internal.misc.SharedSecrets; /** * @@ -726,14 +725,10 @@ public final class TaskHelper { } static Layer createPluginsLayer(List paths) { - Path[] arr = new Path[paths.size()]; - paths.toArray(arr); - ModuleFinder finder = ModuleFinder.of(arr); - // jmods are located at link-time - if (finder instanceof ConfigurableModuleFinder) { - ((ConfigurableModuleFinder) finder).configurePhase(Phase.LINK_TIME); - } + Path[] dirs = paths.toArray(new Path[0]); + ModuleFinder finder = SharedSecrets.getJavaLangModuleAccess() + .newModulePath(Runtime.version(), true, dirs); Configuration bootConfiguration = Layer.boot().configuration(); try { diff --git a/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java b/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java index d5db2a00991..081609d9309 100644 --- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java +++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java @@ -98,8 +98,6 @@ import jdk.internal.joptsimple.OptionSpec; import jdk.internal.joptsimple.ValueConverter; import jdk.internal.misc.JavaLangModuleAccess; import jdk.internal.misc.SharedSecrets; -import jdk.internal.module.ConfigurableModuleFinder; -import jdk.internal.module.ConfigurableModuleFinder.Phase; import jdk.internal.module.ModuleHashes; import jdk.internal.module.ModuleInfoExtender; import jdk.tools.jlink.internal.Utils; @@ -1298,9 +1296,7 @@ public class JmodTask { options.manPages = opts.valuesOf(manPages); if (opts.has(modulePath)) { Path[] dirs = opts.valuesOf(modulePath).toArray(new Path[0]); - options.moduleFinder = ModuleFinder.of(dirs); - if (options.moduleFinder instanceof ConfigurableModuleFinder) - ((ConfigurableModuleFinder)options.moduleFinder).configurePhase(Phase.LINK_TIME); + options.moduleFinder = JLMA.newModulePath(Runtime.version(), true, dirs); } if (opts.has(moduleVersion)) options.moduleVersion = opts.valueOf(moduleVersion); diff --git a/jdk/test/java/lang/module/AutomaticModulesTest.java b/jdk/test/java/lang/module/AutomaticModulesTest.java index 680a727127e..3ad04a2a943 100644 --- a/jdk/test/java/lang/module/AutomaticModulesTest.java +++ b/jdk/test/java/lang/module/AutomaticModulesTest.java @@ -158,39 +158,84 @@ public class AutomaticModulesTest { */ public void testPackages() throws IOException { Path dir = Files.createTempDirectory(USER_DIR, "mods"); - createDummyJarFile(dir.resolve("m1.jar"), + createDummyJarFile(dir.resolve("m.jar"), "p/C1.class", "p/C2.class", "q/C1.class"); ModuleFinder finder = ModuleFinder.of(dir); + Optional mref = finder.find("m"); + assertTrue(mref.isPresent(), "m not found"); - Configuration parent = Layer.boot().configuration(); - Configuration cf = resolve(parent, finder, "m1"); + ModuleDescriptor descriptor = mref.get().descriptor(); - ModuleDescriptor m1 = findDescriptor(cf, "m1"); - - Set exports - = m1.exports().stream().map(Exports::source).collect(Collectors.toSet()); + assertTrue(descriptor.packages().size() == 2); + assertTrue(descriptor.packages().contains("p")); + assertTrue(descriptor.packages().contains("q")); + Set exports = descriptor.exports().stream() + .map(Exports::source) + .collect(Collectors.toSet()); assertTrue(exports.size() == 2); assertTrue(exports.contains("p")); assertTrue(exports.contains("q")); - assertTrue(m1.conceals().isEmpty()); } - /** - * Test class file in JAR file where the entry does not correspond to a + * Test class files in JAR file where the entry does not correspond to a * legal package name. */ - @Test(expectedExceptions = FindException.class) public void testBadPackage() throws IOException { Path dir = Files.createTempDirectory(USER_DIR, "mods"); - createDummyJarFile(dir.resolve("m1.jar"), "p-/T.class"); + createDummyJarFile(dir.resolve("m.jar"), "p/C1.class", "p-/C2.class"); - // should throw FindException - ModuleFinder.of(dir).findAll(); + ModuleFinder finder = ModuleFinder.of(dir); + Optional mref = finder.find("m"); + assertTrue(mref.isPresent(), "m not found"); + + ModuleDescriptor descriptor = mref.get().descriptor(); + + assertTrue(descriptor.packages().size() == 1); + assertTrue(descriptor.packages().contains("p")); + + Set exports = descriptor.exports().stream() + .map(Exports::source) + .collect(Collectors.toSet()); + assertTrue(exports.size() == 1); + assertTrue(exports.contains("p")); } + /** + * Test non-class resources in a JAR file. + */ + public void testNonClassResources() throws IOException { + Path dir = Files.createTempDirectory(USER_DIR, "mods"); + createDummyJarFile(dir.resolve("m.jar"), + "LICENSE", + "README", + "WEB-INF/tags", + "p/Type.class", + "p/resources/m.properties"); + + ModuleFinder finder = ModuleFinder.of(dir); + Optional mref = finder.find("m"); + assertTrue(mref.isPresent(), "m not found"); + + ModuleDescriptor descriptor = mref.get().descriptor(); + + assertTrue(descriptor.packages().size() == 2); + assertTrue(descriptor.packages().contains("p")); + assertTrue(descriptor.packages().contains("p.resources")); + } + + /** + * Test .class file in unnamed package (top-level directory) + */ + @Test(expectedExceptions = FindException.class) + public void testClassInUnnamedPackage() throws IOException { + Path dir = Files.createTempDirectory(USER_DIR, "mods"); + createDummyJarFile(dir.resolve("m.jar"), "Mojo.class"); + ModuleFinder finder = ModuleFinder.of(dir); + finder.findAll(); + } /** * Test JAR file with META-INF/services configuration file @@ -204,12 +249,12 @@ public class AutomaticModulesTest { Files.createDirectories(services); Files.write(services.resolve(service), Set.of(provider)); Path dir = Files.createTempDirectory(USER_DIR, "mods"); - JarUtils.createJarFile(dir.resolve("m1.jar"), tmpdir); + JarUtils.createJarFile(dir.resolve("m.jar"), tmpdir); ModuleFinder finder = ModuleFinder.of(dir); - Optional mref = finder.find("m1"); - assertTrue(mref.isPresent(), "m1 not found"); + Optional mref = finder.find("m"); + assertTrue(mref.isPresent(), "m not found"); ModuleDescriptor descriptor = mref.get().descriptor(); assertTrue(descriptor.provides().size() == 1); @@ -220,17 +265,12 @@ public class AutomaticModulesTest { } - // META-INF/services configuration file/entries that are not legal - @DataProvider(name = "badproviders") - public Object[][] createProviders() { + // META-INF/services files that don't map to legal service names + @DataProvider(name = "badservices") + public Object[][] createBadServices() { return new Object[][] { // service type provider type - - { "p.S", "-" }, - { "p.S", ".S1" }, - { "p.S", "S1." }, - { "-", "p.S1" }, { ".S", "p.S1" }, }; @@ -240,8 +280,8 @@ public class AutomaticModulesTest { * Test JAR file with META-INF/services configuration file with bad * values or names. */ - @Test(dataProvider = "badproviders", expectedExceptions = FindException.class) - public void testBadServicesConfiguration(String service, String provider) + @Test(dataProvider = "badservices") + public void testBadServicesNames(String service, String provider) throws IOException { Path tmpdir = Files.createTempDirectory(USER_DIR, "tmp"); @@ -249,7 +289,41 @@ public class AutomaticModulesTest { Files.createDirectories(services); Files.write(services.resolve(service), Set.of(provider)); Path dir = Files.createTempDirectory(USER_DIR, "mods"); - JarUtils.createJarFile(dir.resolve("m1.jar"), tmpdir); + JarUtils.createJarFile(dir.resolve("m.jar"), tmpdir); + + Optional omref = ModuleFinder.of(dir).find("m"); + assertTrue(omref.isPresent()); + ModuleDescriptor descriptor = omref.get().descriptor(); + assertTrue(descriptor.provides().isEmpty()); + } + + + // META-INF/services configuration file entries that are not legal + @DataProvider(name = "badproviders") + public Object[][] createBadProviders() { + return new Object[][] { + + // service type provider type + { "p.S", "-" }, + { "p.S", ".S1" }, + { "p.S", "S1." }, + }; + } + + /** + * Test JAR file with META-INF/services configuration file with bad + * values or names. + */ + @Test(dataProvider = "badproviders", expectedExceptions = FindException.class) + public void testBadProvideNames(String service, String provider) + throws IOException + { + Path tmpdir = Files.createTempDirectory(USER_DIR, "tmp"); + Path services = tmpdir.resolve("META-INF").resolve("services"); + Files.createDirectories(services); + Files.write(services.resolve(service), Set.of(provider)); + Path dir = Files.createTempDirectory(USER_DIR, "mods"); + JarUtils.createJarFile(dir.resolve("m.jar"), tmpdir); // should throw FindException ModuleFinder.of(dir).findAll(); diff --git a/jdk/test/java/lang/module/ModuleFinderTest.java b/jdk/test/java/lang/module/ModuleFinderTest.java index 58c2ae56334..9a79a5757de 100644 --- a/jdk/test/java/lang/module/ModuleFinderTest.java +++ b/jdk/test/java/lang/module/ModuleFinderTest.java @@ -29,6 +29,7 @@ * @summary Basic tests for java.lang.module.ModuleFinder */ +import java.io.File; import java.io.OutputStream; import java.lang.module.FindException; import java.lang.module.InvalidModuleDescriptorException; @@ -38,6 +39,7 @@ import java.lang.module.ModuleReference; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -316,6 +318,111 @@ public class ModuleFinderTest { } + /** + * Test ModuleFinder with a JAR file containing a mix of class and + * non-class resources. + */ + public void testOfOneJarFileWithResources() throws Exception { + Path dir = Files.createTempDirectory(USER_DIR, "mods"); + Path jar = createModularJar(dir.resolve("m.jar"), "m", + "LICENSE", + "README", + "WEB-INF/tags", + "p/Type.class", + "p/resources/m.properties", + "q-/Type.class", // not a legal package name + "q-/resources/m/properties"); + + ModuleFinder finder = ModuleFinder.of(jar); + Optional mref = finder.find("m"); + assertTrue(mref.isPresent(), "m not found"); + + ModuleDescriptor descriptor = mref.get().descriptor(); + + assertTrue(descriptor.packages().size() == 2); + assertTrue(descriptor.packages().contains("p")); + assertTrue(descriptor.packages().contains("p.resources")); + } + + + /** + * Test ModuleFinder with an exploded module containing a mix of class + * and non-class resources + */ + public void testOfOneExplodedModuleWithResources() throws Exception { + Path dir = Files.createTempDirectory(USER_DIR, "mods"); + Path m_dir = createExplodedModule(dir.resolve("m"), "m", + "LICENSE", + "README", + "WEB-INF/tags", + "p/Type.class", + "p/resources/m.properties", + "q-/Type.class", // not a legal package name + "q-/resources/m/properties"); + + ModuleFinder finder = ModuleFinder.of(m_dir); + Optional mref = finder.find("m"); + assertTrue(mref.isPresent(), "m not found"); + + ModuleDescriptor descriptor = mref.get().descriptor(); + + assertTrue(descriptor.packages().size() == 2); + assertTrue(descriptor.packages().contains("p")); + assertTrue(descriptor.packages().contains("p.resources")); + } + + + /** + * Test ModuleModule with a JAR file containing a .class file in the top + * level directory. + */ + public void testOfOneJarFileWithTopLevelClass() throws Exception { + Path dir = Files.createTempDirectory(USER_DIR, "mods"); + Path jar = createModularJar(dir.resolve("m.jar"), "m", "Mojo.class"); + + ModuleFinder finder = ModuleFinder.of(jar); + try { + finder.find("m"); + assertTrue(false); + } catch (FindException e) { + assertTrue(e.getCause() instanceof InvalidModuleDescriptorException); + } + + finder = ModuleFinder.of(jar); + try { + finder.findAll(); + assertTrue(false); + } catch (FindException e) { + assertTrue(e.getCause() instanceof InvalidModuleDescriptorException); + } + } + + /** + * Test ModuleModule with a JAR file containing a .class file in the top + * level directory. + */ + public void testOfOneExplodedModuleWithTopLevelClass() throws Exception { + Path dir = Files.createTempDirectory(USER_DIR, "mods"); + Path m_dir = createExplodedModule(dir.resolve("m"), "m", "Mojo.class"); + + ModuleFinder finder = ModuleFinder.of(m_dir); + try { + finder.find("m"); + assertTrue(false); + } catch (FindException e) { + assertTrue(e.getCause() instanceof InvalidModuleDescriptorException); + } + + finder = ModuleFinder.of(m_dir); + try { + finder.findAll(); + assertTrue(false); + } catch (FindException e) { + assertTrue(e.getCause() instanceof InvalidModuleDescriptorException); + } + } + + /** * Test ModuleFinder.of with a path to a file that does not exist. */ @@ -641,7 +748,7 @@ public class ModuleFinderTest { vs = mid.substring(i+1); } ModuleDescriptor.Builder builder - = new ModuleDescriptor.Builder(mn).requires("java.base"); + = new ModuleDescriptor.Builder(mn).requires("java.base"); if (vs != null) builder.version(vs); return builder.build(); @@ -651,13 +758,22 @@ public class ModuleFinderTest { * Creates an exploded module in the given directory and containing a * module descriptor with the given module name/version. */ - static Path createExplodedModule(Path dir, String mid) throws Exception { + static Path createExplodedModule(Path dir, String mid, String... entries) + throws Exception + { ModuleDescriptor descriptor = newModuleDescriptor(mid); Files.createDirectories(dir); Path mi = dir.resolve("module-info.class"); try (OutputStream out = Files.newOutputStream(mi)) { ModuleInfoWriter.write(descriptor, out); } + + for (String entry : entries) { + Path file = dir.resolve(entry.replace('/', File.separatorChar)); + Files.createDirectories(file.getParent()); + Files.createFile(file); + } + return dir; } diff --git a/jdk/test/java/lang/module/ModuleReader/ModuleReaderTest.java b/jdk/test/java/lang/module/ModuleReader/ModuleReaderTest.java index 139a56871b9..63c4c7404e0 100644 --- a/jdk/test/java/lang/module/ModuleReader/ModuleReaderTest.java +++ b/jdk/test/java/lang/module/ModuleReader/ModuleReaderTest.java @@ -24,9 +24,8 @@ /** * @test * @library /lib/testlibrary - * @modules java.base/jdk.internal.module + * @modules java.base/jdk.internal.misc * jdk.compiler - * jdk.jlink * @build ModuleReaderTest CompilerUtils JarUtils * @run testng ModuleReaderTest * @summary Basic tests for java.lang.module.ModuleReader @@ -47,11 +46,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import java.util.spi.ToolProvider; -import jdk.internal.module.ConfigurableModuleFinder; -import jdk.internal.module.ConfigurableModuleFinder.Phase; +import jdk.internal.misc.SharedSecrets; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; @@ -101,7 +103,7 @@ public class ModuleReaderTest { /** * Test ModuleReader to module in runtime image */ - public void testImage() throws Exception { + public void testImage() throws IOException { ModuleFinder finder = ModuleFinder.ofSystem(); ModuleReference mref = finder.find(BASE_MODULE).get(); @@ -119,6 +121,7 @@ public class ModuleReaderTest { testFind(reader, name, expectedBytes); testOpen(reader, name, expectedBytes); testRead(reader, name, expectedBytes); + testList(reader, name); } @@ -168,7 +171,7 @@ public class ModuleReaderTest { /** * Test ModuleReader to exploded module */ - public void testExplodedModule() throws Exception { + public void testExplodedModule() throws IOException { test(MODS_DIR); } @@ -176,7 +179,7 @@ public class ModuleReaderTest { /** * Test ModuleReader to modular JAR */ - public void testModularJar() throws Exception { + public void testModularJar() throws IOException { Path dir = Files.createTempDirectory(USER_DIR, "mlib"); // jar cf mlib/${TESTMODULE}.jar -C mods . @@ -190,7 +193,7 @@ public class ModuleReaderTest { /** * Test ModuleReader to JMOD */ - public void testJMod() throws Exception { + public void testJMod() throws IOException { Path dir = Files.createTempDirectory(USER_DIR, "mlib"); // jmod create --class-path mods/${TESTMODULE} mlib/${TESTMODULE}.jmod @@ -211,13 +214,10 @@ public class ModuleReaderTest { * The test module is found on the given module path. Open a ModuleReader * to the test module and test the reader. */ - void test(Path mp) throws Exception { + void test(Path mp) throws IOException { - ModuleFinder finder = ModuleFinder.of(mp); - if (finder instanceof ConfigurableModuleFinder) { - // need ModuleFinder to be in the phase to find JMOD files - ((ConfigurableModuleFinder)finder).configurePhase(Phase.LINK_TIME); - } + ModuleFinder finder = SharedSecrets.getJavaLangModuleAccess() + .newModulePath(Runtime.version(), true, mp); ModuleReference mref = finder.find(TEST_MODULE).get(); ModuleReader reader = mref.open(); @@ -234,6 +234,7 @@ public class ModuleReaderTest { testFind(reader, name, expectedBytes); testOpen(reader, name, expectedBytes); testRead(reader, name, expectedBytes); + testList(reader, name); } // test "not found" @@ -275,13 +276,18 @@ public class ModuleReaderTest { reader.read(TEST_RESOURCES[0]); assertTrue(false); } catch (IOException expected) { } + + try { + reader.list(); + assertTrue(false); + } catch (IOException expected) { } } /** * Test ModuleReader#find */ void testFind(ModuleReader reader, String name, byte[] expectedBytes) - throws Exception + throws IOException { Optional ouri = reader.find(name); assertTrue(ouri.isPresent()); @@ -301,7 +307,7 @@ public class ModuleReaderTest { * Test ModuleReader#open */ void testOpen(ModuleReader reader, String name, byte[] expectedBytes) - throws Exception + throws IOException { Optional oin = reader.open(name); assertTrue(oin.isPresent()); @@ -317,7 +323,7 @@ public class ModuleReaderTest { * Test ModuleReader#read */ void testRead(ModuleReader reader, String name, byte[] expectedBytes) - throws Exception + throws IOException { Optional obb = reader.read(name); assertTrue(obb.isPresent()); @@ -334,4 +340,24 @@ public class ModuleReaderTest { } } + /** + * Test ModuleReader#list + */ + void testList(ModuleReader reader, String name) throws IOException { + List list = reader.list().collect(Collectors.toList()); + Set names = new HashSet<>(list); + assertTrue(names.size() == list.size()); // no duplicates + + assertTrue(names.contains("module-info.class")); + assertTrue(names.contains(name)); + + // all resources should be locatable via find + for (String e : names) { + assertTrue(reader.find(e).isPresent()); + } + + // should not contain directories + names.forEach(e -> assertFalse(e.endsWith("/"))); + } + } diff --git a/jdk/test/java/lang/module/ModuleReader/MultiReleaseJarTest.java b/jdk/test/java/lang/module/ModuleReader/MultiReleaseJarTest.java deleted file mode 100644 index 4bb6ceaf34f..00000000000 --- a/jdk/test/java/lang/module/ModuleReader/MultiReleaseJarTest.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/** - * @test - * @library /lib/testlibrary - * @modules java.base/jdk.internal.module - * @build MultiReleaseJarTest JarUtils - * @run testng MultiReleaseJarTest - * @run testng/othervm -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarTest - * @summary Basic test of ModuleReader with a modular JAR that is also a - * multi-release JAR - */ - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.module.ModuleDescriptor; -import java.lang.module.ModuleFinder; -import java.lang.module.ModuleReader; -import java.lang.module.ModuleReference; -import java.net.URI; -import java.net.URLConnection; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Optional; -import java.util.Set; -import java.util.jar.Attributes; -import java.util.jar.Manifest; -import java.util.stream.Collectors; - -import jdk.internal.module.ModuleInfoWriter; - -import org.testng.annotations.Test; -import static org.testng.Assert.*; - -/** - * Exercises ModuleReader with a modular JAR containing the following files: - * - *
    {@code
    - *     module-info.class
    - *     META-INF/versions//module.class
    - * }
    - * - * The module-info.class in the top-level directory is the binary form of: - *
    {@code
    - *     module jdk.test {
    - *         requires java.base;
    - *     }
    - * }
    - * - * The module-info.class in the versioned section is the binary form of: - *
    {@code
    - *     module jdk.test {
    - *         requires java.base;
    - *         requires jdk.unsupported;
    - *     }
    - * }
    - */ - -@Test -public class MultiReleaseJarTest { - - // Java SE/JDK major release - private static final int RELEASE = Runtime.version().major(); - - // the name of the test module - private static final String MODULE_NAME = "jdk.test"; - - private static final String MODULE_INFO_CLASS = "module-info.class"; - - /** - * Uses the ModuleFinder API to locate the module packaged as a modular - * and mutli-release JAR and then creates a ModuleReader to access the - * contents of the module. - */ - public void testMultiReleaseJar() throws IOException { - - // are multi-release JARs enabled? - String s = System.getProperty("jdk.util.jar.enableMultiRelease"); - boolean multiRelease = (s == null || Boolean.parseBoolean(s)); - - // create the multi-release modular JAR - Path jarfile = createJarFile(); - - // find the module - ModuleFinder finder = ModuleFinder.of(jarfile); - Optional omref = finder.find(MODULE_NAME); - assertTrue((omref.isPresent())); - ModuleReference mref = omref.get(); - - // test that correct module-info.class was read - checkDescriptor(mref.descriptor(), multiRelease); - - // test ModuleReader - try (ModuleReader reader = mref.open()) { - - // open resource - Optional oin = reader.open(MODULE_INFO_CLASS); - assertTrue(oin.isPresent()); - try (InputStream in = oin.get()) { - checkDescriptor(ModuleDescriptor.read(in), multiRelease); - } - - // read resource - Optional obb = reader.read(MODULE_INFO_CLASS); - assertTrue(obb.isPresent()); - ByteBuffer bb = obb.get(); - try { - checkDescriptor(ModuleDescriptor.read(bb), multiRelease); - } finally { - reader.release(bb); - } - - // find resource - Optional ouri = reader.find(MODULE_INFO_CLASS); - assertTrue(ouri.isPresent()); - URI uri = ouri.get(); - - String expectedTail = "!/"; - if (multiRelease) - expectedTail += "META-INF/versions/" + RELEASE + "/"; - expectedTail += MODULE_INFO_CLASS; - assertTrue(uri.toString().endsWith(expectedTail)); - - URLConnection uc = uri.toURL().openConnection(); - uc.setUseCaches(false); - try (InputStream in = uc.getInputStream()) { - checkDescriptor(ModuleDescriptor.read(in), multiRelease); - } - - } - - } - - /** - * Checks that the module descriptor is the expected module descriptor. - * When the multi release JAR feature is enabled then the module - * descriptor is expected to have been read from the versioned section - * of the JAR file. - */ - private void checkDescriptor(ModuleDescriptor descriptor, boolean multiRelease) { - Set requires = descriptor.requires().stream() - .map(ModuleDescriptor.Requires::name) - .collect(Collectors.toSet()); - assertTrue(requires.contains("java.base")); - assertTrue(requires.contains("jdk.unsupported") == multiRelease); - } - - /** - * Creates the modular JAR for the test, returning the Path to the JAR file. - */ - private Path createJarFile() throws IOException { - - // module descriptor for top-level directory - ModuleDescriptor descriptor1 - = new ModuleDescriptor.Builder(MODULE_NAME) - .requires("java.base") - .build(); - - // module descriptor for versioned section - ModuleDescriptor descriptor2 - = new ModuleDescriptor.Builder(MODULE_NAME) - .requires("java.base") - .requires("jdk.unsupported") - .build(); - - Path top = Paths.get(MODULE_NAME); - Files.createDirectories(top); - - Path mi1 = Paths.get(MODULE_INFO_CLASS); - try (OutputStream out = Files.newOutputStream(top.resolve(mi1))) { - ModuleInfoWriter.write(descriptor1, out); - } - - Path vdir = Paths.get("META-INF", "versions", Integer.toString(RELEASE)); - Files.createDirectories(top.resolve(vdir)); - - Path mi2 = vdir.resolve(MODULE_INFO_CLASS); - try (OutputStream out = Files.newOutputStream(top.resolve(mi2))) { - ModuleInfoWriter.write(descriptor2, out); - } - - Manifest man = new Manifest(); - Attributes attrs = man.getMainAttributes(); - attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); - attrs.put(Attributes.Name.MULTI_RELEASE, "true"); - - Path jarfile = Paths.get(MODULE_NAME + ".jar"); - JarUtils.createJarFile(jarfile, man, top, mi1, mi2); - - return jarfile; - } -} diff --git a/jdk/test/java/lang/module/MultiReleaseJarTest.java b/jdk/test/java/lang/module/MultiReleaseJarTest.java new file mode 100644 index 00000000000..ad982650334 --- /dev/null +++ b/jdk/test/java/lang/module/MultiReleaseJarTest.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @library /lib/testlibrary + * @modules java.base/jdk.internal.module + * @build MultiReleaseJarTest JarUtils + * @run testng MultiReleaseJarTest + * @run testng/othervm -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarTest + * @summary Basic test of modular JARs as multi-release JARs + */ + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReader; +import java.lang.module.ModuleReference; +import java.net.URI; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import jdk.internal.module.ModuleInfoWriter; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + + +@Test +public class MultiReleaseJarTest { + + private static final String MODULE_INFO = "module-info.class"; + + private static final int RELEASE = Runtime.version().major(); + + // are multi-release JARs enabled? + private static final boolean MULTI_RELEASE; + static { + String s = System.getProperty("jdk.util.jar.enableMultiRelease"); + MULTI_RELEASE = (s == null || Boolean.parseBoolean(s)); + } + + /** + * Basic test of a multi-release JAR. + */ + public void testBasic() throws Exception { + String name = "m1"; + + ModuleDescriptor descriptor = new ModuleDescriptor.Builder(name) + .requires("java.base") + .build(); + + Path jar = new JarBuilder(name) + .moduleInfo("module-info.class", descriptor) + .resource("p/Main.class") + .resource("p/Helper.class") + .resource("META-INF/versions/9/p/Helper.class") + .resource("META-INF/versions/9/p/internal/Helper9.class") + .build(); + + // find the module + ModuleFinder finder = ModuleFinder.of(jar); + Optional omref = finder.find(name); + assertTrue((omref.isPresent())); + ModuleReference mref = omref.get(); + + // check module packages + descriptor = mref.descriptor(); + Set packages = descriptor.packages(); + assertTrue(packages.contains("p")); + if (MULTI_RELEASE) { + assertTrue(packages.size() == 2); + assertTrue(packages.contains("p.internal")); + } else { + assertTrue(packages.size() == 1); + } + } + + /** + * Test a multi-release JAR with a module-info.class in the versioned + * section of the JAR. + */ + public void testModuleInfoInVersionedSection() throws Exception { + String name = "m1"; + + ModuleDescriptor descriptor1 = new ModuleDescriptor.Builder(name) + .requires("java.base") + .build(); + + // module descriptor for versioned section + ModuleDescriptor descriptor2 = new ModuleDescriptor.Builder(name) + .requires("java.base") + .requires("jdk.unsupported") + .build(); + + Path jar = new JarBuilder(name) + .moduleInfo(MODULE_INFO, descriptor1) + .resource("p/Main.class") + .resource("p/Helper.class") + .moduleInfo("META-INF/versions/9/" + MODULE_INFO, descriptor2) + .resource("META-INF/versions/9/p/Helper.class") + .resource("META-INF/versions/9/p/internal/Helper9.class") + .build(); + + // find the module + ModuleFinder finder = ModuleFinder.of(jar); + Optional omref = finder.find(name); + assertTrue((omref.isPresent())); + ModuleReference mref = omref.get(); + + // ensure that the right module-info.class is loaded + ModuleDescriptor descriptor = mref.descriptor(); + assertEquals(descriptor.name(), name); + if (MULTI_RELEASE) { + assertEquals(descriptor.requires(), descriptor2.requires()); + } else { + assertEquals(descriptor.requires(), descriptor1.requires()); + } + } + + /** + * Test multi-release JAR as an automatic module. + */ + public void testAutomaticModule() throws Exception { + String name = "m"; + + Path jar = new JarBuilder(name) + .resource("p/Main.class") + .resource("p/Helper.class") + .resource("META-INF/versions/9/p/Helper.class") + .resource("META-INF/versions/9/p/internal/Helper9.class") + .build(); + + // find the module + ModuleFinder finder = ModuleFinder.of(jar); + Optional omref = finder.find(name); + assertTrue((omref.isPresent())); + ModuleReference mref = omref.get(); + + // check module packages + ModuleDescriptor descriptor = mref.descriptor(); + Set packages = descriptor.packages(); + if (MULTI_RELEASE) { + assertTrue(packages.size() == 2); + assertTrue(packages.contains("p.internal")); + } else { + assertTrue(packages.size() == 1); + } + } + + /** + * Exercise ModuleReader on a multi-release JAR + */ + public void testModuleReader() throws Exception { + String name = "m1"; + + ModuleDescriptor descriptor1 = new ModuleDescriptor.Builder(name) + .requires("java.base") + .build(); + + // module descriptor for versioned section + ModuleDescriptor descriptor2 = new ModuleDescriptor.Builder(name) + .requires("java.base") + .requires("jdk.unsupported") + .build(); + + Path jar = new JarBuilder(name) + .moduleInfo(MODULE_INFO, descriptor1) + .moduleInfo("META-INF/versions/9/" + MODULE_INFO, descriptor2) + .build(); + + // find the module + ModuleFinder finder = ModuleFinder.of(jar); + Optional omref = finder.find(name); + assertTrue((omref.isPresent())); + ModuleReference mref = omref.get(); + + ModuleDescriptor expected; + if (MULTI_RELEASE) { + expected = descriptor2; + } else { + expected = descriptor1; + } + + // test ModuleReader by reading module-info.class resource + try (ModuleReader reader = mref.open()) { + + // open resource + Optional oin = reader.open(MODULE_INFO); + assertTrue(oin.isPresent()); + try (InputStream in = oin.get()) { + checkRequires(ModuleDescriptor.read(in), expected); + } + + // read resource + Optional obb = reader.read(MODULE_INFO); + assertTrue(obb.isPresent()); + ByteBuffer bb = obb.get(); + try { + checkRequires(ModuleDescriptor.read(bb), expected); + } finally { + reader.release(bb); + } + + // find resource + Optional ouri = reader.find(MODULE_INFO); + assertTrue(ouri.isPresent()); + URI uri = ouri.get(); + + String expectedTail = "!/"; + if (MULTI_RELEASE) + expectedTail += "META-INF/versions/" + RELEASE + "/"; + expectedTail += MODULE_INFO; + assertTrue(uri.toString().endsWith(expectedTail)); + + URLConnection uc = uri.toURL().openConnection(); + uc.setUseCaches(false); + try (InputStream in = uc.getInputStream()) { + checkRequires(ModuleDescriptor.read(in), expected); + } + + } + } + + /** + * Check that two ModuleDescriptor have the same requires + */ + static void checkRequires(ModuleDescriptor md1, ModuleDescriptor md2) { + assertEquals(md1.requires(), md2.requires()); + } + + /** + * A builder of multi-release JAR files. + */ + static class JarBuilder { + private String name; + private Set resources = new HashSet<>(); + private Map descriptors = new HashMap<>(); + + JarBuilder(String name) { + this.name = name; + } + + /** + * Adds a module-info.class to the JAR file. + */ + JarBuilder moduleInfo(String name, ModuleDescriptor descriptor) { + descriptors.put(name, descriptor); + return this; + } + + /** + * Adds a dummy resource to the JAR file. + */ + JarBuilder resource(String name) { + resources.add(name); + return this; + } + + /** + * Create the multi-release JAR, returning its file path. + */ + Path build() throws Exception { + Path dir = Files.createTempDirectory(Paths.get(""), "jar"); + List files = new ArrayList<>(); + + // write the module-info.class + for (Map.Entry e : descriptors.entrySet()) { + String name = e.getKey(); + ModuleDescriptor descriptor = e.getValue(); + Path mi = Paths.get(name.replace('/', File.separatorChar)); + Path parent = dir.resolve(mi).getParent(); + if (parent != null) + Files.createDirectories(parent); + try (OutputStream out = Files.newOutputStream(dir.resolve(mi))) { + ModuleInfoWriter.write(descriptor, out); + } + files.add(mi); + } + + // write the dummy resources + for (String name : resources) { + Path file = Paths.get(name.replace('/', File.separatorChar)); + // create dummy resource + Path parent = dir.resolve(file).getParent(); + if (parent != null) + Files.createDirectories(parent); + Files.createFile(dir.resolve(file)); + files.add(file); + } + + Manifest man = new Manifest(); + Attributes attrs = man.getMainAttributes(); + attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attrs.put(Attributes.Name.MULTI_RELEASE, "true"); + + Path jarfile = Paths.get(name + ".jar"); + JarUtils.createJarFile(jarfile, man, dir, files.toArray(new Path[0])); + return jarfile; + } + } +} diff --git a/jdk/test/java/util/ResourceBundle/modules/basic/src/asiabundles/jdk/test/resources/MyResources_ja_JP.properties b/jdk/test/java/util/ResourceBundle/modules/basic/src/asiabundles/jdk/test/resources/asia/MyResources_ja_JP.properties similarity index 100% rename from jdk/test/java/util/ResourceBundle/modules/basic/src/asiabundles/jdk/test/resources/MyResources_ja_JP.properties rename to jdk/test/java/util/ResourceBundle/modules/basic/src/asiabundles/jdk/test/resources/asia/MyResources_ja_JP.properties diff --git a/jdk/test/java/util/ResourceBundle/modules/basic/src/mainbundles/jdk/test/resources/MyResourcesProvider.java b/jdk/test/java/util/ResourceBundle/modules/basic/src/mainbundles/jdk/test/resources/MyResourcesProvider.java index c9ebc4b837d..3cbbe563368 100644 --- a/jdk/test/java/util/ResourceBundle/modules/basic/src/mainbundles/jdk/test/resources/MyResourcesProvider.java +++ b/jdk/test/java/util/ResourceBundle/modules/basic/src/mainbundles/jdk/test/resources/MyResourcesProvider.java @@ -62,9 +62,7 @@ public class MyResourcesProvider extends AbstractResourceBundleProvider { @Override protected String toBundleName(String baseName, Locale locale) { - // The resource bundle for Locale.JAPAN is loccated at jdk.test.resources - // in module "asiabundles". - String name = locale.equals(Locale.JAPAN) ? baseName : addRegion(baseName); + String name = addRegion(baseName); return Control.getControl(Control.FORMAT_DEFAULT).toBundleName(name, locale); } diff --git a/jdk/test/tools/jmod/hashes/HashesTest.java b/jdk/test/tools/jmod/hashes/HashesTest.java index e01ecdb8046..22752e759f1 100644 --- a/jdk/test/tools/jmod/hashes/HashesTest.java +++ b/jdk/test/tools/jmod/hashes/HashesTest.java @@ -26,7 +26,8 @@ * @summary Test the recording and checking of module hashes * @author Andrei Eremeev * @library /lib/testlibrary - * @modules java.base/jdk.internal.module + * @modules java.base/jdk.internal.misc + * java.base/jdk.internal.module * jdk.jlink * jdk.compiler * @build CompilerUtils @@ -39,7 +40,6 @@ import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReader; import java.lang.module.ModuleReference; -import java.lang.reflect.Method; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -55,8 +55,10 @@ import java.util.Set; import java.util.spi.ToolProvider; import java.util.stream.Collectors; -import jdk.internal.module.ConfigurableModuleFinder; +import jdk.internal.misc.SharedSecrets; +import jdk.internal.misc.JavaLangModuleAccess; import jdk.internal.module.ModuleHashes; + import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; @@ -74,7 +76,6 @@ public class HashesTest { private final Path jmods = Paths.get("jmods"); private final String[] modules = new String[] { "m1", "m2", "m3"}; - private static Method hashesMethod; @BeforeTest private void setup() throws Exception { if (Files.exists(jmods)) { @@ -97,13 +98,6 @@ public class HashesTest { // compile org.bar and org.foo compileModule("org.bar", modSrc); compileModule("org.foo", modSrc); - - try { - hashesMethod = ModuleDescriptor.class.getDeclaredMethod("hashes"); - hashesMethod.setAccessible(true); - } catch (ReflectiveOperationException x) { - throw new InternalError(x); - } } @Test @@ -143,17 +137,14 @@ public class HashesTest { } private Optional hashes(String name) throws Exception { - ModuleFinder finder = ModuleFinder.of(jmods.resolve(name + ".jmod")); - if (finder instanceof ConfigurableModuleFinder) { - ((ConfigurableModuleFinder) finder) - .configurePhase(ConfigurableModuleFinder.Phase.LINK_TIME); - } + ModuleFinder finder = SharedSecrets.getJavaLangModuleAccess() + .newModulePath(Runtime.version(), true, jmods.resolve(name + ".jmod")); ModuleReference mref = finder.find(name).orElseThrow(RuntimeException::new); ModuleReader reader = mref.open(); try (InputStream in = reader.open("module-info.class").get()) { ModuleDescriptor md = ModuleDescriptor.read(in); - Optional hashes = - (Optional) hashesMethod.invoke(md); + JavaLangModuleAccess jmla = SharedSecrets.getJavaLangModuleAccess(); + Optional hashes = jmla.hashes(md); System.out.format("hashes in module %s %s%n", name, hashes.isPresent() ? "present" : "absent"); if (hashes.isPresent()) {