blob: bf5ed804d91d87f5208ae0caedc1b1551ef5f901 [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.camel.impl;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import org.apache.camel.impl.scan.AnnotatedWithAnyPackageScanFilter;
import org.apache.camel.impl.scan.AnnotatedWithPackageScanFilter;
import org.apache.camel.impl.scan.AssignableToPackageScanFilter;
import org.apache.camel.impl.scan.CompositePackageScanFilter;
import org.apache.camel.spi.PackageScanClassResolver;
import org.apache.camel.spi.PackageScanFilter;
import org.apache.camel.util.IOHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Default implement of {@link org.apache.camel.spi.PackageScanClassResolver}
*/
public class DefaultPackageScanClassResolver implements PackageScanClassResolver {
protected final transient Logger log = LoggerFactory.getLogger(getClass());
private final Set<ClassLoader> classLoaders = new LinkedHashSet<ClassLoader>();
private Set<PackageScanFilter> scanFilters;
private String[] acceptableSchemes = {};
public DefaultPackageScanClassResolver() {
try {
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl != null) {
if (log.isTraceEnabled()) {
log.trace("Adding ContextClassLoader from current thread: " + ccl);
}
classLoaders.add(ccl);
}
} catch (Exception e) {
// Ignore this exception
log.warn("Cannot add ContextClassLoader from current thread due " + e.getMessage() + ". This exception will be ignored.");
}
classLoaders.add(DefaultPackageScanClassResolver.class.getClassLoader());
}
public void addClassLoader(ClassLoader classLoader) {
classLoaders.add(classLoader);
}
public void addFilter(PackageScanFilter filter) {
if (scanFilters == null) {
scanFilters = new LinkedHashSet<PackageScanFilter>();
}
scanFilters.add(filter);
}
public void removeFilter(PackageScanFilter filter) {
if (scanFilters != null) {
scanFilters.remove(filter);
}
}
public void setAcceptableSchemes(String schemes) {
if (schemes != null) {
acceptableSchemes = schemes.split(";");
}
}
public boolean isAcceptableScheme(String urlPath) {
if (urlPath != null) {
for (String scheme : acceptableSchemes) {
if (urlPath.startsWith(scheme)) {
return true;
}
}
}
return false;
}
public Set<ClassLoader> getClassLoaders() {
// return a new set to avoid any concurrency issues in other runtimes such as OSGi
return Collections.unmodifiableSet(new LinkedHashSet<ClassLoader>(classLoaders));
}
public void setClassLoaders(Set<ClassLoader> classLoaders) {
// add all the class loaders
this.classLoaders.addAll(classLoaders);
}
@SuppressWarnings("unchecked")
public Set<Class<?>> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
if (packageNames == null) {
return Collections.EMPTY_SET;
}
if (log.isDebugEnabled()) {
log.debug("Searching for annotations of " + annotation.getName() + " in packages: " + Arrays.asList(packageNames));
}
PackageScanFilter test = getCompositeFilter(new AnnotatedWithPackageScanFilter(annotation, true));
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
for (String pkg : packageNames) {
find(test, pkg, classes);
}
if (log.isDebugEnabled()) {
log.debug("Found: " + classes);
}
return classes;
}
@SuppressWarnings("unchecked")
public Set<Class<?>> findAnnotated(Set<Class<? extends Annotation>> annotations, String... packageNames) {
if (packageNames == null) {
return Collections.EMPTY_SET;
}
if (log.isDebugEnabled()) {
log.debug("Searching for annotations of " + annotations + " in packages: " + Arrays.asList(packageNames));
}
PackageScanFilter test = getCompositeFilter(new AnnotatedWithAnyPackageScanFilter(annotations, true));
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
for (String pkg : packageNames) {
find(test, pkg, classes);
}
if (log.isDebugEnabled()) {
log.debug("Found: " + classes);
}
return classes;
}
@SuppressWarnings("unchecked")
public Set<Class<?>> findImplementations(Class parent, String... packageNames) {
if (packageNames == null) {
return Collections.EMPTY_SET;
}
if (log.isDebugEnabled()) {
log.debug("Searching for implementations of " + parent.getName() + " in packages: " + Arrays.asList(packageNames));
}
PackageScanFilter test = getCompositeFilter(new AssignableToPackageScanFilter(parent));
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
for (String pkg : packageNames) {
find(test, pkg, classes);
}
if (log.isDebugEnabled()) {
log.debug("Found: " + classes);
}
return classes;
}
@SuppressWarnings("unchecked")
public Set<Class<?>> findByFilter(PackageScanFilter filter, String... packageNames) {
if (packageNames == null) {
return Collections.EMPTY_SET;
}
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
for (String pkg : packageNames) {
find(filter, pkg, classes);
}
if (log.isDebugEnabled()) {
log.debug("Found: " + classes);
}
return classes;
}
protected void find(PackageScanFilter test, String packageName, Set<Class<?>> classes) {
packageName = packageName.replace('.', '/');
Set<ClassLoader> set = getClassLoaders();
for (ClassLoader classLoader : set) {
find(test, packageName, classLoader, classes);
}
}
protected void find(PackageScanFilter test, String packageName, ClassLoader loader, Set<Class<?>> classes) {
if (log.isTraceEnabled()) {
log.trace("Searching for: " + test + " in package: " + packageName + " using classloader: "
+ loader.getClass().getName());
}
Enumeration<URL> urls;
try {
urls = getResources(loader, packageName);
if (!urls.hasMoreElements()) {
log.trace("No URLs returned by classloader");
}
} catch (IOException ioe) {
log.warn("Cannot read package: " + packageName, ioe);
return;
}
while (urls.hasMoreElements()) {
URL url = null;
try {
url = urls.nextElement();
if (log.isTraceEnabled()) {
log.trace("URL from classloader: " + url);
}
url = customResourceLocator(url);
String urlPath = url.getFile();
urlPath = URLDecoder.decode(urlPath, "UTF-8");
if (log.isTraceEnabled()) {
log.trace("Decoded urlPath: " + urlPath + " with protocol: " + url.getProtocol());
}
// If it's a file in a directory, trim the stupid file: spec
if (urlPath.startsWith("file:")) {
// file path can be temporary folder which uses characters that the URLDecoder decodes wrong
// for example + being decoded to something else (+ can be used in temp folders on Mac OS)
// to remedy this then create new path without using the URLDecoder
try {
urlPath = new URI(url.getFile()).getPath();
} catch (URISyntaxException e) {
// fallback to use as it was given from the URLDecoder
// this allows us to work on Windows if users have spaces in paths
}
if (urlPath.startsWith("file:")) {
urlPath = urlPath.substring(5);
}
}
// osgi bundles should be skipped
if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
log.trace("It's a virtual osgi bundle, skipping");
continue;
}
// Else it's in a JAR, grab the path to the jar
if (urlPath.indexOf('!') > 0) {
urlPath = urlPath.substring(0, urlPath.indexOf('!'));
}
if (log.isTraceEnabled()) {
log.trace("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
}
File file = new File(urlPath);
if (file.isDirectory()) {
if (log.isTraceEnabled()) {
log.trace("Loading from directory using file: " + file);
}
loadImplementationsInDirectory(test, packageName, file, classes);
} else {
InputStream stream;
if (urlPath.startsWith("http:") || urlPath.startsWith("https:")
|| urlPath.startsWith("sonicfs:")
|| isAcceptableScheme(urlPath)) {
// load resources using http/https, sonicfs and other acceptable scheme
// sonic ESB requires to be loaded using a regular URLConnection
if (log.isTraceEnabled()) {
log.trace("Loading from jar using url: " + urlPath);
}
URL urlStream = new URL(urlPath);
URLConnection con = urlStream.openConnection();
// disable cache mainly to avoid jar file locking on Windows
con.setUseCaches(false);
stream = con.getInputStream();
} else {
if (log.isTraceEnabled()) {
log.trace("Loading from jar using file: " + file);
}
stream = new FileInputStream(file);
}
loadImplementationsInJar(test, packageName, stream, urlPath, classes);
}
} catch (IOException e) {
// use debug logging to avoid being to noisy in logs
log.debug("Cannot read entries in url: " + url, e);
}
}
}
// We can override this method to support the custom ResourceLocator
protected URL customResourceLocator(URL url) throws IOException {
// Do nothing here
return url;
}
/**
* Strategy to get the resources by the given classloader.
* <p/>
* Notice that in WebSphere platforms there is a {@link WebSpherePackageScanClassResolver}
* to take care of WebSphere's odditiy of resource loading.
*
* @param loader the classloader
* @param packageName the packagename for the package to load
* @return URL's for the given package
* @throws IOException is thrown by the classloader
*/
protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
if (log.isTraceEnabled()) {
log.trace("Getting resource URL for package: " + packageName + " with classloader: " + loader);
}
// If the URL is a jar, the URLClassloader.getResources() seems to require a trailing slash. The
// trailing slash is harmless for other URLs
if (!packageName.endsWith("/")) {
packageName = packageName + "/";
}
return loader.getResources(packageName);
}
private PackageScanFilter getCompositeFilter(PackageScanFilter filter) {
if (scanFilters != null) {
CompositePackageScanFilter composite = new CompositePackageScanFilter(scanFilters);
composite.addFilter(filter);
return composite;
}
return filter;
}
/**
* Finds matches in a physical directory on a filesystem. Examines all files
* within a directory - if the File object is not a directory, and ends with
* <i>.class</i> the file is loaded and tested to see if it is acceptable
* according to the Test. Operates recursively to find classes within a
* folder structure matching the package structure.
*
* @param test a Test used to filter the classes that are discovered
* @param parent the package name up to this directory in the package
* hierarchy. E.g. if /classes is in the classpath and we wish to
* examine files in /classes/org/apache then the values of
* <i>parent</i> would be <i>org/apache</i>
* @param location a File object representing a directory
*/
private void loadImplementationsInDirectory(PackageScanFilter test, String parent, File location, Set<Class<?>> classes) {
File[] files = location.listFiles();
StringBuilder builder = null;
for (File file : files) {
builder = new StringBuilder(100);
String name = file.getName();
if (name != null) {
name = name.trim();
builder.append(parent).append("/").append(name);
String packageOrClass = parent == null ? name : builder.toString();
if (file.isDirectory()) {
loadImplementationsInDirectory(test, packageOrClass, file, classes);
} else if (name.endsWith(".class")) {
addIfMatching(test, packageOrClass, classes);
}
}
}
}
/**
* Finds matching classes within a jar files that contains a folder
* structure matching the package structure. If the File is not a JarFile or
* does not exist a warning will be logged, but no error will be raised.
*
* @param test a Test used to filter the classes that are discovered
* @param parent the parent package under which classes must be in order to
* be considered
* @param stream the inputstream of the jar file to be examined for classes
* @param urlPath the url of the jar file to be examined for classes
*/
private void loadImplementationsInJar(PackageScanFilter test, String parent, InputStream stream, String urlPath, Set<Class<?>> classes) {
JarInputStream jarStream = null;
try {
jarStream = new JarInputStream(stream);
JarEntry entry;
while ((entry = jarStream.getNextJarEntry()) != null) {
String name = entry.getName();
if (name != null) {
name = name.trim();
if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
addIfMatching(test, name, classes);
}
}
}
} catch (IOException ioe) {
log.warn("Cannot search jar file '" + urlPath + "' for classes matching criteria: " + test
+ " due to an IOException: " + ioe.getMessage(), ioe);
} finally {
IOHelper.close(jarStream, urlPath, log);
}
}
/**
* Add the class designated by the fully qualified class name provided to
* the set of resolved classes if and only if it is approved by the Test
* supplied.
*
* @param test the test used to determine if the class matches
* @param fqn the fully qualified name of a class
*/
protected void addIfMatching(PackageScanFilter test, String fqn, Set<Class<?>> classes) {
try {
String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
Set<ClassLoader> set = getClassLoaders();
boolean found = false;
for (ClassLoader classLoader : set) {
if (log.isTraceEnabled()) {
log.trace("Testing for class " + externalName + " matches criteria [" + test + "] using classloader:" + classLoader);
}
try {
Class<?> type = classLoader.loadClass(externalName);
if (log.isTraceEnabled()) {
log.trace("Loaded the class: " + type + " in classloader: " + classLoader);
}
if (test.matches(type)) {
if (log.isTraceEnabled()) {
log.trace("Found class: " + type + " which matches the filter in classloader: " + classLoader);
}
classes.add(type);
}
found = true;
break;
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled()) {
log.trace("Cannot find class '" + fqn + "' in classloader: " + classLoader
+ ". Reason: " + e, e);
}
} catch (NoClassDefFoundError e) {
if (log.isTraceEnabled()) {
log.trace("Cannot find the class definition '" + fqn + "' in classloader: " + classLoader
+ ". Reason: " + e, e);
}
}
}
if (!found) {
if (log.isDebugEnabled()) {
// use debug to avoid being noisy in logs
log.debug("Cannot find class '" + fqn + "' in any classloaders: " + set);
}
}
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Cannot examine class '" + fqn + "' due to a " + e.getClass().getName()
+ " with message: " + e.getMessage(), e);
}
}
}
}