blob: ba464ecc47308565a299240f3f7ebedebd3446a8 [file] [log] [blame]
/*
* 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 org.apache.openejb.util;
import org.apache.xbean.asm.AnnotationVisitor;
import org.apache.xbean.asm.Attribute;
import org.apache.xbean.asm.ClassReader;
import org.apache.xbean.asm.ClassVisitor;
import org.apache.xbean.asm.FieldVisitor;
import org.apache.xbean.asm.MethodVisitor;
import org.apache.xbean.finder.UrlSet;
import org.apache.xbean.finder.archive.FileArchive;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
/**
* ClassFinder searches the classpath of the specified classloader for
* packages, classes, constructors, methods, or fields with specific annotations.
*
* For security reasons ASM is used to find the annotations. Classes are not
* loaded unless they match the requirements of a called findAnnotated* method.
* Once loaded, these classes are cached.
*
* The getClassesNotLoaded() method can be used immediately after any find*
* method to get a list of classes which matched the find requirements (i.e.
* contained the annotation), but were unable to be loaded.
*
* @version $Rev: 1423884 $ $Date: 2012-12-19 15:28:37 +0000 (Wed, 19 Dec 2012) $
*/
public class AnnotationFinder {
private final ClassLoader classLoader;
private final List<String> classesNotLoaded = new ArrayList<String>();
private final int ASM_FLAGS = ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES;
private List<String> classNames;
/**
* Creates a ClassFinder that will search the urls in the specified classloader
* excluding the urls in the classloader's parent.
*
* To include the parent classloader, use:
*
* new ClassFinder(classLoader, false);
*
* To exclude the parent's parent, use:
*
* new ClassFinder(classLoader, classLoader.getParent().getParent());
*
* @param classLoader source of classes to scan
* @throws Exception if something goes wrong
*/
public AnnotationFinder(final ClassLoader classLoader) throws Exception {
this(classLoader, true);
}
/**
* Creates a ClassFinder that will search the urls in the specified classloader.
*
* @param classLoader source of classes to scan
* @param excludeParent Allegedly excludes classes from parent classloader, whatever that might mean
* @throws Exception if something goes wrong.
*/
public AnnotationFinder(final ClassLoader classLoader, final boolean excludeParent) throws Exception {
this(classLoader, AnnotationFinder.getUrls(classLoader, excludeParent));
}
/**
* Creates a ClassFinder that will search the urls in the specified classloader excluding
* the urls in the 'exclude' classloader.
*
* @param classLoader source of classes to scan
* @param exclude source of classes to exclude from scanning
* @throws Exception if something goes wrong
*/
public AnnotationFinder(final ClassLoader classLoader, final ClassLoader exclude) throws Exception {
this(classLoader, AnnotationFinder.getUrls(classLoader, exclude));
}
public AnnotationFinder(final ClassLoader classLoader, final URL url) {
this(classLoader, Arrays.asList(url));
}
public AnnotationFinder(final ClassLoader classLoader, final Collection<URL> urls) {
this.classLoader = classLoader;
classNames = new ArrayList<String>();
for (final URL location : urls) {
if (location == null) {
continue;
}
try {
if (location.getProtocol().equals("jar")) {
classNames.addAll(jar(location));
} else if (location.getProtocol().equals("file")) {
try {
// See if it's actually a jar
final URL jarUrl = new URL("jar", "", location.toExternalForm() + "!/");
final JarURLConnection juc = (JarURLConnection) jarUrl.openConnection();
juc.getJarFile();
classNames.addAll(jar(jarUrl));
} catch (IOException e) {
classNames.addAll(file(location));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Returns a list of classes that could not be loaded in last invoked findAnnotated* method.
* <p/>
* The list will only contain entries of classes whose byte code matched the requirements
* of last invoked find* method, but were unable to be loaded and included in the results.
* <p/>
* The list returned is unmodifiable. Once obtained, the returned list will be a live view of the
* results from the last findAnnotated* method call.
* <p/>
* This method is not thread safe.
* @return an unmodifiable live view of classes that could not be loaded in previous findAnnotated* call.
*/
public List<String> getClassesNotLoaded() {
return Collections.unmodifiableList(classesNotLoaded);
}
public boolean find(final Filter filter){
final Visitor annotationVisitor = new Visitor(filter);
for (final String className : classNames) {
try {
readClassDef(className, annotationVisitor);
} catch (NotFoundException e) {
// no-op
} catch (FoundException e) {
return true;
}
}
return false;
}
public interface Filter {
boolean accept(String annotationName);
}
private static Collection<URL> getUrls(final ClassLoader classLoader, final boolean excludeParent) throws IOException {
return AnnotationFinder.getUrls(classLoader, excludeParent? classLoader.getParent() : null);
}
private static Collection<URL> getUrls(final ClassLoader classLoader, final ClassLoader excludeParent) throws IOException {
UrlSet urlSet = new UrlSet(classLoader);
if (excludeParent != null){
urlSet = urlSet.exclude(excludeParent);
}
return urlSet.getUrls();
}
private List<String> file(final URL location) {
final List<String> classNames = new ArrayList<String>();
File dir = null;
try {
dir = new File(URLDecoder.decode(location.getPath(),"UTF-8"));
} catch (Exception e) {
dir = new File(URLDecoder.decode(location.getPath()));
}
if (dir.getName().equals("META-INF")) {
dir = dir.getParentFile(); // Scrape "META-INF" off
}
if (dir.isDirectory()) {
scanDir(dir, classNames, "");
}
return classNames;
}
private void scanDir(final File dir, final List<String> classNames, final String packageName) {
final File[] files = dir.listFiles();
if (files != null) {
for (final File file : files) {
if (file.isDirectory()) {
scanDir(file, classNames, packageName + file.getName() + ".");
} else if (file.getName().endsWith(".class")) {
String name = file.getName();
name = name.replaceFirst(".class$", "");
if (name.contains(".")) continue;
classNames.add(packageName + name);
}
}
}
}
private List<String> jar(final URL location) throws IOException {
String jarPath = location.getFile();
if (jarPath.contains("!")){
jarPath = jarPath.substring(0, jarPath.indexOf("!"));
}
final URL url = new URL(jarPath);
if ("file".equals(url.getProtocol())) { // ZipFile is faster than ZipInputStream
final JarFile jarFile = new JarFile(url.getFile().replace(" ", "%20"));
return jar(jarFile);
} else {
InputStream in = url.openStream();
in = new BufferedInputStream(in);
try {
final JarInputStream jarStream = new JarInputStream(in);
return jar(jarStream);
} finally {
in.close();
}
}
}
private List<String> jar(final JarFile jarFile) {
final List<String> classNames = new ArrayList<String>();
final Enumeration<? extends JarEntry> jarEntries =jarFile.entries();
while (jarEntries.hasMoreElements()) {
final JarEntry entry = jarEntries.nextElement();
addClassName(classNames, entry);
}
return classNames;
}
private List<String> jar(final JarInputStream jarStream) throws IOException {
final List<String> classNames = new ArrayList<String>();
JarEntry entry;
while ((entry = jarStream.getNextJarEntry()) != null) {
addClassName(classNames, entry);
}
return classNames;
}
private void addClassName(final List<String> classNames, final JarEntry entry) {
if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
return;
}
String className = entry.getName();
className = className.replaceFirst(".class$", "");
if (className.contains(".")) {
return;
}
className = className.replace(File.separatorChar, '.');
classNames.add(className);
}
private void readClassDef(String className, final ClassVisitor visitor) {
classes++;
if (!className.endsWith(".class")) {
className = className.replace('.', '/') + ".class";
}
try {
final URL resource = classLoader.getResource(className);
if (resource != null) {
InputStream in = resource.openStream();
in = new BufferedInputStream(in);
try {
final ClassReader classReader = new ClassReader(in);
classReader.accept(visitor, ASM_FLAGS);
} finally {
in.close();
}
} else {
new Exception("Could not load " + className).printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static int classes;
public static class NotFoundException extends RuntimeException {
}
public static class FoundException extends RuntimeException {
}
public class Visitor implements ClassVisitor {
private NotFoundException notFoundException;
private FoundException foundException;
private final Filter filter;
public Visitor(final Filter filter) {
this.filter = filter;
try {
throw new NotFoundException();
} catch (NotFoundException e) {
notFoundException = e;
}
try {
throw new FoundException();
} catch (FoundException e) {
foundException = e;
}
}
@Override
public AnnotationVisitor visitAnnotation(String name, final boolean visible) {
// annotation names show up as
// Ljavax.ejb.Stateless;
// so we hack of the first and last chars and replace the slashes
final StringBuilder sb = new StringBuilder(name);
sb.deleteCharAt(0);
sb.deleteCharAt(sb.length()-1);
for (int i = 0; i < sb.length(); i++) {
if (sb.charAt(i) == '/'){
sb.setCharAt(i, '.');
}
}
name = sb.toString();
if (filter.accept(name)){
throw foundException;
}
return null;
}
@Override
public void visit(final int i, final int i1, final String string, final String string1, final String string2, final String[] strings) {
}
@Override
public void visitSource(final String string, final String string1) {
}
@Override
public void visitOuterClass(final String string, final String string1, final String string2) {
}
@Override
public void visitAttribute(final Attribute attribute) {
throw notFoundException;
}
@Override
public void visitInnerClass(final String string, final String string1, final String string2, final int i) {
throw notFoundException;
}
@Override
public FieldVisitor visitField(final int i, final String string, final String string1, final String string2, final Object object) {
throw notFoundException;
}
@Override
public MethodVisitor visitMethod(final int i, final String string, final String string1, final String string2, final String[] strings) {
throw notFoundException;
}
@Override
public void visitEnd() {
throw notFoundException;
}
}
}