package xapi.dev.scanner.impl; import java.io.File; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Target; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.regex.Pattern; import xapi.bytecode.ClassFile; import xapi.bytecode.ClassPool; import xapi.bytecode.FieldInfo; import xapi.bytecode.MemberInfo; import xapi.bytecode.MethodInfo; import xapi.bytecode.NotFoundException; import xapi.collect.api.Fifo; import xapi.collect.impl.MultithreadedStringTrie; import xapi.collect.impl.SimpleFifo; import xapi.dev.resource.api.ClasspathResource; import xapi.dev.resource.impl.ByteCodeResource; import xapi.dev.resource.impl.SourceCodeResource; import xapi.dev.resource.impl.StringDataResource; import xapi.log.X_Log; import xapi.source.X_Source; import xapi.util.X_Debug; import xapi.util.api.MatchesValue; import xapi.util.api.ProvidesValue; public class ClasspathResourceMap { private final ResourceTrie<ByteCodeResource> bytecode; private final ResourceTrie<SourceCodeResource> sources; private final ResourceTrie<StringDataResource> resources; private final Set<Class<? extends Annotation>> annotations; private final Set<Pattern> bytecodeMatchers; private final Set<Pattern> resourceMatchers; private final Set<Pattern> sourceMatchers; private final Fifo<ByteCodeResource> pending; private final ProvidesValue<ExecutorService> executor; private AnnotatedClassIterator allAnnos; private AnnotatedMethodIterator allMethodsWithAnnos; private ClassPool pool; private ArrayList<URL> classpath; public ClasspathResourceMap(final ProvidesValue<ExecutorService> executor, final Set<Class<? extends Annotation>> annotations, final Set<Pattern> bytecodeMatchers, final Set<Pattern> resourceMatchers, final Set<Pattern> sourceMatchers) { this.annotations = annotations; this.bytecodeMatchers = bytecodeMatchers; this.executor = executor; this.resourceMatchers = resourceMatchers; this.sourceMatchers = sourceMatchers; this.bytecode = new ResourceTrie<ByteCodeResource>(); this.sources = new ResourceTrie<SourceCodeResource>(); this.resources = new ResourceTrie<StringDataResource>(); this.pending = new SimpleFifo<ByteCodeResource>(); } public void addBytecode(final String name, final ByteCodeResource bytecode) { this.bytecode.put(X_Source.stripClassExtension(name.replace(File.separatorChar, '.')), bytecode); if (!preloadClasses()) { return; } if (pending.isEmpty()) { synchronized (pending) { // double-checked lock if (pending.isEmpty()) { getExecutor().submit(new Runnable() { // We use one thread to iterate and preload class files @Override public void run() { while (!pending.isEmpty()) { final Iterator<ByteCodeResource> iter = pending.iterator(); while (iter.hasNext()) { // Preload classes addSubclasses(iter.next().getClassData()); iter.remove(); } } } }); } pending.give(bytecode); } // end synchro } else { pending.give(bytecode); } } protected void addSubclasses(final ClassFile classData) { // TODO reenable or delete this // if (classData.getEnclosedName().indexOf('.')>-1) { // bytecode.put(classData.getQualifiedName(), classData); // } } private boolean preloadClasses() { return !annotations.isEmpty() || !bytecodeMatchers.isEmpty(); } protected void accept(final String name, final ByteCodeResource bytecode, final Iterable<Class<? extends Annotation>> classAnnotations) { final ClassFile classFile = bytecode.getClassData(); for (final Class<? extends Annotation> annoClass : classAnnotations) { maybeAccept(name, bytecode, classFile, annoClass); } } protected void maybeAccept(final String name, final ByteCodeResource bytecode, final ClassFile classFile, final Class<? extends Annotation> annoClass) { final xapi.bytecode.annotation.Annotation anno = classFile.getAnnotation(annoClass.getName()); if (anno != null) { this.bytecode.put(name, bytecode); return; } // check the target retention of these annotations, and scan methods or fields try { final Target target = annoClass.getAnnotation(Target.class); ElementType[] targets; if (target == null) { targets = getDefaultAnnotationTargets(); } else { targets = target.value(); } for (final ElementType type : targets) { switch (type) { case METHOD: for (final MethodInfo method : classFile.getMethods()) { if (accepts(method, annoClass)) { this.bytecode.put(name, bytecode); return; } } break; case FIELD: for (final FieldInfo field : classFile.getFields()) { if (accepts(field, annoClass)) { this.bytecode.put(name, bytecode); return; } } break; case CONSTRUCTOR: for (final MethodInfo method : classFile.getMethods()) { if (method.getName().contains("<init>") && accepts(method, annoClass)) { this.bytecode.put(name, bytecode); return; } } break; default: break; } } } catch (final Throwable e) { throw X_Debug.rethrow(e); } } private boolean accepts(final MemberInfo method, final Class<? extends Annotation> annoClass) { return method.getAnnotation(annoClass.getName()) != null; } protected ElementType[] getDefaultAnnotationTargets() { return shouldScanMethods() ? new ElementType[]{ElementType.METHOD} : new ElementType[0]; } protected boolean shouldScanMethods() { return allMethodsWithAnnos != null; } public void addSourcecode(final String name, final SourceCodeResource sourcecode) { this.sources.put(name, sourcecode); } public void addResource(final String name, final StringDataResource resource) { this.resources.put(name, resource); } public boolean includeResource(final String name) { for (final Pattern p : resourceMatchers) { if (p.matcher(name).matches()) { return true; } if (p.matcher(name.substring(name.lastIndexOf('/')+1)).matches()) { return true; } } return false; } public boolean includeSourcecode(final String name) { for (final Pattern p : sourceMatchers) { if (p.matcher(name).matches()) { return true; } } return false; } public boolean includeBytecode(final String name) { for (final Pattern p : bytecodeMatchers) { if (p.matcher(name).matches()) { return true; } } return false; } public final SourceCodeResource findSource(final String name) { return sources.get(name); } public final Iterable<SourceCodeResource> findSources(final String prefix, final Pattern ... patterns) { if (patterns.length == 0) { return sources.findPrefixed(prefix); } class Itr implements Iterator<SourceCodeResource> { SourceCodeResource cls; Iterator<SourceCodeResource> iter = sources.findPrefixed(prefix).iterator(); @Override public boolean hasNext() { while(iter.hasNext()) { cls = iter.next(); for (final Pattern pattern : patterns) { if (pattern.matcher(cls.getResourceName()).matches()) { return true; } } } return false; } @Override public SourceCodeResource next() { return cls; } @Override public void remove() { throw new UnsupportedOperationException(); } } return new Iterable<SourceCodeResource>() { @Override public Iterator<SourceCodeResource> iterator() { return new Itr(); } }; } public final StringDataResource findResource(final String name) { return resources.get(name); } public final Iterable<StringDataResource> findResources(final String prefix, final Pattern ... patterns) { if (patterns.length == 0) { return resources.findPrefixed(prefix); } class Itr implements Iterator<StringDataResource> { StringDataResource cls; Iterator<StringDataResource> iter = resources.findPrefixed(prefix).iterator(); @Override public boolean hasNext() { while(iter.hasNext()) { cls = iter.next(); for (final Pattern pattern : patterns) { if (pattern.matcher(cls.getResourceName()).matches()) { return true; } } } return false; } @Override public StringDataResource next() { return cls; } @Override public void remove() { throw new UnsupportedOperationException(); } } return new Iterable<StringDataResource>() { @Override public Iterator<StringDataResource> iterator() { return new Itr(); } }; } public final ClassFile findClass(String clsName) { clsName = clsName.replace('/', '.'); final ByteCodeResource resource = bytecode.get(clsName); return resource == null ? null : resource.getClassData(); } @SuppressWarnings("unchecked") public final Iterable<ClassFile> getAllClasses(){ return new ClassFileIterator(MatchesValue.ANY, bytecode); } public Iterable<ClassFile> findClassesInPackage(final String name) { return new ClassFileIterator(new MatchesValue<ClassFile>() { @Override public boolean matches(final ClassFile value) { return !"package-info".equals(value.getEnclosedName()) && value.getPackage().equals(name); } }, bytecode); } public Iterable<ClassFile> findClassesBelowPackage(final String name) { return new ClassFileIterator(new MatchesValue<ClassFile>() { @Override public boolean matches(final ClassFile value) { return !"package-info".equals(value.getEnclosedName()) && value.getPackage().startsWith(name); } }, bytecode); } public Iterable<ClassFile> findPackagesBelowPackage(final String name) { return new ClassFileIterator(new MatchesValue<ClassFile>() { @Override public boolean matches(final ClassFile value) { return "package-info".equals(value.getEnclosedName()) && value.getPackage().startsWith(name+"."); } }, bytecode); } /** * Finds all classes that are direct subclasses of one of the supplied types. * * This does not check interfaces, only the direct supertype. * It will _not_ match types equal to the supplied types. * * This is primarily used for types that cannot have more than one subclass, * like Enum or Annotation. * * @param superClasses * @return */ public final Iterable<ClassFile> findDirectSubclasses( final Class<?> ... superClasses) { return findDirectSubclasses(X_Source.toStringBinary(superClasses)); } public final Iterable<ClassFile> findDirectSubclasses( final String ... superClasses) { // Local class to capture the final method parameter class Itr implements Iterator<ClassFile> { ClassFile cls; Iterator<ByteCodeResource> iter = bytecode.findPrefixed("").iterator(); final MatchesDirectSubclasses matcher = new MatchesDirectSubclasses(superClasses); @Override public boolean hasNext() { while(iter.hasNext()) { cls = iter.next().getClassData(); if (matcher.matches(cls)) { return true; } } return false; } @Override public ClassFile next() { return cls; } @Override public void remove() { throw new UnsupportedOperationException(); } } return new Iterable<ClassFile>() { @Override public Iterator<ClassFile> iterator() { return new Itr(); } }; } public final Iterable<ClassFile> findImplementationOf( final Class<?> ... superClasses) { return findImplementationOf(X_Source.toStringBinary(superClasses)); } public final Iterable<ClassFile> findImplementationOf( final String ... superClasses) { // Local class to capture the final method parameter class Itr implements Iterator<ClassFile> { ClassFile cls; Iterator<ByteCodeResource> iter = bytecode.findPrefixed("").iterator(); final MatchesImplementationsOf matcher = new MatchesImplementationsOf(bytecode, superClasses); @Override public boolean hasNext() { while(iter.hasNext()) { cls = iter.next().getClassData(); if (matcher.matches(cls)) { return true; } } return false; } @Override public ClassFile next() { return cls; } @Override public void remove() { throw new UnsupportedOperationException(); } } return new Iterable<ClassFile>() { @Override public Iterator<ClassFile> iterator() { return new Itr(); } }; } public final Iterable<ClassFile> findClassAnnotatedWith( @SuppressWarnings("unchecked") final Class<? extends Annotation> ... annotations) { if (allAnnos == null) { allAnnos = new AnnotatedClassIterator(getExecutor(), bytecode); } // Local class to capture the final method parameter class Itr implements Iterator<ClassFile> { ClassFile cls; Iterator<ClassFile> iter = allAnnos.iterator(); @Override public boolean hasNext() { while(iter.hasNext()) { cls = iter.next(); for (final Class<?> annotation : annotations) { if (cls.getAnnotation(annotation.getName())!=null) { return true; } } } return false; } @Override public ClassFile next() { return cls; } @Override public void remove() { throw new UnsupportedOperationException(); } } return new Iterable<ClassFile>() { @Override public Iterator<ClassFile> iterator() { return new Itr(); } }; } public final Iterable<ClassFile> findClassWithAnnotatedMethods( @SuppressWarnings("unchecked") final Class<? extends Annotation> ... annotations) { if (allMethodsWithAnnos == null) { allMethodsWithAnnos = new AnnotatedMethodIterator(getExecutor(), bytecode); } // Local class to capture the final method parameter class Itr implements Iterator<ClassFile> { ClassFile cls; Iterator<ClassFile> iter = allMethodsWithAnnos.iterator(); @Override public boolean hasNext() { while(iter.hasNext()) { cls = iter.next(); for (final MethodInfo method : cls.getMethods()) { if (allMethodsWithAnnos.scanMethod(method)) { for (final Class<?> annotation : annotations) { if (method.getAnnotation(annotation.getName()) != null) { return true; } } } } } return false; } @Override public ClassFile next() { return cls; } @Override public void remove() { throw new UnsupportedOperationException(); } } return new Iterable<ClassFile>() { @Override public Iterator<ClassFile> iterator() { preloadClasses(); return new Itr(); } }; } protected ExecutorService getExecutor() { return executor.get(); } public ClassPool getClassPool() { if (pool == null) { pool = new ClassPool(); for (final URL cp : classpath) { String asString = cp.toString(); if (asString.endsWith("/")) { asString = asString.substring(0, asString.length()-1); } if (asString.startsWith("jar:")) { asString = asString.substring(4); } if (asString.startsWith("file:/")) { asString = asString.substring(6); } final int index = asString.indexOf("jar!"); if (index != -1) { asString=asString.substring(0, index+3); } try { pool.appendClassPath(asString); } catch (final NotFoundException e) { X_Log.warn(getClass(), "Could not find resource "+cp, e); } } } return pool; } public void setClasspath(final Set<URL> keySet) { this.classpath = new ArrayList<URL>(keySet); } } class ResourceTrie <ResourceType extends ClasspathResource> extends MultithreadedStringTrie<ResourceType> { protected class PrioritizedEdge extends Edge { private static final long serialVersionUID = 7917481802519184433L; public PrioritizedEdge() { } public PrioritizedEdge(final char[] key, final int index, final int end, final ResourceType value) { super(key, index, end); this.value = value; } @Override protected void setValue(final ResourceType value) { if (this.value != null) { if (this.value.priority() > value.priority()) { return; } } super.setValue(value); } } @Override protected Edge newEdge() { return new PrioritizedEdge(); } @Override protected Edge newEdge(final char[] key, final int index, final int end, final ResourceType value) { return new PrioritizedEdge(key, index, end, value); } }