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

com.aspectran.utils.ClassScanner Maven / Gradle / Ivy

There is a newer version: 8.1.3
Show newest version
/*
 * Copyright (c) 2008-2024 The Aspectran Project
 *
 * 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.
 */
package com.aspectran.utils;

import com.aspectran.utils.annotation.jsr305.NonNull;
import com.aspectran.utils.annotation.jsr305.Nullable;
import com.aspectran.utils.logging.Logger;
import com.aspectran.utils.logging.LoggerFactory;
import com.aspectran.utils.wildcard.WildcardMatcher;
import com.aspectran.utils.wildcard.WildcardPattern;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import static com.aspectran.utils.ClassUtils.PACKAGE_SEPARATOR_CHAR;
import static com.aspectran.utils.PathUtils.REGULAR_FILE_SEPARATOR;
import static com.aspectran.utils.PathUtils.REGULAR_FILE_SEPARATOR_CHAR;

/**
 * The Class ClassScanner.
 *
 * @author Juho Jeong
 */
public class ClassScanner {

    private static final Logger logger = LoggerFactory.getLogger(ClassScanner.class);

    private final ClassLoader classLoader;

    public ClassScanner(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    /**
     * Find all classes that match the class name pattern.
     * @param classNamePattern the class name pattern
     * @return a Map for scanned classes
     * @throws IOException if an I/O error has occurred
     */
    public Map> scan(String classNamePattern) throws IOException {
        final Map> scannedClasses = new LinkedHashMap<>();
        scan(classNamePattern, scannedClasses);
        return scannedClasses;
    }

    /**
     * Find all classes that match the class name pattern.
     * @param classNamePattern the class name pattern
     * @param scannedClasses the Map for scanned classes
     * @throws IOException if an I/O error has occurred
     */
    public void scan(String classNamePattern, @NonNull final Map> scannedClasses) throws IOException {
        scan(classNamePattern, scannedClasses::put);
    }

    /**
     * Find all classes that match the class name pattern.
     * @param classNamePattern the class name pattern
     * @param saveHandler the save handler
     * @throws IOException if an I/O error has occurred
     */
    public void scan(String classNamePattern, SaveHandler saveHandler) throws IOException {
        if (classNamePattern == null) {
            throw new IllegalArgumentException("classNamePattern must not be null");
        }

        classNamePattern = classNamePattern.replace(PACKAGE_SEPARATOR_CHAR, REGULAR_FILE_SEPARATOR_CHAR);
        String basePackageName = determineBasePackageName(classNamePattern);
        if (basePackageName == null) {
            return;
        }

        String subPattern;
        if (classNamePattern.length() > basePackageName.length()) {
            subPattern = classNamePattern.substring(basePackageName.length());
        } else {
            subPattern = StringUtils.EMPTY;
        }

        WildcardPattern pattern = WildcardPattern.compile(subPattern, REGULAR_FILE_SEPARATOR_CHAR);
        WildcardMatcher matcher = new WildcardMatcher(pattern);

        Enumeration resources = classLoader.getResources(basePackageName);

        if (!StringUtils.endsWith(basePackageName, REGULAR_FILE_SEPARATOR_CHAR)) {
            basePackageName += REGULAR_FILE_SEPARATOR_CHAR;
        }

        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();

            if (logger.isDebugEnabled()) {
                logger.debug("Scan components from " + resource.getFile());
            }

            if (isJarResource(resource)) {
                scanFromJarResource(resource, matcher, saveHandler);
            } else {
                scan(resource.getFile(), basePackageName, null, matcher, saveHandler);
            }
        }
    }

    /**
     * Recursive method used to find all classes in a given directory and sub dirs.
     * @param targetPath the target path
     * @param basePackageName the base package name
     * @param relativePackageName the relative package name
     * @param matcher the matcher
     * @param saveHandler the save handler
     */
    private void scan(final String targetPath, final String basePackageName, final String relativePackageName,
                      final WildcardMatcher matcher, final SaveHandler saveHandler) {
        final File target = new File(targetPath);
        if (!target.exists()) {
            return;
        }

        target.listFiles(file -> {
            String fileName = file.getName();
            if (file.isDirectory()) {
                String subPackageName;
                if (relativePackageName != null) {
                    subPackageName = relativePackageName + fileName + REGULAR_FILE_SEPARATOR;
                } else {
                    subPackageName = fileName + REGULAR_FILE_SEPARATOR;
                }

                String basePath2 = targetPath + fileName + REGULAR_FILE_SEPARATOR;
                scan(basePath2, basePackageName, subPackageName, matcher, saveHandler);
            } else if (fileName.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) {
                String fn = fileName.substring(0, fileName.length() - ClassUtils.CLASS_FILE_SUFFIX.length());
                String className;
                if (relativePackageName != null) {
                    className = basePackageName + relativePackageName + fn;
                } else {
                    className = basePackageName + fn;
                }

                String relativePath = className.substring(basePackageName.length());
                if (matcher.matches(relativePath)) {
                    String resourceName = targetPath + fileName;
                    Class targetClass = loadClass(className);
                    saveHandler.save(resourceName, targetClass);
                }
            }
            return false;
        });
    }

