de.spricom.dessert.resolve.ClassResolver Maven / Gradle / Ivy
Show all versions of dessert-core Show documentation
package de.spricom.dessert.resolve;
/*-
* #%L
* Dessert Dependency Assertion Library for Java
* %%
* Copyright (C) 2017 - 2023 Hans Jörg Heßmann
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import de.spricom.dessert.matching.NamePattern;
import de.spricom.dessert.util.Predicate;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The class resolver provides fast access to the classes and packages to analyze.
* Therefore, it maintains of a list of all {@link ClassRoot} objects for which
* each represents a classes directory or a JAR file. And it has two HashMaps
* for all the packages and classes contained in any of these roots. The key
* used for theses HashMaps is the fully qualified class or package name.
*
* The {@link ClassPackage} has a collection of all classes subpackages it contains.
* Thus it provides direct access to all classes and subpackages of some package
* within some root.
*
* Each {@link ClassPackage} or {@link ClassEntry} belongs to one root. The
* same class or package name may appear within different roots. In that case the
* ClassPackage or ClassEntry has {@link LinkedList} of all entries with the
* same name. This lists can be accessed by {@link ClassPackage#getAlternatives()}
* or {@link ClassEntry#getAlternatives()} respectively. Each entry points
* to the same list of alternatives. If there are no alternatives the corresponding
* list is null.
*
* Typically one of the static of methods should be used to create
* a ClassResolver.
*/
public final class ClassResolver implements TraversalRoot {
private static final Logger log = Logger.getLogger(ClassResolver.class.getName());
private final List path = new ArrayList(60);
private final ClassResolverCache cache = new ClassResolverCache();
private boolean ignoreManifest;
private boolean frozen;
/**
* Creates a ClassResolver for some arbitrary path.
*
* @param path the path to scan using the system specific classpath format
* @return a ClassResolver with the corresponding entries
*/
public static ClassResolver of(String path) {
ClassResolver r = new ClassResolver();
r.add(path);
return r;
}
/**
* Creates a ClassResolver based on the java.class.path system-property.
* For each JAR that contains a Manifest file with a Class-Path attribute,
* those entries will be added, too, recursively.
*
* @return a ClassResolver with the corresponding entries
* @throws ResolveException if a directory or jar file could not be read
*/
public static ClassResolver ofClassPath() {
ClassResolver r = new ClassResolver();
r.addClassPath();
return r;
}
/**
* Creates a ClassResolver with all entries for {@link #ofClassPath()} and all entries
* from the sun.boot.class.path system-properties for java 8 and before or
* all java runtime modules from Java 9 on.
*
* @return a ClassResolver with the corresponding entries
* @throws ResolveException if a directory or jar file could not be read
*/
public static ClassResolver ofClassPathAndJavaRuntime() {
long ts = System.nanoTime();
ClassResolver r = new ClassResolver();
r.addClassPath();
r.addBootClassPath();
r.addJavaRuntimeModules();
if (log.isLoggable(Level.FINE)) {
log.fine(String.format("Needed %.1f ms to scan classes form java.class.path and runtime.",
(System.nanoTime() - ts) / 1e6));
}
return r;
}
/**
* Creates a ClassResolver containing only the directories on the java.class.path system-property.
*
* @return a ClassResolver with the corresponding entries
* @throws ResolveException if a directory or jar file could not be read
*/
public static ClassResolver ofClassPathWithoutJars() {
ClassResolver r = new ClassResolver();
for (String entry : System.getProperty("java.class.path").split(File.pathSeparator)) {
if (!entry.endsWith(".jar")) {
r.addFile(entry);
}
}
return r;
}
/**
* Creates a ClassResolver similar to {@link #ofClassPath()}, but without the entries
* from the Class-Path attribute of any Manifest file. The {@link #isIgnoreManifest()}
* flag of the resulting ClassResolver will be true.
*
* @return a ClassResolver with the corresponding entries
* @throws ResolveException if a directory or jar file could not be read
*/
public static ClassResolver ofClassPathIgnoringManifests() {
ClassResolver r = new ClassResolver();
r.setIgnoreManifest(true);
r.addClassPath();
return r;
}
/**
* Creates a ClassResolver with all entries for {@link #ofClassPath()} and all entries
* from the sun.boot.class.path system-property. From Java 9 on there is
* no difference to {@link #ofClassPath()}.
*
* @return a ClassResolver with the corresponding entries
* @throws ResolveException if a directory or jar file could not be read
* @deprecated use ofClassPathAndJavaRuntime instead
*/
@Deprecated
public static ClassResolver ofClassPathAndBootClassPath() {
ClassResolver r = new ClassResolver();
r.addClassPath();
r.addBootClassPath();
return r;
}
public void addClassPath() {
add(System.getProperty("java.class.path"));
}
public void addBootClassPath() {
String path = System.getProperty("sun.boot.class.path");
// For JDK 9 there is no sun.boot.class.path property
if (path != null) {
add(path);
}
}
/**
* Adds all modules form the Java Runtime System. For Java 8 and older this
* method has no effect
*
* @throws ResolveException modules could not be read
*/
public void addJavaRuntimeModules() {
if (!isJrtFileSystemAvailable()) {
return;
}
try {
ReflectiveJrtFileSystem fs = new ReflectiveJrtFileSystem();
for (String module : fs.listModules()) {
addRoot(new JrtModuleRoot(module, fs));
}
} catch (IOException ex) {
throw new ResolveException("Unable to read java runtime modules: " + ex.getMessage(), ex);
} catch (InvocationTargetException ex) {
if (ex.getTargetException() instanceof IOException) {
throw new ResolveException("Unable to read java runtime modules: " +
ex.getTargetException().getMessage(), ex.getTargetException());
}
throw new ResolveException("Unable to read java runtime modules.", ex);
} catch (ClassNotFoundException ex) {
throw new ResolveException("Cannot access NIO classes.", ex);
} catch (NoSuchMethodException ex) {
throw new ResolveException("Cannot access NIO classes.", ex);
} catch (IllegalAccessException ex) {
throw new ResolveException("Cannot access NIO classes.", ex);
} catch (URISyntaxException ex) {
throw new ResolveException("Cannot convert jrt-URI.", ex);
}
}
public static boolean isJrtFileSystemAvailable() {
URL resource = String.class.getResource("String.class");
return resource != null && "jrt".equalsIgnoreCase(resource.getProtocol());
}
public void add(String path) {
for (String entry : path.split(File.pathSeparator)) {
addFile(entry);
}
}
private void addFile(String filename) {
add(new File(filename));
}
public void add(File file) {
try {
if (!file.exists()) {
log.warning("Does not exist: " + file.getAbsolutePath());
} else if (getRoot(file) != null) {
log.warning("Already on path: " + file.getAbsolutePath());
} else if (file.isDirectory()) {
addRoot(new DirectoryRoot(file));
} else if (file.isFile() && file.getName().endsWith(".jar")) {
JarRoot root = new JarRoot(file);
addRoot(root);
if (!ignoreManifest) {
addManifestClassPath(root);
}
} else {
log.warning("Don't know how to process: " + file.getAbsolutePath());
}
} catch (IOException ex) {
throw new ResolveException("Unable to resolve " + file.getAbsolutePath() + ": " + ex.getMessage(), ex);
}
}
public void addRoot(ClassRoot root) throws IOException {
if (frozen) {
throw new IllegalStateException("Cannot add root to a frozen ClassResolver.");
}
path.add(root);
long ts = System.nanoTime();
root.scan(cache);
if (log.isLoggable(Level.FINER)) {
log.fine(String.format("Needed %.1f ms to scan classes form %s.",
(System.nanoTime() - ts) / 1e6,
root.getURI()));
}
}
/**
* Adds all entries from the Class-Path attribute of the JAR's Manifest file.
* Does nothing if there is no Manifest file or no Class-Path attribute.
* Processes JAR files recursively unless {@link #isIgnoreManifest()} has been set.
* Use {@link #getRoot(File)} to get the {@link JarRoot} for a file.
*
* @param jarRoot the jar to process
* @throws IOException if the Manifest could not be read
*/
public void addManifestClassPath(JarRoot jarRoot) throws IOException {
Manifest manifest = jarRoot.getManifest();
if (manifest == null) {
return;
}
String classpath = manifest.getMainAttributes().getValue("Class-Path");
if (classpath == null) {
return;
}
URL context = jarRoot.getRootFile().toURI().toURL();
for (String relativeUrl : classpath.split("\\s+")) {
try {
File file = new File(new URL(context, relativeUrl).toURI().getPath());
if (file.exists()) {
add(file);
} else {
log.info("Does not exist: " + file.getAbsolutePath() +
" (referenced by Manifest of " + context + ")");
}
} catch (URISyntaxException ex) {
log.warning("Unable to parse relative path " + relativeUrl
+ " within Manifest of " + context + ": " + ex.getMessage());
}
}
}
public boolean isIgnoreManifest() {
return ignoreManifest;
}
/**
* If set the Class-Path attribute of JAR's Manifest files will be ignored.
*
* @param ignoreManifest the flag
*/
public void setIgnoreManifest(boolean ignoreManifest) {
this.ignoreManifest = ignoreManifest;
}
/**
* After freezing any modification to the path represented by this resolver will
* result in an {@link IllegalStateException}.
*/
public void freeze() {
frozen = true;
}
public ClassRoot getRoot(File file) {
for (ClassRoot root : path) {
if (root.getRootFile() != null && root.getRootFile().equals(file)) {
return root;
}
}
return null;
}
public ClassPackage getPackage(String packageName) {
return cache.getPackage(packageName);
}
public ClassPackage getPackage(File root, String packageName) {
ClassPackage pckg = getPackage(packageName);
if (pckg == null) {
return null;
}
URI rootUri = root.toURI();
if (rootUri.equals(pckg.getRoot().getURI())) {
return pckg;
}
if (pckg.getAlternatives() != null) {
for (ClassPackage alt : pckg.getAlternatives()) {
if (rootUri.equals(alt.getRoot().getURI())) {
return alt;
}
}
}
return null;
}
public ClassEntry getClassEntry(String classname) {
return cache.getClassEntry(classname);
}
public ClassEntry getClassEntry(File root, String classname) {
ClassEntry ce = getClassEntry(classname);
if (ce == null) {
return null;
}
URI rootUri = root.toURI();
if (rootUri.equals(ce.getPackage().getRoot().getURI())) {
return ce;
}
if (ce.getAlternatives() != null) {
for (ClassEntry alt : ce.getAlternatives()) {
if (rootUri.equals(alt.getPackage().getRoot().getURI())) {
return alt;
}
}
}
return null;
}
public void traverse(NamePattern pattern, ClassVisitor visitor) {
for (ClassRoot classRoot : path) {
classRoot.traverse(pattern, visitor);
}
}
public List getPath() {
return path;
}
public Set getRootFiles() {
return getRootFiles(new Predicate() {
@Override
public boolean test(File t) {
return t != null;
}
});
}
public Set getRootJars() {
return getRootFiles(new Predicate() {
@Override
public boolean test(File t) {
return t != null && !t.isDirectory();
}
});
}
public Set getRootDirs() {
return getRootFiles(new Predicate() {
@Override
public boolean test(File t) {
return t != null && t.isDirectory();
}
});
}
public Set getRootFiles(Predicate predicate) {
Set files = new HashSet(path.size());
for (ClassRoot cr : path) {
if (predicate.test(cr.getRootFile())) {
files.add(cr.getRootFile());
}
}
return files;
}
public Map> getDuplicates() {
return cache.getDuplicates();
}
public int getPackageCount() {
return cache.getPackageCount();
}
public int getClassCount() {
return cache.getClassCount();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (File root : getRootFiles()) {
if (sb.length() == 0) {
sb.append("classpath ");
} else {
sb.append(File.pathSeparator);
}
sb.append(root.getAbsolutePath());
}
return sb.toString();
}
}