Path: blob/master/src/java.base/share/classes/jdk/internal/module/ModulePath.java
41159 views
/*1* Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package jdk.internal.module;2627import java.io.BufferedInputStream;28import java.io.BufferedReader;29import java.io.File;30import java.io.IOException;31import java.io.InputStream;32import java.io.InputStreamReader;33import java.io.UncheckedIOException;34import java.lang.module.FindException;35import java.lang.module.InvalidModuleDescriptorException;36import java.lang.module.ModuleDescriptor;37import java.lang.module.ModuleDescriptor.Builder;38import java.lang.module.ModuleFinder;39import java.lang.module.ModuleReference;40import java.net.URI;41import java.nio.file.DirectoryStream;42import java.nio.file.Files;43import java.nio.file.NoSuchFileException;44import java.nio.file.Path;45import java.nio.file.attribute.BasicFileAttributes;46import java.util.ArrayList;47import java.util.HashMap;48import java.util.List;49import java.util.Map;50import java.util.Objects;51import java.util.Optional;52import java.util.Set;53import java.util.jar.Attributes;54import java.util.jar.JarEntry;55import java.util.jar.JarFile;56import java.util.jar.Manifest;57import java.util.regex.Matcher;58import java.util.regex.Pattern;59import java.util.stream.Collectors;60import java.util.zip.ZipException;61import java.util.zip.ZipFile;6263import sun.nio.cs.UTF_8;6465import jdk.internal.jmod.JmodFile;66import jdk.internal.jmod.JmodFile.Section;67import jdk.internal.perf.PerfCounter;6869/**70* A {@code ModuleFinder} that locates modules on the file system by searching71* a sequence of directories or packaged modules. The ModuleFinder can be72* created to work in either the run-time or link-time phases. In both cases it73* locates modular JAR and exploded modules. When created for link-time then it74* additionally locates modules in JMOD files. The ModuleFinder can also75* optionally patch any modules that it locates with a ModulePatcher.76*/7778public class ModulePath implements ModuleFinder {79private static final String MODULE_INFO = "module-info.class";8081// the version to use for multi-release modular JARs82private final Runtime.Version releaseVersion;8384// true for the link phase (supports modules packaged in JMOD format)85private final boolean isLinkPhase;8687// for patching modules, can be null88private final ModulePatcher patcher;8990// the entries on this module path91private final Path[] entries;92private int next;9394// map of module name to module reference map for modules already located95private final Map<String, ModuleReference> cachedModules = new HashMap<>();969798private ModulePath(Runtime.Version version,99boolean isLinkPhase,100ModulePatcher patcher,101Path... entries) {102this.releaseVersion = version;103this.isLinkPhase = isLinkPhase;104this.patcher = patcher;105this.entries = entries.clone();106for (Path entry : this.entries) {107Objects.requireNonNull(entry);108}109}110111/**112* Returns a ModuleFinder that locates modules on the file system by113* searching a sequence of directories and/or packaged modules. The modules114* may be patched by the given ModulePatcher.115*/116public static ModuleFinder of(ModulePatcher patcher, Path... entries) {117return new ModulePath(JarFile.runtimeVersion(), false, patcher, entries);118}119120/**121* Returns a ModuleFinder that locates modules on the file system by122* searching a sequence of directories and/or packaged modules.123*/124public static ModuleFinder of(Path... entries) {125return of((ModulePatcher)null, entries);126}127128/**129* Returns a ModuleFinder that locates modules on the file system by130* searching a sequence of directories and/or packaged modules.131*132* @param version The release version to use for multi-release JAR files133* @param isLinkPhase {@code true} if the link phase to locate JMOD files134*/135public static ModuleFinder of(Runtime.Version version,136boolean isLinkPhase,137Path... entries) {138return new ModulePath(version, isLinkPhase, null, entries);139}140141142@Override143public Optional<ModuleReference> find(String name) {144Objects.requireNonNull(name);145146// try cached modules147ModuleReference m = cachedModules.get(name);148if (m != null)149return Optional.of(m);150151// the module may not have been encountered yet152while (hasNextEntry()) {153scanNextEntry();154m = cachedModules.get(name);155if (m != null)156return Optional.of(m);157}158return Optional.empty();159}160161@Override162public Set<ModuleReference> findAll() {163// need to ensure that all entries have been scanned164while (hasNextEntry()) {165scanNextEntry();166}167return cachedModules.values().stream().collect(Collectors.toSet());168}169170/**171* Returns {@code true} if there are additional entries to scan172*/173private boolean hasNextEntry() {174return next < entries.length;175}176177/**178* Scans the next entry on the module path. A no-op if all entries have179* already been scanned.180*181* @throws FindException if an error occurs scanning the next entry182*/183private void scanNextEntry() {184if (hasNextEntry()) {185186long t0 = System.nanoTime();187188Path entry = entries[next];189Map<String, ModuleReference> modules = scan(entry);190next++;191192// update cache, ignoring duplicates193int initialSize = cachedModules.size();194for (Map.Entry<String, ModuleReference> e : modules.entrySet()) {195cachedModules.putIfAbsent(e.getKey(), e.getValue());196}197198// update counters199int added = cachedModules.size() - initialSize;200moduleCount.add(added);201202scanTime.addElapsedTimeFrom(t0);203}204}205206207/**208* Scan the given module path entry. If the entry is a directory then it is209* a directory of modules or an exploded module. If the entry is a regular210* file then it is assumed to be a packaged module.211*212* @throws FindException if an error occurs scanning the entry213*/214private Map<String, ModuleReference> scan(Path entry) {215216BasicFileAttributes attrs;217try {218attrs = Files.readAttributes(entry, BasicFileAttributes.class);219} catch (NoSuchFileException e) {220return Map.of();221} catch (IOException ioe) {222throw new FindException(ioe);223}224225try {226227if (attrs.isDirectory()) {228Path mi = entry.resolve(MODULE_INFO);229if (!Files.exists(mi)) {230// assume a directory of modules231return scanDirectory(entry);232}233}234235// packaged or exploded module236ModuleReference mref = readModule(entry, attrs);237if (mref != null) {238String name = mref.descriptor().name();239return Map.of(name, mref);240}241242// not recognized243String msg;244if (!isLinkPhase && entry.toString().endsWith(".jmod")) {245msg = "JMOD format not supported at execution time";246} else {247msg = "Module format not recognized";248}249throw new FindException(msg + ": " + entry);250251} catch (IOException ioe) {252throw new FindException(ioe);253}254}255256257/**258* Scans the given directory for packaged or exploded modules.259*260* @return a map of module name to ModuleReference for the modules found261* in the directory262*263* @throws IOException if an I/O error occurs264* @throws FindException if an error occurs scanning the entry or the265* directory contains two or more modules with the same name266*/267private Map<String, ModuleReference> scanDirectory(Path dir)268throws IOException269{270// The map of name -> mref of modules found in this directory.271Map<String, ModuleReference> nameToReference = new HashMap<>();272273try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {274for (Path entry : stream) {275BasicFileAttributes attrs;276try {277attrs = Files.readAttributes(entry, BasicFileAttributes.class);278} catch (NoSuchFileException ignore) {279// file has been removed or moved, ignore for now280continue;281}282283ModuleReference mref = readModule(entry, attrs);284285// module found286if (mref != null) {287// can have at most one version of a module in the directory288String name = mref.descriptor().name();289ModuleReference previous = nameToReference.put(name, mref);290if (previous != null) {291String fn1 = fileName(mref);292String fn2 = fileName(previous);293throw new FindException("Two versions of module "294+ name + " found in " + dir295+ " (" + fn1 + " and " + fn2 + ")");296}297}298}299}300301return nameToReference;302}303304305/**306* Reads a packaged or exploded module, returning a {@code ModuleReference}307* to the module. Returns {@code null} if the entry is not recognized.308*309* @throws IOException if an I/O error occurs310* @throws FindException if an error occurs parsing its module descriptor311*/312private ModuleReference readModule(Path entry, BasicFileAttributes attrs)313throws IOException314{315try {316317// exploded module318if (attrs.isDirectory()) {319return readExplodedModule(entry); // may return null320}321322// JAR or JMOD file323if (attrs.isRegularFile()) {324String fn = entry.getFileName().toString();325boolean isDefaultFileSystem = isDefaultFileSystem(entry);326327// JAR file328if (fn.endsWith(".jar")) {329if (isDefaultFileSystem) {330return readJar(entry);331} else {332// the JAR file is in a custom file system so333// need to copy it to the local file system334Path tmpdir = Files.createTempDirectory("mlib");335Path target = Files.copy(entry, tmpdir.resolve(fn));336return readJar(target);337}338}339340// JMOD file341if (isDefaultFileSystem && isLinkPhase && fn.endsWith(".jmod")) {342return readJMod(entry);343}344}345346return null;347348} catch (InvalidModuleDescriptorException e) {349throw new FindException("Error reading module: " + entry, e);350}351}352353/**354* Returns a string with the file name of the module if possible.355* If the module location is not a file URI then return the URI356* as a string.357*/358private String fileName(ModuleReference mref) {359URI uri = mref.location().orElse(null);360if (uri != null) {361if (uri.getScheme().equalsIgnoreCase("file")) {362Path file = Path.of(uri);363return file.getFileName().toString();364} else {365return uri.toString();366}367} else {368return "<unknown>";369}370}371372// -- JMOD files --373374private Set<String> jmodPackages(JmodFile jf) {375return jf.stream()376.filter(e -> e.section() == Section.CLASSES)377.map(JmodFile.Entry::name)378.map(this::toPackageName)379.flatMap(Optional::stream)380.collect(Collectors.toSet());381}382383/**384* Returns a {@code ModuleReference} to a module in JMOD file on the385* file system.386*387* @throws IOException388* @throws InvalidModuleDescriptorException389*/390private ModuleReference readJMod(Path file) throws IOException {391try (JmodFile jf = new JmodFile(file)) {392ModuleInfo.Attributes attrs;393try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) {394attrs = ModuleInfo.read(in, () -> jmodPackages(jf));395}396return ModuleReferences.newJModModule(attrs, file);397}398}399400401// -- JAR files --402403private static final String SERVICES_PREFIX = "META-INF/services/";404405private static final Attributes.Name AUTOMATIC_MODULE_NAME406= new Attributes.Name("Automatic-Module-Name");407408/**409* Returns the service type corresponding to the name of a services410* configuration file if it is a legal type name.411*412* For example, if called with "META-INF/services/p.S" then this method413* returns a container with the value "p.S".414*/415private Optional<String> toServiceName(String cf) {416assert cf.startsWith(SERVICES_PREFIX);417int index = cf.lastIndexOf("/") + 1;418if (index < cf.length()) {419String prefix = cf.substring(0, index);420if (prefix.equals(SERVICES_PREFIX)) {421String sn = cf.substring(index);422if (Checks.isClassName(sn))423return Optional.of(sn);424}425}426return Optional.empty();427}428429/**430* Reads the next line from the given reader and trims it of comments and431* leading/trailing white space.432*433* Returns null if the reader is at EOF.434*/435private String nextLine(BufferedReader reader) throws IOException {436String ln = reader.readLine();437if (ln != null) {438int ci = ln.indexOf('#');439if (ci >= 0)440ln = ln.substring(0, ci);441ln = ln.trim();442}443return ln;444}445446/**447* Treat the given JAR file as a module as follows:448*449* 1. The value of the Automatic-Module-Name attribute is the module name450* 2. The version, and the module name when the Automatic-Module-Name451* attribute is not present, is derived from the file ame of the JAR file452* 3. All packages are derived from the .class files in the JAR file453* 4. The contents of any META-INF/services configuration files are mapped454* to "provides" declarations455* 5. The Main-Class attribute in the main attributes of the JAR manifest456* is mapped to the module descriptor mainClass if possible457*/458private ModuleDescriptor deriveModuleDescriptor(JarFile jf)459throws IOException460{461// Read Automatic-Module-Name attribute if present462Manifest man = jf.getManifest();463Attributes attrs = null;464String moduleName = null;465if (man != null) {466attrs = man.getMainAttributes();467if (attrs != null) {468moduleName = attrs.getValue(AUTOMATIC_MODULE_NAME);469}470}471472// Derive the version, and the module name if needed, from JAR file name473String fn = jf.getName();474int i = fn.lastIndexOf(File.separator);475if (i != -1)476fn = fn.substring(i + 1);477478// drop ".jar"479String name = fn.substring(0, fn.length() - 4);480String vs = null;481482// find first occurrence of -${NUMBER}. or -${NUMBER}$483Matcher matcher = Patterns.DASH_VERSION.matcher(name);484if (matcher.find()) {485int start = matcher.start();486487// attempt to parse the tail as a version string488try {489String tail = name.substring(start + 1);490ModuleDescriptor.Version.parse(tail);491vs = tail;492} catch (IllegalArgumentException ignore) { }493494name = name.substring(0, start);495}496497// Create builder, using the name derived from file name when498// Automatic-Module-Name not present499Builder builder;500if (moduleName != null) {501try {502builder = ModuleDescriptor.newAutomaticModule(moduleName);503} catch (IllegalArgumentException e) {504throw new FindException(AUTOMATIC_MODULE_NAME + ": " + e.getMessage());505}506} else {507builder = ModuleDescriptor.newAutomaticModule(cleanModuleName(name));508}509510// module version if present511if (vs != null)512builder.version(vs);513514// scan the names of the entries in the JAR file515Map<Boolean, Set<String>> map = jf.versionedStream()516.filter(e -> !e.isDirectory())517.map(JarEntry::getName)518.filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX)))519.collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX),520Collectors.toSet()));521522Set<String> classFiles = map.get(Boolean.FALSE);523Set<String> configFiles = map.get(Boolean.TRUE);524525// the packages containing class files526Set<String> packages = classFiles.stream()527.map(this::toPackageName)528.flatMap(Optional::stream)529.distinct()530.collect(Collectors.toSet());531532// all packages are exported and open533builder.packages(packages);534535// map names of service configuration files to service names536Set<String> serviceNames = configFiles.stream()537.map(this::toServiceName)538.flatMap(Optional::stream)539.collect(Collectors.toSet());540541// parse each service configuration file542for (String sn : serviceNames) {543JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);544List<String> providerClasses = new ArrayList<>();545try (InputStream in = jf.getInputStream(entry)) {546BufferedReader reader547= new BufferedReader(new InputStreamReader(in, UTF_8.INSTANCE));548String cn;549while ((cn = nextLine(reader)) != null) {550if (!cn.isEmpty()) {551String pn = packageName(cn);552if (!packages.contains(pn)) {553String msg = "Provider class " + cn + " not in module";554throw new InvalidModuleDescriptorException(msg);555}556providerClasses.add(cn);557}558}559}560if (!providerClasses.isEmpty())561builder.provides(sn, providerClasses);562}563564// Main-Class attribute if it exists565if (attrs != null) {566String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);567if (mainClass != null) {568mainClass = mainClass.replace('/', '.');569if (Checks.isClassName(mainClass)) {570String pn = packageName(mainClass);571if (packages.contains(pn)) {572builder.mainClass(mainClass);573}574}575}576}577578return builder.build();579}580581/**582* Patterns used to derive the module name from a JAR file name.583*/584private static class Patterns {585static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");586static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");587static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");588static final Pattern LEADING_DOTS = Pattern.compile("^\\.");589static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");590}591592/**593* Clean up candidate module name derived from a JAR file name.594*/595private static String cleanModuleName(String mn) {596// replace non-alphanumeric597mn = Patterns.NON_ALPHANUM.matcher(mn).replaceAll(".");598599// collapse repeating dots600mn = Patterns.REPEATING_DOTS.matcher(mn).replaceAll(".");601602// drop leading dots603if (!mn.isEmpty() && mn.charAt(0) == '.')604mn = Patterns.LEADING_DOTS.matcher(mn).replaceAll("");605606// drop trailing dots607int len = mn.length();608if (len > 0 && mn.charAt(len-1) == '.')609mn = Patterns.TRAILING_DOTS.matcher(mn).replaceAll("");610611return mn;612}613614private Set<String> jarPackages(JarFile jf) {615return jf.versionedStream()616.filter(e -> !e.isDirectory())617.map(JarEntry::getName)618.map(this::toPackageName)619.flatMap(Optional::stream)620.collect(Collectors.toSet());621}622623/**624* Returns a {@code ModuleReference} to a module in modular JAR file on625* the file system.626*627* @throws IOException628* @throws FindException629* @throws InvalidModuleDescriptorException630*/631private ModuleReference readJar(Path file) throws IOException {632try (JarFile jf = new JarFile(file.toFile(),633true, // verify634ZipFile.OPEN_READ,635releaseVersion))636{637ModuleInfo.Attributes attrs;638JarEntry entry = jf.getJarEntry(MODULE_INFO);639if (entry == null) {640641// no module-info.class so treat it as automatic module642try {643ModuleDescriptor md = deriveModuleDescriptor(jf);644attrs = new ModuleInfo.Attributes(md, null, null, null);645} catch (RuntimeException e) {646throw new FindException("Unable to derive module descriptor for "647+ jf.getName(), e);648}649650} else {651attrs = ModuleInfo.read(jf.getInputStream(entry),652() -> jarPackages(jf));653}654655return ModuleReferences.newJarModule(attrs, patcher, file);656} catch (ZipException e) {657throw new FindException("Error reading " + file, e);658}659}660661662// -- exploded directories --663664private Set<String> explodedPackages(Path dir) {665try {666return Files.find(dir, Integer.MAX_VALUE,667((path, attrs) -> attrs.isRegularFile() && !isHidden(path)))668.map(path -> dir.relativize(path))669.map(this::toPackageName)670.flatMap(Optional::stream)671.collect(Collectors.toSet());672} catch (IOException x) {673throw new UncheckedIOException(x);674}675}676677/**678* Returns a {@code ModuleReference} to an exploded module on the file679* system or {@code null} if {@code module-info.class} not found.680*681* @throws IOException682* @throws InvalidModuleDescriptorException683*/684private ModuleReference readExplodedModule(Path dir) throws IOException {685Path mi = dir.resolve(MODULE_INFO);686ModuleInfo.Attributes attrs;687try (InputStream in = Files.newInputStream(mi)) {688attrs = ModuleInfo.read(new BufferedInputStream(in),689() -> explodedPackages(dir));690} catch (NoSuchFileException e) {691// for now692return null;693}694return ModuleReferences.newExplodedModule(attrs, patcher, dir);695}696697/**698* Maps a type name to its package name.699*/700private static String packageName(String cn) {701int index = cn.lastIndexOf('.');702return (index == -1) ? "" : cn.substring(0, index);703}704705/**706* Maps the name of an entry in a JAR or ZIP file to a package name.707*708* @throws InvalidModuleDescriptorException if the name is a class file in709* the top-level directory of the JAR/ZIP file (and it's not710* module-info.class)711*/712private Optional<String> toPackageName(String name) {713assert !name.endsWith("/");714int index = name.lastIndexOf("/");715if (index == -1) {716if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {717String msg = name + " found in top-level directory"718+ " (unnamed package not allowed in module)";719throw new InvalidModuleDescriptorException(msg);720}721return Optional.empty();722}723724String pn = name.substring(0, index).replace('/', '.');725if (Checks.isPackageName(pn)) {726return Optional.of(pn);727} else {728// not a valid package name729return Optional.empty();730}731}732733/**734* Maps the relative path of an entry in an exploded module to a package735* name.736*737* @throws InvalidModuleDescriptorException if the name is a class file in738* the top-level directory (and it's not module-info.class)739*/740private Optional<String> toPackageName(Path file) {741assert file.getRoot() == null;742743Path parent = file.getParent();744if (parent == null) {745String name = file.toString();746if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {747String msg = name + " found in top-level directory"748+ " (unnamed package not allowed in module)";749throw new InvalidModuleDescriptorException(msg);750}751return Optional.empty();752}753754String pn = parent.toString().replace(File.separatorChar, '.');755if (Checks.isPackageName(pn)) {756return Optional.of(pn);757} else {758// not a valid package name759return Optional.empty();760}761}762763/**764* Returns true if the given file exists and is a hidden file765*/766private boolean isHidden(Path file) {767try {768return Files.isHidden(file);769} catch (IOException ioe) {770return false;771}772}773774775/**776* Return true if a path locates a path in the default file system777*/778private boolean isDefaultFileSystem(Path path) {779return path.getFileSystem().provider()780.getScheme().equalsIgnoreCase("file");781}782783784private static final PerfCounter scanTime785= PerfCounter.newPerfCounter("jdk.module.finder.modulepath.scanTime");786private static final PerfCounter moduleCount787= PerfCounter.newPerfCounter("jdk.module.finder.modulepath.modules");788}789790791