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

com.arakelian.core.utils.AbstractClassScanner Maven / Gradle / Ivy

Go to download

Small utility classes useful in a variety of projects. Don't reinvent the wheel. Stay small.

There is a newer version: 4.0.1
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.arakelian.core.utils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.immutables.value.Value;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.arakelian.core.feature.Nullable;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;

/**
 * Search for Java classes that implement one or more interfaces.
 *
 * The search is restricted to Java classes that are publically scoped. Inner static public classes
 * are also searched.
 *
 * Based upon com.sun.jersey.server.impl.container.config.AnnotatedClassScanner
 */
@Value.Immutable(copy = false)
@Value.Style(typeAbstract = { "Abstract*" }, typeImmutable = "*")
public abstract class AbstractClassScanner {
    public static class NamePredicate implements Predicate {
        /**
         * File matching pattern
         */
        private final Pattern[] patterns;

        public NamePredicate(final Pattern... patterns) {
            this.patterns = patterns;
        }

        @Override
        public boolean apply(final String name) {
            for (int i = 0, n = patterns != null ? patterns.length : 0; i < n; i++) {
                if (patterns[i].matcher(name).matches()) {
                    return true;
                }
            }
            return false;
        }
    }

    private final class ClassFilter extends ClassVisitor {
        /**
         * The name of the visited class.
         */
        private String className;

        /**
         * True if the class has the correct scope
         */
        private boolean isScoped;

        public ClassFilter() {
            super(Opcodes.ASM4);
        }

        @Override
        public void visit(
                final int version,
                final int access,
                final String name,
                final String signature,
                final String superName,
                final String[] interfaces) {
            className = name;
            isScoped = (access & Opcodes.ACC_PUBLIC) != 0;
        }

        @Override
        public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
            // Do nothing
            return null;
        }

        @Override
        public void visitAttribute(final Attribute attribute) {
            // Do nothing
        }

        @Override
        public void visitEnd() {
            if (isScoped) {
                final Class clazz = getClassForName(className.replaceAll("/", "."));
                if (clazz != null) {
                    final boolean haveClass = getAnnotatedWith().size() != 0
                            || getAssignableFrom().size() != 0;
                    if (haveClass && !annotatedWithOrAssignableFrom(clazz)) {
                        // did not implement or have any of the annotations specified
                        return;
                    }
                    if (getClassPredicate() == null || getClassPredicate().apply(clazz)) {
                        matchingClasses.add(clazz);
                    }
                }
            }
        }

        @Override
        public FieldVisitor visitField(
                final int i,
                final String string,
                final String string0,
                final String string1,
                final Object object) {
            // Do nothing
            return null;
        }

        @Override
        public void visitInnerClass(
                final String name,
                final String outerName,
                final String innerName,
                final int access) {
            // Do nothing
        }

        @Override
        public MethodVisitor visitMethod(
                final int i,
                final String string,
                final String string0,
                final String string1,
                final String[] string2) {
            // Do nothing
            return null;
        }

        @Override
        public void visitOuterClass(final String string, final String string0, final String string1) {
            // Do nothing
        }

        @Override
        public void visitSource(final String string, final String string0) {
            // Do nothing
        }

