org.primeframework.mvc.util.ClassClasspathResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.primeframework.mvc.util.ClassClasspathResolver.java

Source

/*
 * Copyright (c) 2001-2007, Inversoft, All Rights Reserved
 *
 * 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 org.primeframework.mvc.util;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.primeframework.mvc.PrimeException;

import static java.util.Arrays.asList;

/**
 * Locates classes within the current ClassLoader/ClassPath. This class begins from a directory and locates all the
 * <strong>.class</strong> files in that directory and possibly in sub-directories. For each file located either on the
 * file system or in a JAR file, the classes are loaded into the JVM as Class objects. These objects are then passed to
 * the various Test classes and interfaces defined in this class.
 * <p/>
 * When Class instances are matched using the Test interfaces and classes from this class that are added to a set of
 * matches. These matches can then be fetched and used however is required.
 *
 * @author Brian Pontarelli
 */
public class ClassClasspathResolver<U> {

    /**
     * Attempts to discover resources that pass the test.
     * <p/>
     * Examples:
     * <p/>
     * <pre>
     * locators: foo bar
     *
     * JAR file in classpath
     * ----------------------
     * com/example/foo/sub/File.class
     *
     * Directory in classpath (/opt/classpath)
     * ----------------------
     * /opt/classpath/com/example/bar/sub/File2.class
     *
     * This will find directories com/example/foo and com/example/bar
     * </pre>
     *
     * @param test       The test implementation to determine matching resources.
     * @param recursive  If true, this will recurse into sub-directories. If false, this will only look in the directories
     *                   given.
     * @param locators   A list of directory locators that are used to locate directories to find resources in.
     * @return The matching set.
     * @throws IOException If there was any errors while inspecting the classpath.
     */
    public Set<Class<U>> findByLocators(Test<Class<U>> test, boolean recursive, String... locators)
            throws IOException {
        if (locators == null) {
            return null;
        }

        Classpath classpath = Classpath.getCurrentClassPath();
        List<String> names = classpath.getNames();
        Set<Class<U>> matches = new HashSet<Class<U>>();
        for (String name : names) {
            File f = new File(name);
            if (f.isDirectory()) {
                for (String locator : locators) {
                    Set<File> directories = findDirectories(f, locator);
                    for (File dir : directories) {
                        matches.addAll(loadFromDirectory(dir, test, recursive));
                    }
                }
            } else if (f.isFile()) {
                matches.addAll(loadFromJar(f, test, recursive, asList(locators), true));
            }
        }

        return matches;
    }

    private Set<File> findDirectories(File dir, String locator) {
        // Loop over the files using tail-recursion
        Set<File> directories = new HashSet<File>();
        Queue<File> files = new LinkedList<File>(safeListFiles(dir, DirectoryFileFilter.INSTANCE));
        while (!files.isEmpty()) {
            File file = files.poll();
            if (file.isDirectory() && file.getName().equals(locator)) {
                directories.add(file);
            } else if (file.isDirectory()) {
                files.addAll(safeListFiles(file, null));
            }
        }

        return directories;
    }

    /**
     * This provides a safe mechanism for listing all of the files in a directory. The listFiles method can return null
     * and cause major issues. This performs that method in a null safe manner.
     *
     *
     * @param dir    The directory to list the files for.
     * @param filter An optional FileFilter.
     * @return A List of Files, which is never null.
     */
    private List<File> safeListFiles(File dir, FileFilter filter) {
        File[] files = dir.listFiles(filter);

        if (files == null) {
            return Collections.emptyList();
        }

        return asList(files);
    }

    private Collection<Class<U>> loadFromDirectory(File dir, Test<Class<U>> test, boolean recursive)
            throws IOException {
        Set<Class<U>> matches = new HashSet<Class<U>>();

        // Loop over the files
        Queue<File> files = new LinkedList<File>(safeListFiles(dir, null));
        while (!files.isEmpty()) {
            File file = files.poll();
            if (file.isDirectory() && recursive) {
                files.addAll(safeListFiles(file, null));
            } else if (file.isFile()) {
                // This file matches, test it
                Testable<Class<U>> testable = test.prepare(file);
                if (testable != null && testable.passes()) {
                    matches.add(testable.result());
                }
            }
        }

        return matches;
    }