    protected void scanFromJarResource(@NonNull URL resource, WildcardMatcher matcher, SaveHandler saveHandler)
            throws IOException {
        URLConnection conn = resource.openConnection();
        JarFile jarFile;
        String jarFileUrl;
        String entryNamePrefix;
        boolean newJarFile = false;

        if (conn instanceof JarURLConnection jarCon) {
            // Should usually be the case for traditional JAR files.
            jarCon.setUseCaches(false);
            jarFile = jarCon.getJarFile();
            jarFileUrl = jarCon.getJarFileURL().toExternalForm();
            JarEntry jarEntry = jarCon.getJarEntry();
            entryNamePrefix = (jarEntry != null ? jarEntry.getName() : "");
        } else {
            // No JarURLConnection -> need to resort to URL file parsing.
            // We'll assume URLs of the format "jar:path!/entry", with the protocol
            // being arbitrary as long as following the entry format.
            // We'll also handle paths with and without leading "file:" prefix.
            String urlFile = resource.getFile();
            int separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
            if (separatorIndex != -1) {
                jarFileUrl = urlFile.substring(0, separatorIndex);
                entryNamePrefix = urlFile.substring(separatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());
                jarFile = getJarFile(jarFileUrl);
            } else {
                jarFile = new JarFile(urlFile);
                jarFileUrl = urlFile;
                entryNamePrefix = "";
            }
            newJarFile = true;
        }

        try {
            //Looking for matching resources in jar file [" + jarFileUrl + "]"
            if (!entryNamePrefix.endsWith(REGULAR_FILE_SEPARATOR)) {
                // Root entry path must end with slash to allow for proper matching.
                // The Sun JRE does not return a slash here, but BEA JRockit does.
                entryNamePrefix = entryNamePrefix + REGULAR_FILE_SEPARATOR;
            }

            for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) {
                JarEntry entry = entries.nextElement();
                String entryName = entry.getName();
                if (entryName.startsWith(entryNamePrefix) && entryName.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) {
                    String entryNameSuffix = entryName.substring(entryNamePrefix.length(), entryName.length() -
                            ClassUtils.CLASS_FILE_SUFFIX.length());

                    if (matcher.matches(entryNameSuffix)) {
                        String resourceName = jarFileUrl + ResourceUtils.JAR_URL_SEPARATOR + entryName;
                        String className = entryNamePrefix + entryNameSuffix;
                        Class targetClass = loadClass(className);
                        saveHandler.save(resourceName, targetClass);
                    }
                }
            }
        } finally {
            // Close jar file, but only if freshly obtained -
            // not from JarURLConnection, which might cache the file reference.
            if (newJarFile) {
                jarFile.close();
            }
        }
    }

    @NonNull
    private JarFile getJarFile(@NonNull String jarFileUrl) throws IOException {
        if (jarFileUrl.startsWith(ResourceUtils.FILE_URL_PREFIX)) {
            try {
                return new JarFile(ResourceUtils.toURI(jarFileUrl).getSchemeSpecificPart());
            } catch (URISyntaxException ex) {
                // Fallback for URLs that are not valid URIs (should hardly ever happen).
                return new JarFile(jarFileUrl.substring(ResourceUtils.FILE_URL_PREFIX.length()));
            }
        } else {
            return new JarFile(jarFileUrl);
        }
    }

    private boolean isJarResource(@NonNull URL url) {
        String protocol = url.getProtocol();
        return (ResourceUtils.URL_PROTOCOL_JAR.equals(protocol) || ResourceUtils.URL_PROTOCOL_ZIP.equals(protocol));
    }

    @Nullable
    private String determineBasePackageName(String classNamePattern) {
        WildcardPattern pattern = new WildcardPattern(classNamePattern, REGULAR_FILE_SEPARATOR_CHAR);
        WildcardMatcher matcher = new WildcardMatcher(pattern);

        boolean matched = matcher.matches(classNamePattern);
        if (!matched) {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        while (matcher.hasNext()) {
            String str = matcher.next();
            if (WildcardPattern.hasWildcards(str)) {
                break;
            }
            sb.append(str).append(REGULAR_FILE_SEPARATOR_CHAR);
        }
        return sb.toString();
    }

    private Class loadClass(String className) {
        className = className.replace(REGULAR_FILE_SEPARATOR_CHAR, PACKAGE_SEPARATOR_CHAR);
        try {
            return classLoader.loadClass(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Unable to load class: " + className, e);
        }
    }

    public interface SaveHandler {

        void save(String resourceName, Class targetClass);

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy