/* * 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; } } } } }