    private Collection<Class<U>> loadFromJar(File f, Test<Class<U>> test, boolean recursive,
            Iterable<String> locators, boolean embeddable) throws IOException {
        Set<Class<U>> matches = new HashSet<Class<U>>();

        JarFile jarFile;
        try {
            jarFile = new JarFile(f);
        } catch (IOException e) {
            throw new IOException("Error opening JAR file [" + f.getAbsolutePath() + "]", e);
        }

        Enumeration<JarEntry> en = jarFile.entries();
        while (en.hasMoreElements()) {
            JarEntry entry = en.nextElement();
            String name = entry.getName();

            // Verify against the locators
            for (String locator : locators) {
                int index = name.indexOf(locator + "/");
                boolean match = (!embeddable && index == 0) || (embeddable && index >= 0);
                if (!match) {
                    continue;
                }

                match = recursive || name.indexOf('/', index + locator.length() + 1) == -1;
                if (!match) {
                    continue;
                }

                Testable<Class<U>> testable = test.prepare(f, jarFile, entry);
                if (testable != null && testable.passes()) {
                    matches.add(testable.result());
                    break;
                }
            }
        }

        jarFile.close();

        return matches;
    }

    /**
     * This is the testing interface that produces a testable object, given a file or JAR entry.
     */
    public static interface Test<T> {
        Testable<T> prepare(File file) throws IOException;

        Testable<T> prepare(File jar, JarFile jarFile, JarEntry jarEntry) throws IOException;
    }

    /**
     * This is the testing interface that is used to accept or reject resources.
     */
    public static interface Testable<T> {
        /**
         * @return True if the testable passes, false if it doesn't.
         */
        boolean passes();

        /**
         * @return The test result.
         */
        T result();
    }

    /**
     * Attempts to load the given file into a ClassReader.
     *
     * @param file The file to load.
     * @return The ClassReader and never null.
     * @throws IOException If the file doesn't point to a valid class.
     */
    public static ClassReader load(File file) throws IOException {
        try {
            return new ClassReader(new FileInputStream(file));
        } catch (IOException e) {
            throw new IOException("Error parsing class file at [" + file.getAbsolutePath() + "]", e);
        }
    }

    /**
     * Attempts to load the JarEntry into a ClassReader.
     *
     * @param jar      The JAR file that the entry is in.
     * @param jarFile  The JAR file used to get the InputStream to the entry.
     * @param jarEntry The JAR entry to load.
     * @return The ClassReader and never null.
     * @throws IOException If the JarEntry doesn't point to a valid class.
     */
    public static ClassReader load(File jar, JarFile jarFile, JarEntry jarEntry) throws IOException {
        try {
            return new ClassReader(jarFile.getInputStream(jarEntry));
        } catch (IOException e) {
            throw new IOException(
                    "Error parsing class file at [" + jar.getAbsolutePath() + "!/" + jarEntry.getName() + "]", e);
        }
    }

    /**
     * A Test that checks to see if each class is assignable to the provided class. Note that this test will match the
     * parent type itself if it is presented for matching.
     */
    public static class IsA<U> implements Test<Class<U>> {
        private final Class<U> parent;

        public IsA(Class<U> parent) {
            this.parent = parent;
        }

        public Testable<Class<U>> prepare(File file) throws IOException {
            if (!file.getName().endsWith(".class")) {
                return null;
            }
            return new IsATestable<U>(parent, load(file));
        }

        public Testable<Class<U>> prepare(File jar, JarFile jarFile, JarEntry jarEntry) throws IOException {
            if (!jarEntry.getName().endsWith(".class")) {
                return null;
            }
            return new IsATestable<U>(parent, load(jar, jarFile, jarEntry));
        }

        private static class IsATestable<U> implements Testable<Class<U>> {
            private final ClassReader classReader;
            private IsAClassVisitor<U> visitor;

            public IsATestable(Class<U> parent, ClassReader classReader) {
                this.visitor = new IsAClassVisitor<U>(parent);
                this.classReader = classReader;
            }

            public boolean passes() {
                classReader.accept(visitor, ClassReader.SKIP_CODE);
                return visitor.isPasses();
            }

