package xapi.dev.scanner.impl; import xapi.annotation.inject.InstanceDefault; import xapi.collect.api.Fifo; import xapi.collect.impl.SimpleFifo; import xapi.dev.resource.impl.ByteCodeResource; import xapi.dev.resource.impl.FileBackedResource; import xapi.dev.resource.impl.JarBackedResource; import xapi.dev.resource.impl.SourceCodeResource; import xapi.dev.resource.impl.StringDataResource; import xapi.dev.scanner.api.ClasspathScanner; import xapi.except.ThreadsafeUncaughtExceptionHandler; import xapi.util.X_Debug; import xapi.util.X_Namespace; import xapi.util.X_Properties; import xapi.util.X_Util; import xapi.util.api.ProvidesValue; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.annotation.Annotation; import java.net.JarURLConnection; import java.net.URL; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Pattern; @InstanceDefault(implFor = ClasspathScanner.class) public class ClasspathScannerDefault implements ClasspathScanner { final Set<String> pkgs; final Set<Class<? extends Annotation>> annotations; final Set<Pattern> resourceMatchers; final Set<Pattern> bytecodeMatchers; final Set<Pattern> sourceMatchers; final Set<String> activeJars; private boolean scanSystemJars; public ClasspathScannerDefault() { pkgs = new HashSet<String>(); annotations = new HashSet<Class<? extends Annotation>>(); resourceMatchers = new HashSet<Pattern>(); bytecodeMatchers = new HashSet<Pattern>(); sourceMatchers = new HashSet<Pattern>(); activeJars = new HashSet<String>(); } protected class ScanRunner implements Runnable { private final URL classpath; private final ClasspathResourceMap map; private final int priority; private final Iterable<String> pathRoot; private Thread creatorThread; public ScanRunner(final URL classpath, final Iterable<String> pkgs, final ClasspathResourceMap map, final int priority) { this.classpath = classpath; this.map = map; this.priority = priority; this.pathRoot = pkgs; creatorThread = Thread.currentThread(); } @Override public void run() { Thread.currentThread().setUncaughtExceptionHandler(new ThreadsafeUncaughtExceptionHandler(creatorThread)); creatorThread = null; // determine if we should run in jar mode or file mode File file; String path = classpath.toExternalForm(); final boolean jarUrl = path.startsWith("jar:"); if (jarUrl) { path = path.substring("jar:".length()); } final boolean fileUrl = path.startsWith("file:"); if (fileUrl) { path = path.substring("file:".length()); } boolean jarFile = path.contains(".jar!"); if (jarFile) { path = path.substring(0, path.indexOf(".jar!") + ".jar".length()); } else { jarFile = path.endsWith(".jar"); } if (!(file = new java.io.File(path)).exists()) { path = new File(path).toURI().toString(); if ((file = new java.io.File(path)).exists()) { // should be impossible since we get these urls from classloader throw X_Util.rethrow(new FileNotFoundException()); } } try { if (classpath.getProtocol().equals("jar")) { scan(((JarURLConnection)classpath.openConnection()).getJarFile()); return; } assert classpath.getProtocol().equals("file") : "ScanRunner only handles url and file protocols"; if (jarFile) { scan(new JarFile(file)); } else { // For files, we need to strip everything up to the package we are // scanning String fileRoot = file.getCanonicalPath().replace('\\', '/'); int delta = 0; if (!fileRoot.endsWith("/")) { delta = -1; fileRoot += "/"; } for (final String pkg : pathRoot) { if (fileRoot.replace('/', '.').endsWith(pkg.endsWith(".")?pkg:pkg+".")) { scan(file, fileRoot.substring(0, fileRoot.length() - pkg.length() + delta)); break; } } } } catch (final Exception e) { final Thread t = Thread.currentThread(); t.getUncaughtExceptionHandler().uncaughtException(t, e); } } private final void scan(final File file, final String pathRoot) throws IOException { if (file.isDirectory()) { scan(file.listFiles(), pathRoot); } else { addFile(file, pathRoot); } } private void scan(final File[] listFiles, final String pathRoot) throws IOException { for (final File file : listFiles) { scan(file, pathRoot); } } private final void scan(final JarFile jarFile) { if (activeJars.add(jarFile.getName())) { final Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { final JarEntry next = entries.nextElement(); addEntry(jarFile, next); } } } protected void addFile(final File file, final String pathRoot) throws IOException { String name = file.getCanonicalPath().substring(pathRoot.length()); if (name.startsWith(File.separator)) { name = name.substring(1); } if (name.endsWith(".class")) { if (map.includeBytecode(name)) { map.addBytecode(name, new ByteCodeResource( new FileBackedResource(name, file, priority))); } } else if (name.endsWith(".java")) { if (map.includeSourcecode(name)) { map.addSourcecode(name, new SourceCodeResource( new FileBackedResource(name, file, priority))); } } else { if (map.includeResource(name)) { map.addResource(name, new StringDataResource( new FileBackedResource(name, file, priority))); } } } protected void addEntry(final JarFile file, final JarEntry entry) { final String name = entry.getName(); for (final String pkg : pkgs) { if (name.startsWith(pkg)) { if (name.endsWith(".class")) { if (map.includeBytecode(name)) { map.addBytecode(name, new ByteCodeResource( new JarBackedResource(file, entry, priority))); } } else if (name.endsWith(".java")) { if (map.includeSourcecode(name)) { map.addSourcecode(name, new SourceCodeResource( new JarBackedResource(file, entry, priority))); } } else { if (map.includeResource(name)) { map.addResource(name, new StringDataResource( new JarBackedResource(file, entry, priority))); } } return; } } } } @Override public ClasspathScanner scanAnnotation(final Class<? extends Annotation> annotation) { annotations.add(annotation); return this; } @Override public ClasspathScanner scanAnnotations(@SuppressWarnings("unchecked") final Class<? extends Annotation> ... annotations) { for (final Class<? extends Annotation> annotation : annotations) { this.annotations.add(annotation); } return this; } @Override public ClasspathResourceMap scan(final ClassLoader loaders) { final ExecutorService executor = newExecutor(); try { final ClasspathResourceMap map = scan(loaders, executor).call(); synchronized (map) { return map; } } catch (final Exception e) { throw X_Debug.rethrow(e); } } @Override public ExecutorService newExecutor() { final int threads = Runtime.getRuntime().availableProcessors()*3/2; return Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors()*3/2); } @Override public synchronized Callable<ClasspathResourceMap> scan(final ClassLoader loadFrom, final ExecutorService executor) { // perform the actual scan final Map<URL,Fifo<String>> classPaths = new LinkedHashMap<URL,Fifo<String>>(); if (pkgs.size() == 0 || (pkgs.size() == 1 && "".equals(pkgs.iterator().next()))) { for (final String pkg : X_Properties.getProperty(X_Namespace.PROPERTY_RUNTIME_SCANPATH, ",META-INF,com,org,net,xapi,java").split(",\\s*")) { pkgs.add(pkg); } } for (final String pkg : pkgs) { final Enumeration<URL> resources; try { resources = loadFrom.getResources( pkg.equals(".*")//||pkg.equals("") ?"":pkg.replace('.', '/') ); } catch (final Exception e) { e.printStackTrace(); continue; } while (resources.hasMoreElements()) { final URL resource = resources.nextElement(); final String file = resource.toExternalForm(); if (file.contains("gwt-dev.jar")) { continue; } Fifo<String> fifo = classPaths.get(resource); if (fifo == null) { fifo = new SimpleFifo<String>(); fifo.give(pkg); classPaths.put(resource, fifo); } else { fifo.remove(pkg); fifo.give(pkg); } } } final int pos = 0; final ProvidesValue<ExecutorService> exe = new ProvidesValue<ExecutorService>() { ExecutorService exe = executor; @Override public ExecutorService get() { if (exe.isShutdown()) { exe = newExecutor(); } return exe; } }; final ClasspathResourceMap map = new ClasspathResourceMap(exe, annotations, bytecodeMatchers, resourceMatchers, sourceMatchers); map.setClasspath(classPaths.keySet()); final Fifo<Future<?>> jobs = new SimpleFifo<Future<?>>(); class Finisher implements Callable<ClasspathResourceMap>{ @Override public ClasspathResourceMap call() throws Exception { while (!jobs.isEmpty()) { // drain the work pool final Iterator<Future<?>> iter = jobs.forEach().iterator(); while (iter.hasNext()) { if (iter.next().isDone()) { iter.remove(); } } try { Thread.sleep(0, 500); } catch (final InterruptedException e) { Thread.interrupted(); throw X_Debug.rethrow(e); } } executor.shutdown(); return map; } } for (final URL url : classPaths.keySet()) { final Fifo<String> packages = classPaths.get(url); if (shouldScanUrl(url)) { final ScanRunner scanner = newScanRunner(url, map, executor, packages.forEach(), pos); jobs.give(executor.submit(scanner)); } } return new Finisher(); } protected boolean shouldScanUrl(URL url) { String external = url.toExternalForm(); if (!scanSystemJars) { if (external.contains( System.getProperty("java.home", "--n0t f0und---") )) { return false; } } return !external.contains("idea_rt.jar"); } private ScanRunner newScanRunner(final URL classPath, final ClasspathResourceMap map, final ExecutorService executor, final Iterable<String> pkgs, final int priority) { return new ScanRunner(classPath, pkgs, map, priority); } @Override public ClasspathScanner scanPackage(final String pkg) { pkgs.add(pkg); return this; } @Override public ClasspathScanner scanPackages(final String ... pkgs) { for (final String pkg : pkgs) { this.pkgs.add(pkg); } return this; } @Override public ClasspathScanner matchClassFile(final String regex) { bytecodeMatchers.add(Pattern.compile(regex)); return this; } @Override public ClasspathScanner skipSystemJars(boolean skipSystemJars) { this.scanSystemJars = !skipSystemJars; return this; } @Override public ClasspathScanner matchClassFiles(final String ... regexes) { for (final String regex : regexes) { bytecodeMatchers.add(Pattern.compile(regex)); } return this; } @Override public ClasspathScanner matchResource(final String regex) { resourceMatchers.add(Pattern.compile(regex)); return this; } @Override public ClasspathScanner matchResources(final String ... regexes) { for (final String regex : regexes) { resourceMatchers.add(Pattern.compile(regex)); } return this; } @Override public ClasspathScanner matchSourceFile(final String regex) { sourceMatchers.add(Pattern.compile(regex)); return this; } @Override public ClasspathScanner matchSourceFiles(final String ... regexes) { for (final String regex : regexes) { sourceMatchers.add(Pattern.compile(regex)); } return this; } }