/*
* Copyright 2012 Netflix, Inc.
*
* 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.netflix.governator.lifecycle;
import static org.objectweb.asm.ClassReader.SKIP_CODE;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.netflix.governator.internal.scanner.ClasspathUrlDecoder;
import com.netflix.governator.internal.scanner.DirectoryClassFilter;
import org.objectweb.asm.ClassReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Utility to find annotated classes
*/
public class ClasspathScanner {
private static final Logger log = LoggerFactory.getLogger(ClasspathScanner.class);
protected final ClassLoader classLoader;
private final Set<Class<?>> classes;
private final Set<Constructor> constructors;
private final Set<Method> methods;
private final Set<Field> fields;
/**
* @param basePackages list of packages to search (recursively)
* @param annotations class annotations to search for
*/
public ClasspathScanner(Collection<String> basePackages, Collection<Class<? extends Annotation>> annotations) {
this(basePackages, annotations, Thread.currentThread().getContextClassLoader());
}
/**
* @param basePackages list of packages to search (recursively)
* @param annotations class annotations to search for
* @param classLoader ClassLoader containing the classes to be scanned
*/
public ClasspathScanner(Collection<String> basePackages, Collection<Class<? extends Annotation>> annotations, final ClassLoader classLoader) {
Preconditions.checkNotNull(annotations, "annotations cannot be null");
Preconditions.checkNotNull(classLoader, "classLoader cannot be null");
log.debug("Starting classpath scanning...");
this.classLoader = classLoader;
Set<Class<?>> localClasses = Sets.newHashSet();
Set<Constructor> localConstructors = Sets.newHashSet();
Set<Method> localMethods = Sets.newHashSet();
Set<Field> localFields = Sets.newHashSet();
doScanning(basePackages, annotations, localClasses, localConstructors, localMethods, localFields);
classes = ImmutableSet.copyOf(localClasses);
constructors = ImmutableSet.copyOf(localConstructors);
methods = ImmutableSet.copyOf(localMethods);
fields = ImmutableSet.copyOf(localFields);
log.debug("Classpath scanning done");
}
/**
* @return the found classes
*/
public Set<Class<?>> getClasses() {
return classes;
}
public Set<Constructor> getConstructors() {
return constructors;
}
public Set<Method> getMethods() {
return methods;
}
public Set<Field> getFields() {
return fields;
}
protected void doScanning(Collection<String> basePackages, Collection<Class<? extends Annotation>> annotations, Set<Class<?>> localClasses, Set<Constructor> localConstructors, Set<Method> localMethods, Set<Field> localFields) {
if ( basePackages.isEmpty() ) {
log.warn("No base packages specified - no classpath scanning will be done");
return;
}
log.info("Scanning packages : " + basePackages + " for annotations " + annotations);
for ( String basePackage : basePackages ) {
try {
String basePackageWithSlashes = basePackage.replace(".", "/");
Enumeration<URL> resources = classLoader.getResources(basePackageWithSlashes);
while ( resources.hasMoreElements() ) {
URL url = resources.nextElement();
try {
if ( isJarURL(url)) {
String jarPath = url.getFile();
if ( jarPath.contains("!") ) {
jarPath = jarPath.substring(0, jarPath.indexOf("!"));
url = new URL(jarPath);
}
File file = ClasspathUrlDecoder.toFile(url);
try (JarFile jar = new JarFile(file)) {
for ( Enumeration<JarEntry> list = jar.entries(); list.hasMoreElements(); ) {
JarEntry entry = list.nextElement();
try {
if ( entry.getName().endsWith(".class") && entry.getName().startsWith(basePackageWithSlashes)) {
AnnotationFinder finder = new AnnotationFinder(classLoader, annotations);
new ClassReader(jar.getInputStream(entry)).accept(finder, SKIP_CODE);
applyFinderResults(localClasses, localConstructors, localMethods, localFields, finder);
}
}
catch (Exception e) {
log.debug("Unable to scan JarEntry '{}' in '{}'. {}", new Object[]{entry.getName(), file.getCanonicalPath(), e.getMessage()});
}
}
}
catch (Exception e ) {
log.debug("Unable to scan '{}'. {}", new Object[]{file.getCanonicalPath(), e.getMessage()});
}
}
else {
DirectoryClassFilter filter = new DirectoryClassFilter(classLoader);
for ( String className : filter.filesInPackage(url, basePackage) ) {
AnnotationFinder finder = new AnnotationFinder(classLoader, annotations);
new ClassReader(filter.bytecodeOf(className)).accept(finder, SKIP_CODE);
applyFinderResults(localClasses, localConstructors, localMethods, localFields, finder);
}
}
}
catch (Exception e) {
log.debug("Unable to scan jar '{}'. {} ", new Object[]{url, e.getMessage()});
}
}
}
catch ( Exception e ) {
throw new RuntimeException("Classpath scanning failed for package \'" + basePackage + "\'", e);
}
}
}
private void applyFinderResults(Set<Class<?>> localClasses, Set<Constructor> localConstructors, Set<Method> localMethods, Set<Field> localFields, AnnotationFinder finder) {
for (Class<?> cls : finder.getAnnotatedClasses()) {
if (localClasses.contains(cls)) {
log.debug(String.format("Duplicate class found for '%s'", cls.getCanonicalName()));
}
else {
localClasses.add(cls);
}
}
for (Method method : finder.getAnnotatedMethods()) {
if (localMethods.contains(method)) {
log.debug(String.format("Duplicate method found for '%s:%s'", method.getClass().getCanonicalName(), method.getName()));
}
else {
localMethods.add(method);
}
}
for (Constructor<?> ctor : finder.getAnnotatedConstructors()) {
if (localConstructors.contains(ctor)) {
log.debug(String.format("Duplicate constructor found for '%s:%s'", ctor.getClass().getCanonicalName(), ctor.toString()));
}
else {
localConstructors.add(ctor);
}
}
for (Field field : finder.getAnnotatedFields()) {
if (localFields.contains(field)) {
log.debug(String.format("Duplicate field found for '%s:%s'", field.getClass().getCanonicalName(), field.toString()));
}
else {
localFields.add(field);
}
}
}
private boolean isJarURL(URL url) {
String protocol = url.getProtocol();
return "zip".equals(protocol) || "jar".equals(protocol) ||
("file".equals(protocol) && url.getPath().endsWith(".jar"));
}
}