            @SuppressWarnings("unchecked")
            public Class<U> result() {
                try {
                    return (Class<U>) Thread.currentThread().getContextClassLoader()
                            .loadClass(classReader.getClassName().replace('/', '.'));
                } catch (ClassNotFoundException e) {
                    throw new PrimeException(e);
                }
            }

            private static class IsAClassVisitor<U> extends ClassVisitor {
                private final Class<U> parent;
                private boolean passes;

                private IsAClassVisitor(Class<U> parent) {
                    super(Opcodes.ASM4);
                    this.parent = parent;
                }

                @Override
                public void visit(int version, int access, String name, String signature, String superName,
                        String[] interfaces) {
                    String parentInternalName = parent.getName().replace('.', '/');
                    passes = superName.equals(parentInternalName);
                    if (passes) {
                        return;
                    }

                    for (String anInterface : interfaces) {
                        passes = anInterface.equals(parentInternalName);
                        if (passes) {
                            break;
                        }
                    }
                    if (passes) {
                        return;
                    }

                    // Walk the inheritance chain
                    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                    if (!superName.equals("java/lang/Object")) {
                        try {
                            InputStream is = classLoader
                                    .getResourceAsStream(superName.replace('.', '/') + ".class");
                            ClassReader reader = new ClassReader(is);
                            reader.accept(this, ClassReader.SKIP_CODE);
                        } catch (IOException e) {
                            // Smother and move on
                        }
                    }
                    if (passes) {
                        return;
                    }

                    // Walk the inheritance and implementation chains
                    for (String anInterface : interfaces) {
                        try {
                            InputStream is = classLoader
                                    .getResourceAsStream(anInterface.replace('.', '/') + ".class");
                            ClassReader reader = new ClassReader(is);
                            reader.accept(this, ClassReader.SKIP_CODE);
                        } catch (IOException e) {
                            // Smother and move on
                        }
                        if (passes) {
                            return;
                        }
                    }
                }

                public boolean isPasses() {
                    return passes;
                }
            }
        }
    }

    /**
     * A Test that checks to see if each class is annotated with any of a number of annotations. If it is, then the test
     * returns true, otherwise false.
     */
    public static class AnnotatedWith<T extends Annotation, U> implements Test<Class<U>> {
        private final Class<T> annotation;

        public AnnotatedWith(Class<T> annotation) {
            this.annotation = annotation;
        }

        public Testable<Class<U>> prepare(File file) throws IOException {
            if (!file.getName().endsWith(".class")) {
                return null;
            }
            return new AnnotatedWithTestable<T, U>(annotation, load(file));
        }

        public Testable<Class<U>> prepare(File jar, JarFile jarFile, JarEntry jarEntry) throws IOException {
            if (!jarEntry.getName().endsWith(".class")) {
                return null;
            }
            return new AnnotatedWithTestable<T, U>(annotation, load(jar, jarFile, jarEntry));
        }

        private static class AnnotatedWithTestable<T extends Annotation, U> implements Testable<Class<U>> {
            private final ClassReader classReader;
            private AnnotatedWithClassVisitor<T> visitor;

            public AnnotatedWithTestable(Class<T> annotation, ClassReader classReader) {
                this.visitor = new AnnotatedWithClassVisitor<T>(annotation);
                this.classReader = classReader;
            }

            public boolean passes() {
                classReader.accept(visitor, ClassReader.SKIP_CODE);
                return visitor.isPasses();
            }

            @SuppressWarnings("unchecked")
            public Class<U> result() {
                try {
                    return (Class<U>) Thread.currentThread().getContextClassLoader()
                            .loadClass(classReader.getClassName().replace('/', '.'));
                } catch (ClassNotFoundException e) {
                    throw new PrimeException(e);
                }
            }

            private static class AnnotatedWithClassVisitor<T extends Annotation> extends ClassVisitor {
                private final Class<T> annotation;
                private boolean passes;

                private AnnotatedWithClassVisitor(Class<T> annotation) {
                    super(Opcodes.ASM4);
                    this.annotation = annotation;
                }

                @Override
                public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                    desc = desc.replaceAll("^L|;$", "");
                    desc = desc.replace('/', '.');

                    passes |= desc.equals(annotation.getName());
                    return null;
                }

                public boolean isPasses() {
                    return passes;
                }
            }
        }
    }
}