All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.lukehutch.fastclasspathscanner.scanner.ClasspathElementZip Maven / Gradle / Ivy

Go to download

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

There is a newer version: 4.0.0-beta-7
Show newest version
/*
 * 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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy