org.eclipse.jdt.internal.compiler.util.JRTUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ecj Show documentation
Show all versions of ecj Show documentation
Eclipse Compiler for Java(TM)
/*******************************************************************************
* Copyright (c) 2015, 2020 IBM Corporation.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.compiler.util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.ClosedByInterruptException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.IModule;
public class JRTUtil {
public static final boolean DISABLE_CACHE = Boolean.getBoolean("org.eclipse.jdt.disable_JRT_cache"); //$NON-NLS-1$
public static final String JAVA_BASE = "java.base".intern(); //$NON-NLS-1$
public static final char[] JAVA_BASE_CHAR = JAVA_BASE.toCharArray();
static final String MODULES_SUBDIR = "/modules"; //$NON-NLS-1$
static final String[] DEFAULT_MODULE = new String[]{JAVA_BASE};
static final String[] NO_MODULE = new String[0];
static final String MULTIPLE = "MU"; //$NON-NLS-1$
static final String DEFAULT_PACKAGE = ""; //$NON-NLS-1$
static String MODULE_TO_LOAD;
public static final String JRT_FS_JAR = "jrt-fs.jar"; //$NON-NLS-1$
static URI JRT_URI = URI.create("jrt:/"); //$NON-NLS-1$
public static final int NOTIFY_FILES = 0x0001;
public static final int NOTIFY_PACKAGES = 0x0002;
public static final int NOTIFY_MODULES = 0x0004;
public static final int NOTIFY_ALL = NOTIFY_FILES | NOTIFY_PACKAGES | NOTIFY_MODULES;
// TODO: Java 9 Think about clearing the cache too.
private static Map> images = new ConcurrentHashMap<>();
/**
* Map from JDK home path to ct.sym file (located in /lib in the JDK)
*/
private static final Map ctSymFiles = new ConcurrentHashMap<>();
public interface JrtFileVisitor {
public FileVisitResult visitPackage(T dir, T mod, BasicFileAttributes attrs) throws IOException;
public FileVisitResult visitFile(T file, T mod, BasicFileAttributes attrs) throws IOException;
/**
* Invoked when a root directory of a module being visited. The element returned
* contains only the module name segment - e.g. "java.base". Clients can use this to control
* how the JRT needs to be processed, for e.g., clients can skip a particular module
* by returning FileVisitResult.SKIP_SUBTREE
*/
public FileVisitResult visitModule(T path, String name) throws IOException;
}
static abstract class AbstractFileVisitor implements FileVisitor {
@Override
public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(T file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
}
public static JrtFileSystem getJrtSystem(File image) {
return getJrtSystem(image, null);
}
public static JrtFileSystem getJrtSystem(File image, String release) {
String key = image.toString();
if (release != null) key = key + "|" + release; //$NON-NLS-1$
Optional system = images.computeIfAbsent(key, x -> {
try {
return Optional.ofNullable(JrtFileSystem.getNewJrtFileSystem(image, release));
} catch (IOException e) {
// Needs better error handling downstream? But for now, make sure
// a dummy JrtFileSystem is not created.
e.printStackTrace();
return Optional.empty();
}
});
return system.orElse(null);
}
public static CtSym getCtSym(Path jdkHome) throws IOException {
CtSym ctSym;
try {
ctSym = ctSymFiles.compute(jdkHome, (Path x, CtSym current) -> {
if (current == null || !current.getFs().isOpen()) {
try {
return new CtSym(x);
} catch (IOException e) {
throw new RuntimeIOException(e);
}
}
return current;
});
} catch (RuntimeIOException rio) {
throw rio.getCause();
}
return ctSym;
}
/** TEST ONLY (use when changing the "modules.to.load" property). */
public static void reset() {
images.clear();
MODULE_TO_LOAD = System.getProperty("modules.to.load"); //$NON-NLS-1$
}
/**
* Given the path of a modular image file, this method walks the archive content and
* notifies the supplied visitor about packages and files visited.
*
* The file system contains the following top level directories:
* /modules/$MODULE/$PATH
* /packages/$PACKAGE/$MODULE
* The latter provides quick look up of the module that contains a particular package. However,
* this method only notifies its clients of the entries within the modules (former) sub-directory.
* Clients can decide which notifications they want to receive. See {@link JRTUtil#NOTIFY_ALL},
* {@link JRTUtil#NOTIFY_FILES}, {@link JRTUtil#NOTIFY_PACKAGES} and {@link JRTUtil#NOTIFY_MODULES}.
*
* @param image a java.io.File handle to the JRT image.
* @param visitor an instance of JrtFileVisitor to be notified of the entries in the JRT image.
* @param notify flag indicating the notifications the client is interested in.
* @throws IOException
*/
public static void walkModuleImage(File image, final JRTUtil.JrtFileVisitor visitor, int notify) throws IOException {
getJrtSystem(image, null).walkModuleImage(visitor, notify);
}
public static void walkModuleImage(File image, String release, final JRTUtil.JrtFileVisitor visitor, int notify) throws IOException {
getJrtSystem(image, release).walkModuleImage(visitor, notify);
}
public static InputStream getContentFromJrt(File jrt, String fileName, String module) throws IOException {
return getJrtSystem(jrt).getContentFromJrt(fileName, module);
}
public static byte[] getClassfileContent(File jrt, String fileName, String module) throws IOException {
return getJrtSystem(jrt).getClassfileContent(fileName, module);
}
public static ClassFileReader getClassfile(File jrt, String fileName, IModule module) throws IOException, ClassFormatException {
return getJrtSystem(jrt).getClassfile(fileName, module);
}
public static ClassFileReader getClassfile(File jrt, String fileName, String module, Predicate moduleNameFilter) throws IOException, ClassFormatException {
return getJrtSystem(jrt).getClassfile(fileName, module, moduleNameFilter);
}
public static List getModulesDeclaringPackage(File jrt, String qName, String moduleName) {
return getJrtSystem(jrt).getModulesDeclaringPackage(qName, moduleName);
}
public static boolean hasCompilationUnit(File jrt, String qualifiedPackageName, String moduleName) {
return getJrtSystem(jrt).hasClassFile(qualifiedPackageName, moduleName);
}
/*
* Returns only the file name after removing trailing '/' if any for folders
*/
public static String sanitizedFileName(Path path) {
String p = path.getFileName().toString();
if (p.length() > 1 && p.charAt(p.length() - 1) == '/') {
return p.substring(0, p.length() - 1);
}
return p;
}
/**
* Tries to read all bytes of the file denoted by path,
* returns null if the file could not be found or if the read was interrupted.
* @param path
* @return bytes or null
* @throws IOException any IO exception other than NoSuchFileException
*/
public static byte[] safeReadBytes(Path path) throws IOException {
try {
return Files.readAllBytes(path);
} catch (ClosedByInterruptException | NoSuchFileException e) {
return null;
}
}
}
class JrtFileSystemWithOlderRelease extends JrtFileSystem {
final String release;
private List releaseRoots = Collections.emptyList();
protected Path modulePath;
private CtSym ctSym;
/**
* The jrt file system is based on the location of the JRE home whose libraries
* need to be loaded.
*
* @param jrt the path to the root of the JRE whose libraries we are interested in.
* @param release the older release where classes and modules should be searched for.
* @throws IOException
*/
JrtFileSystemWithOlderRelease(File jrt, String release) throws IOException {
super(jrt);
this.release = release;
initialize(jrt, release);
}
@Override
void initialize(File jdk) throws IOException {
// Just to make sure we don't do anything in super.initialize()
// before setting this.release
}
private void initialize(File jdk, String rel) throws IOException {
super.initialize(jdk);
this.fs = null;// reset and proceed, TODO: this is crude and need to be removed.
String releaseCode = CtSym.getReleaseCode(this.release);
this.ctSym = JRTUtil.getCtSym(Paths.get(this.jdkHome));
this.fs = this.ctSym.getFs();
if (!Files.exists(this.fs.getPath(releaseCode))
|| Files.exists(this.fs.getPath(releaseCode, "system-modules"))) { //$NON-NLS-1$
this.fs = null;
}
this.releaseRoots = this.ctSym.releaseRoots(releaseCode);
}
@Override
void walkModuleImage(final JRTUtil.JrtFileVisitor visitor, final int notify) throws IOException {
for (Path p : this.releaseRoots) {
Files.walkFileTree(p, new JRTUtil.AbstractFileVisitor() {
@Override
public FileVisitResult preVisitDirectory(java.nio.file.Path dir, BasicFileAttributes attrs)
throws IOException {
int count = dir.getNameCount();
if (count == 1) {
return FileVisitResult.CONTINUE;
}
if (count == 2) {
// e.g. /9A/java.base
java.nio.file.Path mod = dir.getName(1);
if ((JRTUtil.MODULE_TO_LOAD != null && JRTUtil.MODULE_TO_LOAD.length() > 0
&& JRTUtil.MODULE_TO_LOAD.indexOf(mod.toString()) == -1)) {
return FileVisitResult.SKIP_SUBTREE;
}
return ((notify & JRTUtil.NOTIFY_MODULES) == 0) ? FileVisitResult.CONTINUE
: visitor.visitModule(dir, JRTUtil.sanitizedFileName(mod));
}
if ((notify & JRTUtil.NOTIFY_PACKAGES) == 0) {
// client is not interested in packages
return FileVisitResult.CONTINUE;
}
return visitor.visitPackage(dir.subpath(2, count), dir.getName(1), attrs);
}
@Override
public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs)
throws IOException {
if ((notify & JRTUtil.NOTIFY_FILES) == 0) {
return FileVisitResult.CONTINUE;
}
// This happens when a file in a default package is present. E.g. /modules/some.module/file.name
if (file.getNameCount() == 3) {
cachePackage(JRTUtil.DEFAULT_PACKAGE, file.getName(1).toString());
}
return visitor.visitFile(file.subpath(2, file.getNameCount()), file.getName(1), attrs);
}
});
}
}
}
final class RuntimeIOException extends RuntimeException {
private static final long serialVersionUID = 1L;
public RuntimeIOException(IOException cause) {
super(cause);
}
@Override
public synchronized IOException getCause() {
return (IOException) super.getCause();
}
}
class JrtFileSystem {
private final Map packageToModule = new HashMap();
private final Map> packageToModules = new HashMap>();
private final Map> classCache = new ConcurrentHashMap<>(10007);
FileSystem fs;
Path modRoot;
String jdkHome;
public static JrtFileSystem getNewJrtFileSystem(File jrt, String release) throws IOException {
return (release == null) ? new JrtFileSystem(jrt) :
new JrtFileSystemWithOlderRelease(jrt, release);
}
/**
* The jrt file system is based on the location of the JRE home whose libraries
* need to be loaded.
*
* @param jrt the path to the root of the JRE whose libraries we are interested in.
* @throws IOException
*/
JrtFileSystem(File jrt) throws IOException {
initialize(jrt);
}
void initialize(File jrt) throws IOException {
URL jrtPath = null;
this.jdkHome = null;
if (jrt.toString().endsWith(JRTUtil.JRT_FS_JAR)) {
jrtPath = jrt.toPath().toUri().toURL();
this.jdkHome = jrt.getParentFile().getParent();
} else {
this.jdkHome = jrt.toPath().toString();
jrtPath = Paths.get(this.jdkHome, "lib", JRTUtil.JRT_FS_JAR).toUri().toURL(); //$NON-NLS-1$
}
JRTUtil.MODULE_TO_LOAD = System.getProperty("modules.to.load"); //$NON-NLS-1$
String javaVersion = System.getProperty("java.version"); //$NON-NLS-1$
if (javaVersion != null && javaVersion.startsWith("1.8")) { //$NON-NLS-1$
URLClassLoader loader = new URLClassLoader(new URL[] { jrtPath });
HashMap env = new HashMap<>();
this.fs = FileSystems.newFileSystem(JRTUtil.JRT_URI, env, loader);
} else {
HashMap env = new HashMap<>();
env.put("java.home", this.jdkHome); //$NON-NLS-1$
this.fs = FileSystems.newFileSystem(JRTUtil.JRT_URI, env);
}
this.modRoot = this.fs.getPath(JRTUtil.MODULES_SUBDIR);
// Set up the root directory wherere modules are located
walkJrtForModules();
}
public List getModulesDeclaringPackage(String qualifiedPackageName, String moduleName) {
qualifiedPackageName = qualifiedPackageName.replace('.', '/');
String module = this.packageToModule.get(qualifiedPackageName);
if (moduleName == null) {
// wildcard search:
if (module == null)
return null;
if (module == JRTUtil.MULTIPLE)
return this.packageToModules.get(qualifiedPackageName);
return Collections.singletonList(module);
}
if (module != null) {
// specific search:
if (module == JRTUtil.MULTIPLE) {
List list = this.packageToModules.get(qualifiedPackageName);
if (list.contains(moduleName))
return Collections.singletonList(moduleName);
} else {
if (module.equals(moduleName))
return Collections.singletonList(moduleName);
}
}
return null;
}
public String[] getModules(String fileName) {
int idx = fileName.lastIndexOf('/');
String pack = null;
if (idx != -1) {
pack = fileName.substring(0, idx);
} else {
pack = JRTUtil.DEFAULT_PACKAGE;
}
String module = this.packageToModule.get(pack);
if (module != null) {
if (module == JRTUtil.MULTIPLE) {
List list = this.packageToModules.get(pack);
return list.toArray(new String[0]);
} else {
return new String[]{module};
}
}
return JRTUtil.DEFAULT_MODULE;
}
public boolean hasClassFile(String qualifiedPackageName, String module) {
if (module == null)
return false;
// easy checks first:
String knownModule = this.packageToModule.get(qualifiedPackageName);
if (knownModule == null || (knownModule != JRTUtil.MULTIPLE && !knownModule.equals(module)))
return false;
Path packagePath = this.fs.getPath(JRTUtil.MODULES_SUBDIR, module, qualifiedPackageName);
if (!Files.exists(packagePath))
return false;
// iterate files:
try {
return Files.list(packagePath)
.anyMatch(filePath -> filePath.toString().endsWith(SuffixConstants.SUFFIX_STRING_class)
|| filePath.toString().endsWith(SuffixConstants.SUFFIX_STRING_CLASS));
} catch (IOException e) {
return false;
}
}
public InputStream getContentFromJrt(String fileName, String module) throws IOException {
if (module != null) {
byte[] fileBytes = getFileBytes(fileName, module);
if(fileBytes == null) {
return null;
}
return new ByteArrayInputStream(fileBytes);
}
String[] modules = getModules(fileName);
for (String mod : modules) {
byte[] fileBytes = getFileBytes(fileName, mod);
if(fileBytes != null) {
return new ByteArrayInputStream(fileBytes);
}
}
return null;
}
private ClassFileReader getClassfile(String fileName, Predicate moduleNameFilter) throws IOException, ClassFormatException {
String[] modules = getModules(fileName);
byte[] content = null;
String module = null;
for (String mod : modules) {
if (moduleNameFilter != null && !moduleNameFilter.test(mod)) {
continue;
}
content = getFileBytes(fileName, mod);
if (content != null) {
module = mod;
break;
}
}
if (content != null) {
ClassFileReader reader = new ClassFileReader(content, fileName.toCharArray());
reader.moduleName = module.toCharArray();
return reader;
}
return null;
}
byte[] getClassfileContent(String fileName, String module) throws IOException {
byte[] content = null;
if (module != null) {
content = getFileBytes(fileName, module);
} else {
String[] modules = getModules(fileName);
for (String mod : modules) {
content = getFileBytes(fileName, mod);
if (content != null) {
break;
}
}
}
return content;
}
private byte[] getFileBytes(String fileName, String module) throws IOException {
Path path = this.fs.getPath(JRTUtil.MODULES_SUBDIR, module, fileName);
if(JRTUtil.DISABLE_CACHE) {
return JRTUtil.safeReadBytes(path);
} else {
try {
Optional bytes = this.classCache.computeIfAbsent(path, key -> {
try {
return Optional.ofNullable(JRTUtil.safeReadBytes(key));
} catch (IOException e) {
throw new RuntimeIOException(e);
}
});
return bytes.orElse(null);
} catch (RuntimeIOException rio) {
throw rio.getCause();
}
}
}
public ClassFileReader getClassfile(String fileName, String module, Predicate moduleNameFilter) throws IOException, ClassFormatException {
ClassFileReader reader = null;
if (module == null) {
reader = getClassfile(fileName, moduleNameFilter);
} else {
byte[] content = getFileBytes(fileName, module);
if (content != null) {
reader = new ClassFileReader(content, fileName.toCharArray());
reader.moduleName = module.toCharArray();
}
}
return reader;
}
public ClassFileReader getClassfile(String fileName, IModule module) throws IOException, ClassFormatException {
ClassFileReader reader = null;
if (module == null) {
reader = getClassfile(fileName, (Predicate)null);
} else {
byte[] content = getFileBytes(fileName, new String(module.name()));
if (content != null) {
reader = new ClassFileReader(content, fileName.toCharArray());
}
}
return reader;
}
void walkJrtForModules() throws IOException {
Iterable roots = this.fs.getRootDirectories();
for (java.nio.file.Path path : roots) {
try (DirectoryStream stream = Files.newDirectoryStream(path)) {
for (final java.nio.file.Path subdir: stream) {
if (!subdir.toString().equals(JRTUtil.MODULES_SUBDIR)) {
Files.walkFileTree(subdir, new JRTUtil.AbstractFileVisitor() {
@Override
public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs) throws IOException {
// e.g. /modules/java.base
java.nio.file.Path relative = subdir.relativize(file);
cachePackage(relative.getParent().toString(), relative.getFileName().toString());
return FileVisitResult.CONTINUE;
}
});
}
}
} catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
}
void walkModuleImage(final JRTUtil.JrtFileVisitor visitor, final int notify) throws IOException {
Files.walkFileTree(this.modRoot, new JRTUtil.AbstractFileVisitor() {
@Override
public FileVisitResult preVisitDirectory(java.nio.file.Path dir, BasicFileAttributes attrs) throws IOException {
int count = dir.getNameCount();
if (count == 1) return FileVisitResult.CONTINUE;
if (count == 2) {
// e.g. /modules/java.base
java.nio.file.Path mod = dir.getName(1);
if ((JRTUtil.MODULE_TO_LOAD != null && JRTUtil.MODULE_TO_LOAD.length() > 0 &&
JRTUtil.MODULE_TO_LOAD.indexOf(mod.toString()) == -1)) {
return FileVisitResult.SKIP_SUBTREE;
}
return ((notify & JRTUtil.NOTIFY_MODULES) == 0) ?
FileVisitResult.CONTINUE : visitor.visitModule(dir, JRTUtil.sanitizedFileName(mod));
}
if ((notify & JRTUtil.NOTIFY_PACKAGES) == 0) {
// We are dealing with a module or not client is not interested in packages
return FileVisitResult.CONTINUE;
}
return visitor.visitPackage(dir.subpath(2, count), dir.getName(1), attrs);
}
@Override
public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs) throws IOException {
if ((notify & JRTUtil.NOTIFY_FILES) == 0)
return FileVisitResult.CONTINUE;
int count = file.getNameCount();
// This happens when a file in a default package is present. E.g. /modules/some.module/file.name
if (count == 3) {
cachePackage(JRTUtil.DEFAULT_PACKAGE, file.getName(1).toString());
}
return visitor.visitFile(file.subpath(2, count), file.getName(1), attrs);
}
});
}
synchronized void cachePackage(String packageName, String module) {
packageName = packageName.replace('.', '/');
String currentModule = this.packageToModule.get(packageName);
if (currentModule == null) {
// Nothing found? Cache and return
this.packageToModule.put(packageName.intern(), module.intern());
return;
}
if(currentModule.equals(module)) {
// Same module found? Just return
return;
}
// We observe an additional module containing package
if (currentModule == JRTUtil.MULTIPLE) {
// We have already a list => update it
List list = this.packageToModules.get(packageName);
if (!list.contains(module)) {
if (JRTUtil.JAVA_BASE.equals(module)) {
list.add(0, JRTUtil.JAVA_BASE);
} else {
list.add(module.intern());
}
}
} else {
// We found a second module => create a list
List list = new ArrayList();
// Just do this as comparator might be overkill
if (JRTUtil.JAVA_BASE == currentModule || JRTUtil.JAVA_BASE.equals(currentModule)) {
list.add(currentModule.intern());
list.add(module.intern());
} else {
list.add(module.intern());
list.add(currentModule.intern());
}
packageName = packageName.intern();
this.packageToModules.put(packageName, list);
this.packageToModule.put(packageName, JRTUtil.MULTIPLE);
}
}
}