        @SuppressWarnings({ "unchecked", "NonRuntimeAnnotation" })
        private boolean annotatedWithOrAssignableFrom(final Class clazz) {
            for (final Class c : getAnnotatedWith()) {
                if (c.isAnnotation()) {
                    if (clazz.getAnnotation(c) != null) {
                        if (getClassPredicate() == null || getClassPredicate().apply(clazz)) {
                            return true;
                        }
                    }
                }
            }
            for (final Class c : getAssignableFrom()) {
                if (c.isAssignableFrom(clazz)) {
                    if (getClassPredicate() == null || getClassPredicate().apply(clazz)) {
                        return true;
                    }
                }
            }
            return false;
        }
    }

    /**
     * Logger for this class
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(ClassScanner.class);

    /**
     * Matching annotated classes.
     */
    private final Set> matchingClasses = Sets.newLinkedHashSet();

    /**
     * Matching files.
     */
    private final Set matchingFiles = Sets.newLinkedHashSet();

    /**
     * Tests candidate classes for match against search criteria
     */
    private final ClassFilter classFilter = new ClassFilter();

    /**
     * Returns a list of annotations that we are searching for.
     *
     * @return list of annotations that we are looking for
     */
    public abstract List> getAnnotatedWith();

    /**
     * Returns a list of classes that we are searching for.
     *
     * @return list of classes that we are searching for.
     */
    public abstract List> getAssignableFrom();

    /**
     * Returns a predicate which tests to see if class is a match. If this method returns null, it
     * is assumed that the predicate always returns true.
     *
     * @return predicate which tests to see if class is a match.
     */
    @Nullable
    public abstract Predicate> getClassPredicate();

    /**
     * Returns a predicate which tests to see if file is a match. If this method returns null, it is
     * assumed that the predicate always returns true.
     *
     * @return predicate which tests to see if file is a match.
     */
    @Nullable
    public abstract Predicate getFilePredicate();

    /**
     * Returns true to ignore {@link NoClassDefFoundError} when loading classes.
     *
     * @return true to ignore {@link NoClassDefFoundError} when loading classes.
     */
    public abstract Optional getIgnoreNoClassDefFoundError();

    public List> getMatchingClasses() {
        final List> result = new ArrayList<>(matchingClasses);
        Collections.sort(result, new Comparator>() {
            @Override
            public int compare(final Class c1, final Class c2) {
                return c1.getName().compareTo(c2.getName());
            }
        });
        return ImmutableList.> copyOf(result);
    }

    public Set getMatchingFiles() {
        return matchingFiles;
    }

    /**
     * Returns the class loader to use while loading classes.
     *
     * @return the class loader to use while loading classes.
     */
    @Nullable
    @Value.Default
    public ClassLoader getRootClassloader() {
        return ClassUtils.getContextClassLoader();
    }

    /**
     * Scans paths for matching Java classes
     *
     * @param paths
     *            An array of absolute paths to search.
     */
    public void scan(final File[] paths) {
        for (final File file : paths) {
            scan(file);
        }
    }

    /**
     * Scans packages for matching Java classes.
     *
     * @param packages
     *            An array of packages to search.
     */
    public void scan(final String[] packages) {
        LOGGER.info("Scanning packages {}:", ArrayUtils.toString(packages));
        for (final String pkg : packages) {
            try {
                final String pkgFile = pkg.replace('.', '/');
                final Enumeration urls = getRootClassloader().getResources(pkgFile);
                while (urls.hasMoreElements()) {
                    final URL url = urls.nextElement();
                    try {
                        final URI uri = getURI(url);
                        LOGGER.debug("Scanning {}", uri);
                        scan(uri, pkgFile);
                    } catch (final URISyntaxException e) {
                        LOGGER.debug("URL {} cannot be converted to a URI", url, e);
                    }
                }
            } catch (final IOException ex) {
                throw new RuntimeException("The resources for the package" + pkg + ", could not be obtained",
                        ex);
            }
        }
        LOGGER.info(
                "Found {} matching classes and {} matching files",
                matchingClasses.size(),
                matchingFiles.size());
    }

    private Class getClassForName(final String className) {
        try {
            return ClassUtils.classForNameWithException(className, getRootClassloader());
        } catch (final ClassNotFoundException | NoClassDefFoundError e) {
            LOGGER.warn(
                    "Cannot load class file: {}, exception: {}: {}",
                    className,
                    e.getClass().getName(),
                    e.getMessage());
            return null;
        }
    }

    private ClassReader getClassReader(final JarFile jarFile, final JarEntry entry) {
        InputStream is = null;
        try {
            is = jarFile.getInputStream(entry);
            return new ClassReader(is);
        } catch (final IOException ex) {
            throw new RuntimeException(
                    "Unable to read jar file: " + jarFile.getName() + ", entry: " + entry.getName(), ex);
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (final IOException ex) {
                LOGGER.error(
                        "Error closing input stream of the jar file, {}, entry, {}, closed.",
                        jarFile.getName(),
                        entry.getName());
            }
        }
    }

    private ClassReader getClassReader(final URI classFileUri) {
        InputStream is = null;
        try {
            is = classFileUri.toURL().openStream();
            return new ClassReader(is);
        } catch (final IOException ex) {
            throw new RuntimeException("Error accessing input stream of the class file URI, " + classFileUri,
                    ex);
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (final IOException ex) {
                LOGGER.error("Error closing input stream of the class file URI, {}", classFileUri);
            }
        }
    }

    private JarFile getJarFile(final File file) {
        if (file == null) {
            return null;
        }
        try {
            return new JarFile(file);
        } catch (final IOException ex) {
            throw new RuntimeException(file.getAbsolutePath() + " is not a jar file", ex);
        }
    }

    private URI getURI(final URL url) throws URISyntaxException {
        if (url.getProtocol().equalsIgnoreCase("vfsfile")) {
            // Used with JBoss 5.x: trim prefix "vfs"
            return new URI(url.toString().substring(3));
        }
        return url.toURI();
    }

    private boolean isIgnoreNoClassDefFound() {
        return getIgnoreNoClassDefFoundError().orElse(Boolean.FALSE).booleanValue();
    }

    private void scan(final File file) {
        if (file.isDirectory()) {
            LOGGER.trace("Scanning: {}", file);
            scanDirectory(file, file, true);
        } else if (file.getName().endsWith(".jar") || file.getName().endsWith(".zip")) {
            scanJar(file);
        } else {
            LOGGER.warn("Ignoring {}, it not a directory, a jar file or a zip file", file.getAbsolutePath());
        }
    }

    private void scan(final URI uri, final String filePackageName) {
        final String scheme = uri.getScheme();
        if (scheme.equals("file")) {
            final String uriPath = uri.getPath();
            final File uriFile = new File(uriPath);
            LOGGER.trace("URI: {}", uri);
            LOGGER.trace("Path: {}", uri.getPath());
            if (uriFile.isDirectory()) {
                String rootPath = uriPath.replace('\\', '/');
                if (rootPath.endsWith("/")) {
                    // remove trailing path for comparison below
                    rootPath = rootPath.substring(0, rootPath.length() - 1);
                }
                LOGGER.trace("Root path: {}", rootPath);
                final String packageFolder = filePackageName.replace('.', '/').replace('\\', '/');
                if (rootPath.endsWith(packageFolder)) {
                    rootPath = rootPath.substring(0, rootPath.length() - filePackageName.length());
                    LOGGER.trace("Root path modified: {}", rootPath);
                }

                final File root = new File(rootPath);
                scanDirectory(root, uriFile, false);
            } else {
                LOGGER.warn("URL, {}, is ignored. The path, {}, is not a directory", uri, uriFile.getPath());
            }
        } else if (scheme.equals("jar") || scheme.equals("zip")) {
            final URI jarUri = URI.create(uri.getRawSchemeSpecificPart());
            String jarFile = jarUri.getPath();
            jarFile = jarFile.substring(0, jarFile.indexOf('!'));
            scanJar(new File(jarFile), filePackageName);
        } else {
            LOGGER.warn("URL, {}, is ignored, it not a file or a jar file URL", uri);
        }
    }

    private void scanClassFile(final JarFile jarFile, final JarEntry entry) {
        try {
            getClassReader(jarFile, entry).accept(classFilter, 0);
        } catch (final NoClassDefFoundError e) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(
                        "Skipping {} due to {}: {}",
                        entry.getName(),
                        e.getClass().getSimpleName(),
                        e.getMessage());
            }
            if (!isIgnoreNoClassDefFound()) {
                throw e;
            }
        }
    }

    private void scanClassFile(final URI classFileUri) {
        try {
            getClassReader(classFileUri).accept(classFilter, 0);
        } catch (final NoClassDefFoundError e) {
            LOGGER.debug("Cannot analyze class file: {}, exception: {}", classFileUri, e.getMessage());
            if (!isIgnoreNoClassDefFound()) {
                throw e;
            }
        }
    }

    private void scanDirectory(final File root, final File parent, final boolean indexJars) {
        final String rootString = root.getPath();

        final File[] files = parent.listFiles();
        if (files == null) {
            return;
        }
        for (final File child : files) {
            if (child.isDirectory()) {
                scanDirectory(root, child, indexJars);
                continue;
            }

            String name = child.getPath();
            if (name.startsWith(rootString)) {
                name = name.substring(rootString.length());
            }
            if (getFilePredicate() == null || getFilePredicate().apply(name)) {
                LOGGER.trace("Matching file: {}", name);
                matchingFiles.add(name);
            }

            if (indexJars && name.endsWith(".jar")) {
                scanJar(child);
            } else if (name.endsWith(".class")) {
                scanClassFile(child.toURI());
            }
        }
    }

    private void scanJar(final File file) {
        scanJar(file, "");
    }

    private void scanJar(final File file, final String parent) {
        final JarFile jar = getJarFile(file);
        try {
            final Enumeration entries = jar.entries();
            while (entries.hasMoreElements()) {
                final JarEntry entry = entries.nextElement();
                if (!entry.isDirectory()) {
                    final String name = StringUtils.removeStart(entry.getName(), "BOOT-INF/classes/");
                    if (getFilePredicate() == null || getFilePredicate().apply(name)) {
                        LOGGER.trace("Matching jar file: {}", name);
                        matchingFiles.add(name);
                    }
                    if (name.startsWith(parent) && name.endsWith(".class")) {
                        scanClassFile(jar, entry);
                    }
                }
            }
        } catch (final Exception e) {
            LOGGER.error("Exception while processing jar file: {}", file, e);
        } finally {
            try {
                if (jar != null) {
                    jar.close();
                }
            } catch (final IOException ex) {
                LOGGER.error("Error closing jar file: {}", jar.getName());
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy