io.github.lukehutch.fastclasspathscanner.scanner.ClasspathElementZip Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fast-classpath-scanner Show documentation
Show all versions of fast-classpath-scanner Show documentation
Uber-fast, ultra-lightweight Java classpath scanner. Scans the classpath by parsing the classfile binary format directly rather than by using reflection.
See https://github.com/lukehutch/fast-classpath-scanner
/*
* This file is part of FastClasspathScanner.
*
* Author: Luke Hutchison
*
* Hosted at: https://github.com/lukehutch/fast-classpath-scanner
*
* --
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Luke Hutchison
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
* EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.lukehutch.fastclasspathscanner.scanner;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import io.github.lukehutch.fastclasspathscanner.scanner.ScanSpec.ScanSpecPathMatch;
import io.github.lukehutch.fastclasspathscanner.scanner.matchers.FileMatchProcessorWrapper;
import io.github.lukehutch.fastclasspathscanner.utils.ClasspathUtils;
import io.github.lukehutch.fastclasspathscanner.utils.FastPathResolver;
import io.github.lukehutch.fastclasspathscanner.utils.FileUtils;
import io.github.lukehutch.fastclasspathscanner.utils.InterruptionChecker;
import io.github.lukehutch.fastclasspathscanner.utils.JarfileMetadataReader;
import io.github.lukehutch.fastclasspathscanner.utils.LogNode;
import io.github.lukehutch.fastclasspathscanner.utils.MultiMapKeyToList;
import io.github.lukehutch.fastclasspathscanner.utils.NestedJarHandler;
import io.github.lukehutch.fastclasspathscanner.utils.Recycler;
import io.github.lukehutch.fastclasspathscanner.utils.WorkQueue;
/** A zip/jarfile classpath element. */
class ClasspathElementZip extends ClasspathElement {
private File classpathEltZipFile;
/** Result of parsing the manifest file for this jarfile. */
private JarfileMetadataReader jarfileMetadataReader;
private Recycler zipFileRecycler;
/** A zip/jarfile classpath element. */
ClasspathElementZip(final RelativePath classpathEltPath, final ScanSpec scanSpec, final boolean scanFiles,
final NestedJarHandler nestedJarHandler, final WorkQueue workQueue,
final InterruptionChecker interruptionChecker, final LogNode log) {
super(classpathEltPath, scanSpec, scanFiles, interruptionChecker);
try {
classpathEltZipFile = classpathEltPath.getFile(log);
} catch (final IOException e) {
if (log != null) {
log.log("Exception while trying to canonicalize path " + classpathEltPath.getResolvedPath(), e);
}
skipClasspathElement = true;
return;
}
if (classpathEltZipFile == null || !ClasspathUtils.canRead(classpathEltZipFile)) {
if (log != null) {
log.log("Skipping non-existent jarfile " + classpathEltPath.getResolvedPath());
}
skipClasspathElement = true;
return;
}
try {
zipFileRecycler = nestedJarHandler.getZipFileRecycler(classpathEltZipFile, log);
} catch (final Exception e) {
if (log != null) {
log.log("Exception while creating zipfile recycler for " + classpathEltZipFile + " : " + e);
}
skipClasspathElement = true;
return;
}
final String jarfilePackageRoot = getJarfilePackageRoot();
try {
jarfileMetadataReader = nestedJarHandler.getJarfileMetadataReader(classpathEltZipFile,
jarfilePackageRoot, log);
} catch (final Exception e) {
if (log != null) {
log.log("Exception while reading metadata from " + classpathEltZipFile + " : " + e);
}
skipClasspathElement = true;
return;
}
ZipFile zipFile = null;
try {
try {
zipFile = zipFileRecycler.acquire();
} catch (final IOException e) {
if (log != null) {
log.log("Exception opening zipfile " + classpathEltZipFile + " : " + e.getMessage());
}
skipClasspathElement = true;
return;
}
// Parse the manifest entry if present
if (jarfileMetadataReader != null && jarfileMetadataReader.classPathEntriesToScan != null) {
final LogNode childClasspathLog = log == null ? null
: log.log("Found additional classpath entries in metadata for " + classpathEltZipFile);
// Class-Path entries in the manifest file are resolved relative to the dir the manifest's jarfile
// is contaiin. Get the parent path.
final String pathOfContainingDir = FastPathResolver.resolve(classpathEltZipFile.getParent());
// Create child classpath elements from Class-Path entry
if (childClasspathElts == null) {
childClasspathElts = new ArrayList<>(jarfileMetadataReader.classPathEntriesToScan.size());
}
for (int i = 0; i < jarfileMetadataReader.classPathEntriesToScan.size(); i++) {
final String childClassPathEltPath = jarfileMetadataReader.classPathEntriesToScan.get(i);
final RelativePath childRelativePath = new RelativePath(pathOfContainingDir,
childClassPathEltPath, classpathEltPath.getClassLoaders(), nestedJarHandler, scanSpec,
log);
if (!childRelativePath.equals(classpathEltPath)) {
// Add child classpath element. This may add lib jars more than once, in the case of a
// jar with "BOOT-INF/classes" and "BOOT-INF/lib", since this method may be called initially
// with "" as the package root, and then a second time with "BOOT-INF/classes" as a package
// root, and both times it will find "BOOT-INF/lib" -- but the caller will deduplicate
// the multiply-added lib jars.
childClasspathElts.add(childRelativePath);
if (childClasspathLog != null) {
childClasspathLog.log(childRelativePath.toString());
}
}
}
// Schedule child classpath elements for scanning
if (!childClasspathElts.isEmpty()) {
if (workQueue != null) {
workQueue.addWorkUnits(childClasspathElts);
} else {
// When adding rt.jar, workQueue will be null. But rt.jar should not include Class-Path
// references (so this block should not be reached).
if (log != null) {
log.log("Ignoring Class-Path entries in rt.jar: " + childClasspathElts);
}
}
}
}
if (scanFiles) {
fileMatches = new MultiMapKeyToList<>();
classfileMatches = new ArrayList<>();
fileToLastModified = new HashMap<>();
}
} finally {
zipFileRecycler.release(zipFile);
}
}
/** Scan for path matches within jarfile, and record ZipEntry objects of matching files. */
@Override
public void scanPaths(final LogNode log) {
final String path = classpathEltPath.getResolvedPath();
String canonicalPath = path;
try {
canonicalPath = classpathEltPath.getCanonicalPath(log);
} catch (final IOException e) {
}
final LogNode logNode = log == null ? null
: log.log(canonicalPath, "Scanning jarfile classpath entry " + classpathEltPath
+ (path.equals(canonicalPath) ? "" : " ; canonical path: " + canonicalPath));
ZipFile zipFile = null;
try {
try {
zipFile = zipFileRecycler.acquire();
} catch (final IOException e) {
if (logNode != null) {
logNode.log("Exception opening zipfile " + classpathEltZipFile, e);
}
skipClasspathElement = true;
return;
}
scanZipFile(classpathEltZipFile, zipFile, classpathEltPath.getJarfilePackageRoot(), logNode);
} finally {
zipFileRecycler.release(zipFile);
}
if (logNode != null) {
logNode.addElapsedTime();
}
}
private ClasspathResource newClasspathResource(final File classpathEltFile,
final String pathRelativeToClasspathElt, final String pathRelativeToClasspathPrefix,
final ZipEntry zipEntry) {
return new ClasspathResource(classpathEltFile, /* moduleRef = */ null, pathRelativeToClasspathElt,
pathRelativeToClasspathPrefix) {
ZipFile zipFile = null;
InputStream inputStream = null;
@Override
public InputStream open() throws IOException {
if (skipClasspathElement) {
// Can't open a file inside a zipfile if the zipfile couldn't be opened (should never be
// triggered)
throw new IOException("Parent zipfile could not be opened");
}
try {
if (zipFile != null || inputStream != null) {
// Should not happen, since this will only be called from single-threaded code when
// MatchProcessors are running
throw new RuntimeException("Tried to open classpath resource twice");
}
zipFile = zipFileRecycler.acquire();
inputStream = zipFile.getInputStream(zipEntry);
inputStreamLength = zipEntry.getSize();
return inputStream;
} catch (final Exception e) {
close();
throw new IOException("Could not open " + this, e);
}
}
@Override
public void close() {
if (inputStream != null) {
try {
inputStream.close();
} catch (final Exception e) {
// Ignore
}
inputStream = null;
}
if (zipFile != null) {
zipFileRecycler.release(zipFile);
zipFile = null;
}
}
};
}
/** Scan a zipfile for file path patterns matching the scan spec. */
private void scanZipFile(final File zipFileFile, final ZipFile zipFile, final String classpathBaseDir,
final LogNode log) {
// Support specification of a classpath root within a jarfile, as required by Spring, e.g.
// "spring-project.jar!/BOOT-INF/classes"
String requiredPrefix;
if (!classpathBaseDir.isEmpty()) {
if (log != null) {
log.log("Classpath prefix within jarfile: " + classpathBaseDir);
}
requiredPrefix = classpathBaseDir + "/";
} else {
requiredPrefix = "";
}
if (requiredPrefix.startsWith("/")) {
// Strip any initial "/" to correspond with handling of relativePath below
requiredPrefix = requiredPrefix.substring(1);
}
final int requiredPrefixLen = requiredPrefix.length();
Set loggedNestedClasspathRootPrefixes = null;
String prevParentRelativePath = null;
ScanSpecPathMatch prevParentMatchStatus = null;
for (final ZipEntry zipEntry : jarfileMetadataReader.zipEntries) {
// Normalize path of ZipEntry
String relativePath = zipEntry.getName();
while (relativePath.startsWith("/")) {
relativePath = relativePath.substring(1);
}
// Ignore entries without the correct classpath root prefix
if (requiredPrefixLen > 0) {
if (!relativePath.startsWith(requiredPrefix)) {
continue;
}
// Strip the classpath root prefix from the relative path
relativePath = relativePath.substring(requiredPrefixLen);
}
// Check if the relative path is within a nested classpath root
if (nestedClasspathRootPrefixes != null) {
// This is O(mn), which is inefficient, but the number of nested classpath roots should be small
boolean reachedNestedRoot = false;
for (final String nestedClasspathRoot : nestedClasspathRootPrefixes) {
if (relativePath.startsWith(nestedClasspathRoot)) {
// relativePath has a prefix of nestedClasspathRoot
if (log != null) {
if (loggedNestedClasspathRootPrefixes == null) {
loggedNestedClasspathRootPrefixes = new HashSet<>();
}
if (loggedNestedClasspathRootPrefixes.add(nestedClasspathRoot)) {
log.log("Reached nested classpath root, stopping recursion to avoid duplicate "
+ "scanning: " + nestedClasspathRoot);
}
}
reachedNestedRoot = true;
break;
}
}
if (reachedNestedRoot) {
continue;
}
}
// Get match status of the parent directory of this zipentry file's relative path (or reuse the last
// match status for speed, if the directory name hasn't changed).
final int lastSlashIdx = relativePath.lastIndexOf("/");
final String parentRelativePath = lastSlashIdx < 0 ? "/" : relativePath.substring(0, lastSlashIdx + 1);
final boolean parentRelativePathChanged = !parentRelativePath.equals(prevParentRelativePath);
final ScanSpecPathMatch parentMatchStatus = //
prevParentRelativePath == null || parentRelativePathChanged
? scanSpec.dirWhitelistMatchStatus(parentRelativePath)
: prevParentMatchStatus;
prevParentRelativePath = parentRelativePath;
prevParentMatchStatus = parentMatchStatus;
// Class can only be scanned if it's within a whitelisted path subtree, or if it is a classfile that has
// been specifically-whitelisted
if (parentMatchStatus != ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX
&& parentMatchStatus != ScanSpecPathMatch.AT_WHITELISTED_PATH
&& (parentMatchStatus != ScanSpecPathMatch.AT_WHITELISTED_CLASS_PACKAGE
|| !scanSpec.isSpecificallyWhitelistedClass(relativePath))) {
if (log != null) {
log.log("Skipping non-whitelisted path: " + relativePath);
}
continue;
}
final LogNode subLog = log == null ? null
: log.log(relativePath, "Found whitelisted file: " + relativePath);
// Store relative paths of any classfiles encountered
if (FileUtils.isClassfile(relativePath)) {
classfileMatches.add(
newClasspathResource(zipFileFile, requiredPrefix + relativePath, relativePath, zipEntry));
}
// Match file paths against path patterns
for (final FileMatchProcessorWrapper fileMatchProcessorWrapper : //
scanSpec.getFileMatchProcessorWrappers()) {
if (fileMatchProcessorWrapper.filePathMatches(relativePath, subLog)) {
// File's relative path matches.
fileMatches.put(fileMatchProcessorWrapper, newClasspathResource(zipFileFile,
requiredPrefix + relativePath, relativePath, zipEntry));
}
}
}
// Don't use the last modified time from the individual zipEntry
// objects, we use the last modified time for the zipfile itself instead.
fileToLastModified.put(zipFileFile, zipFileFile.lastModified());
}
/** Close and free all open ZipFiles. */
@Override
public void close() {
if (zipFileRecycler != null) {
zipFileRecycler.close();
}
zipFileRecycler = null;
}
}