package net.sourceforge.javautil.common.classloader;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import net.sourceforge.javautil.common.ClassNameUtil;
import net.sourceforge.javautil.common.ReflectionUtil;
import net.sourceforge.javautil.common.exception.ThrowableManagerRegistry;
import net.sourceforge.javautil.common.io.IVirtualArtifact;
import net.sourceforge.javautil.common.io.IVirtualDirectory;
import net.sourceforge.javautil.common.io.VirtualDirectoryVisitorContext;
import net.sourceforge.javautil.common.io.IVirtualFile;
import net.sourceforge.javautil.common.io.IVirtualPath;
import net.sourceforge.javautil.common.io.VirtualArtifactURLStreamHandler.VirtualArtifactURLConnection;
import net.sourceforge.javautil.common.io.impl.ArtifactCollector;
import net.sourceforge.javautil.common.io.impl.SimplePath;
import net.sourceforge.javautil.common.io.impl.SystemDirectory;
import net.sourceforge.javautil.common.io.impl.SystemFile;
import net.sourceforge.javautil.common.io.impl.ZippedDirectory;
import net.sourceforge.javautil.common.proxy.CollectionSourceProxy;
import net.sourceforge.javautil.common.proxy.CollectionTargetProxy;
import net.sourceforge.javautil.common.visitor.IVisitable;
import net.sourceforge.javautil.common.visitor.IVisitorContext;
/**
* This will allow for standardized and customizable scanning of the class loader
* and related resources.
*
* @author elponderador
* @author $Author: ponderator $
* @version $Id: ClassLoaderScanner.java 2554 2010-11-12 03:37:00Z ponderator $
*/
public class ClassLoaderScanner implements IVisitable<ClassLoaderVisitor> {
/**
* @param <T> The type of object generated by the factory
* @param resourceName The resource for java archive detection
* @param patterns The patterns (patterns can start with a - to indicate exclusion)
* @return A list, possibly empty, of detected resources and generated objects
*/
public static <T> List<T> detect (String resourceName, IResourceObjectFactory<T> factory, String... patterns) {
ClassLoaderScanner scanner = new ClassLoaderScanner();
scanner.setDirectoryResourceName(resourceName);
for (String pattern : patterns) {
if (pattern.startsWith("-")) {
scanner.addExclusion(Pattern.compile(pattern.substring(1)));
} else {
if (pattern.startsWith("+")) pattern = pattern.substring(1);
scanner.addInclusion(Pattern.compile(pattern));
}
}
List<T> generated = new ArrayList<T>();
for (ClassLoaderResource rsc : scanner.getResources()) {
generated.add( factory.generate(rsc) );
}
return generated;
}
/**
* @param <T> The type the resources must extend
* @param resourceName The resource for java archive detection
* @param iface The type's class
* @param patterns The patterns (patterns can start with a - to indicate exclusion)
* @return A proxy that will target instances of all types found, or null if none could be located
*/
public static <T> List<T> detect (String resourceName, Class<T> iface, String... patterns) {
ClassLoaderScanner scanner = new ClassLoaderScanner();
scanner.setDirectoryResourceName(resourceName);
for (String pattern : patterns) {
if (pattern.startsWith("-")) {
scanner.addExclusion(Pattern.compile(pattern.substring(1)));
} else {
if (pattern.startsWith("+")) pattern = pattern.substring(1);
scanner.addInclusion(Pattern.compile(pattern));
}
}
return scanner.getClasses(iface).getInstances(iface);
}
/**
* This will load the class specified with the current thread-context class loader.
*
* @see #getResource(Class)
*/
public static ClassLoaderResource getClassResource (String className) { return getClassResource(ReflectionUtil.getClass(className)); }
/**
* @param clazz The clazz
* @return A resource wrapper for accessing the origin of the class
*/
public static ClassLoaderResource getClassResource (Class clazz) {
URL url = clazz.getResource("/" + ClassNameUtil.toRelativeClassPath(clazz.getName()));
IVirtualDirectory archive = createFor(url, ClassNameUtil.toRelativeClassPath(clazz.getName()));
return new ClassLoaderResource(clazz.getClassLoader(), new SimplePath(ClassNameUtil.toRelativeClassPath(clazz.getName())), archive);
}
/**
* @param name The resource name
* @return A resource wrapper for accessing the origin of the resource, or null if no resource could be found
*/
public static ClassLoaderResource getResource (String name) {
URL url = Thread.currentThread().getContextClassLoader().getResource(name);
if (url == null) return null;
IVirtualDirectory archive = createFor(url, name);
return new ClassLoaderResource(Thread.currentThread().getContextClassLoader(), new SimplePath(name), archive);
}
/**
* @param url The URL for which to create a parent directory
* @param resourceName The resource name that was found in relation to the URL
* @return The virtual directory implementation
*/
public static IVirtualDirectory createFor (URL url, String resourceName) {
try {
String urlPath = url.getFile();
urlPath = URLDecoder.decode(urlPath, "UTF-8");
if (resourceName != null)
urlPath = urlPath.substring(0, urlPath.length() - resourceName.length());
if ( urlPath.startsWith("vas:") || urlPath.startsWith("virtual:") ) {
try {
URLConnection connection = new URL(urlPath).openConnection();
if (connection instanceof VirtualArtifactURLConnection) {
IVirtualArtifact artifact = ((VirtualArtifactURLConnection)connection).getArtifact();
if (artifact instanceof IVirtualDirectory) return (IVirtualDirectory) artifact;
}
} catch (IOException e) {
throw ThrowableManagerRegistry.caught(e);
}
}
if ( urlPath.startsWith("file:") ) urlPath = urlPath.substring(5);
if ( urlPath.indexOf('!')>0 ) urlPath = urlPath.substring(0, urlPath.indexOf('!'));
File file = new File(urlPath);
return file.isDirectory() ? new SystemDirectory(file) : new ZippedDirectory(new SystemFile(file));
} catch (UnsupportedEncodingException e) {
throw ThrowableManagerRegistry.caught(e);
}
}
/**
* @param <T> The type of proxy
* @param resourceName The resource for java archive detection
* @param iface The type's class
* @param patterns The patterns (patterns can start with a - to indicate exclusion)
* @return A proxy that will target instances of all types found, or null if none could be located
*/
public static <T> T createAutoDetectedCompositeProxy (String resourceName, Class<T> iface, String... patterns) {
List<T> instances = detect(resourceName, iface, patterns);
if (Comparable.class.isAssignableFrom(iface)) Collections.sort((List<? extends Comparable>)instances);
return instances.size() == 0 ? null : CollectionSourceProxy.create(iface, instances);
}
protected final ClassLoader loader;
protected String directoryResourceName;
protected List<Pattern> inclusion = new ArrayList<Pattern>();
protected List<Pattern> exclusion = new ArrayList<Pattern>();
protected List<Pattern> inclusionPaths = new ArrayList<Pattern>();
protected List<Pattern> exclusionPaths = new ArrayList<Pattern>();
protected Boolean collectionDefault;
public ClassLoaderScanner() { this(Thread.currentThread().getContextClassLoader()); }
public ClassLoaderScanner(ClassLoader loader) {
this.loader = loader;
}
public <I extends ClassLoaderVisitor> I accept(I visitor) {
new ClassLoaderVisitorContext(this).visit(visitor, loader); return visitor;
}
/**
* This can be used to override the default logic for artifact collection which
* assumes that if there is at least one inclusion pattern, then artifacts are
* by default excluded, whereas if there are only exclusion patterns then artifacts
* are collected by default.
*
* @return True if artifacts are to be collected by default, false if not, or null if default logic is to be followed
*/
public Boolean getCollectionDefault() { return collectionDefault; }
public void setCollectionDefault(Boolean collectionDefault) { this.collectionDefault = collectionDefault; }
/**
* @return The resource name used to filter which class archives are searched, otherwise null if all should be searched
*/
public String getDirectoryResourceName() { return directoryResourceName; }
public ClassLoaderScanner setDirectoryResourceName(String directoryResourceName) { this.directoryResourceName = directoryResourceName; return this; }
/**
* @param inclusion The pattern specifying resources to include
* @return This for chaining
*/
public ClassLoaderScanner addInclusion (Pattern inclusion) { this.inclusion.add(inclusion); return this; }
/**
* @param exclusion The pattern specifying resources to exclude
* @return This for chaining
*/
public ClassLoaderScanner addExclusion (Pattern exclusion) { this.exclusion.add(exclusion); return this; }
/**
* @param inclusion The pattern specifying resources to include
* @return This for chaining
*/
public ClassLoaderScanner addPathInclusion (Pattern inclusion) { this.inclusionPaths.add(inclusion); return this; }
/**
* @param exclusion The pattern specifying resources to exclude
* @return This for chaining
*/
public ClassLoaderScanner addPathExclusion (Pattern exclusion) { this.exclusionPaths.add(exclusion); return this; }
/**
* @return The first resource found according to the current criteria, or null if none found
*/
public ClassLoaderResource getFirstResource () {
return this.accept( new ClassLoaderVisitor<ClassLoader>(true) ).getFirstResource();
}
/**
* @return The resources found according to the current criteria, maybe an empty list
*/
public List<ClassLoaderResource> getResources () {
return this.accept( new ClassLoaderVisitor<ClassLoader>(false) ).getResources();
}
/**
* @param archive The archive to search
* @return The resources that were detected in the specified archive
*/
public List<ClassLoaderResource> getResources (IVirtualDirectory archive) {
ClassLoaderVisitor visitor = new ClassLoaderVisitor(false);
this.visitResources(archive, visitor);
return visitor.getResources();
}
/**
* @param archive The archive to search
* @param visitor The visitor that will visit each resource
*/
public void visitResources (IVirtualDirectory archive, ClassLoaderVisitor<?> visitor) {
this.visitResources(archive, new ClassLoaderVisitorContext(this), visitor);
}
/**
* @param archive The archive for which resources are desired
* @param ctx The context in which to visit the resources
* @param visitor The visitor that will visit each resource
*
* @see #getArchives()
*/
public void visitResources (final IVirtualDirectory archive, final ClassLoaderVisitorContext ctx, final ClassLoaderVisitor<?> visitor) {
archive.accept( this.createCollector(ctx, visitor, archive) );
}
/**
* This will call {@link #getResources()} from which it will extract the requested classes.
*
* @see #getClasses(List, Class...)
*/
public ClassLoaderClassGroup getClasses (Class... types) { return this.getClasses(this.getResources(), types); }
/**
* @param types The types of classes that need to be extracted. If a requested type is not in the
* returned hash, it is because no classes of that type were found. The types should be declared in
* less specific order.
*
* @return The map of super class <-> found concrete/extended classes
*/
public ClassLoaderClassGroup getClasses (List<ClassLoaderResource> rscs, Class... types) {
ClassLoaderClassGroup group = new ClassLoaderClassGroup();
for (int r=0; r<rscs.size(); r++) {
ClassLoaderResource rsc = rscs.get(r);
if (rsc.isClassFile()) {
try {
Class clazz = rsc.loadClass();
for (Class type : types) if (type.isAssignableFrom(clazz)) {
if (!group.classes.containsKey(type)) { group.classes.put(type, new ArrayList<Class<?>>()); }
group.classes.get(type).add(clazz);
break;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
return group;
}
/**
* Facility method for extracting a list of a single type.
*
* @see #getClasses(List, Class...)
*/
public <T> List<Class<T>> getClassList (List<ClassLoaderResource> rscs, Class<T> type) {
return getClasses(rscs, type).getClasses(type);
}
/**
* This will be called by the {@link ClassLoaderVisitorContext}.
*
* @return An iterator for providing access to {@link IVirtualDirectory} implementations for accessing the internal resources
*/
public Iterator<? extends IVirtualDirectory> getArchives (ClassLoader cl) {
try {
String name = this.directoryResourceName == null ? "/" : this.directoryResourceName;
return new DirectoryIterator( cl.getResources( name ), name );
} catch (IOException e) {
throw ThrowableManagerRegistry.caught(e);
}
}
/**
* @return The archives that would be searched
*
* @see #getDirectoryResourceName()
*/
public Iterator<? extends IVirtualDirectory> getArchives () {
return this.getArchives(loader);
}
/**
* This will be called by the {@link ClassLoaderVisitorContext}.
*
* @param directory The directory for which to create a collector
* @return The collector to use for visiting the directory
*
* @see #getCollectionDefault()
*/
public ArtifactCollector<IVirtualFile> createCollector (final ClassLoaderVisitorContext clvctx, final ClassLoaderVisitor<?> visitor, IVirtualDirectory directory) {
ArtifactCollector<IVirtualFile> collector = new ArtifactCollector<IVirtualFile>() {
@Override public CollectResult collect(VirtualDirectoryVisitorContext ctx) {
CollectResult result = super.collect(ctx);
if (result == CollectResult.INCLUDED || isCollectionDefault()) {
clvctx.setVisited(new ClassLoaderResource(loader, ctx.getVisitable().getRelativePath(ctx.getVisited()), ctx.getVisitable()));
visitor.visit(clvctx);
if (clvctx.isSkipped()) ctx.skip();
else if (clvctx.isAborted()) ctx.abort();
}
return result;
}
}.setIncludeDirectories(false);
for (Pattern inclusion : this.inclusion) collector.addInclusionPatternFilter(inclusion);
for (Pattern exclusion : this.exclusion) collector.addExclusionPatternFilter(exclusion);
for (Pattern inclusion : this.inclusionPaths) collector.addInclusionPathPatternFilter(inclusion);
for (Pattern exclusion : this.exclusionPaths) collector.addExclusionPathPatternFilter(exclusion);
collector.setCollectionDefault( this.collectionDefault == null ?
this.inclusion.size() == 0 && this.inclusionPaths.size() == 0 : this.collectionDefault );
return collector;
}
/**
* This will iterator over URL's for which {@link IVirtualDirectory} implementations
* can be generated, but only creating them when needed.
*
* @author elponderador
* @author $Author: ponderator $
* @version $Id: ClassLoaderScanner.java 2554 2010-11-12 03:37:00Z ponderator $
*/
private class DirectoryIterator implements Iterator<IVirtualDirectory> {
protected final Enumeration<URL> urls;
protected final String resourceName;
public DirectoryIterator(Enumeration<URL> urls, String resourceName) {
this.urls = urls;
this.resourceName = resourceName;
}
public boolean hasNext() { return urls.hasMoreElements(); }
public IVirtualDirectory next() { return createFor(urls.nextElement(), this.resourceName); }
public void remove() { throw new UnsupportedOperationException(); }
}
}