/** * AnalyzerBeans * Copyright (C) 2014 Neopost - Customer Information Management * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.eobjects.analyzer.descriptors; import java.io.File; import java.io.FileFilter; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.net.JarURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.eobjects.analyzer.beans.api.Analyzer; import org.eobjects.analyzer.beans.api.AnalyzerBean; import org.eobjects.analyzer.beans.api.Filter; import org.eobjects.analyzer.beans.api.FilterBean; import org.eobjects.analyzer.beans.api.Renderer; import org.eobjects.analyzer.beans.api.RendererBean; import org.eobjects.analyzer.beans.api.RenderingFormat; import org.eobjects.analyzer.beans.api.Transformer; import org.eobjects.analyzer.beans.api.TransformerBean; import org.eobjects.analyzer.job.concurrent.SingleThreadedTaskRunner; import org.eobjects.analyzer.job.concurrent.TaskListener; import org.eobjects.analyzer.job.concurrent.TaskRunner; import org.eobjects.analyzer.job.tasks.Task; import org.eobjects.analyzer.util.ClassLoaderUtils; import org.apache.metamodel.util.ExclusionPredicate; import org.apache.metamodel.util.FileHelper; import org.apache.metamodel.util.Predicate; import org.apache.metamodel.util.Ref; import org.apache.metamodel.util.TruePredicate; import org.kohsuke.asm5.ClassReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Descriptor provider implementation that works by scanning particular packages * in the classpath for annotated classes. Descriptors will be generated based * on encountered annotations. * * This implementation also supports adding single descriptors by using the * add... methods. * * Classes with either of these annotations will be picked up by the classpath * scanner: * <ul> * <li>{@link AnalyzerBean}</li> * <li>{@link TransformerBean}</li> * <li>{@link FilterBean}</li> * <li>{@link RendererBean}</li> * </ul> * * */ public final class ClasspathScanDescriptorProvider extends AbstractDescriptorProvider { private static final Logger logger = LoggerFactory.getLogger(ClasspathScanDescriptorProvider.class); private final Map<String, AnalyzerBeanDescriptor<?>> _analyzerBeanDescriptors = new HashMap<String, AnalyzerBeanDescriptor<?>>(); private final Map<String, FilterBeanDescriptor<?, ?>> _filterBeanDescriptors = new HashMap<String, FilterBeanDescriptor<?, ?>>(); private final Map<String, TransformerBeanDescriptor<?>> _transformerBeanDescriptors = new HashMap<String, TransformerBeanDescriptor<?>>(); private final Map<String, RendererBeanDescriptor<?>> _rendererBeanDescriptors = new HashMap<String, RendererBeanDescriptor<?>>(); private final TaskRunner _taskRunner; private final Predicate<Class<? extends RenderingFormat<?>>> _renderingFormatPredicate; private final AtomicInteger _tasksPending; /** * Default constructor. Will perform classpath scanning in the calling * thread(s). */ public ClasspathScanDescriptorProvider() { this(new SingleThreadedTaskRunner()); } /** * Constructs a {@link ClasspathScanDescriptorProvider} using a specified * {@link TaskRunner}. The taskrunner will be used to perform the classpath * scan, potentially in a parallel fashion. * * @param taskRunner */ public ClasspathScanDescriptorProvider(TaskRunner taskRunner) { this(taskRunner, new TruePredicate<Class<? extends RenderingFormat<?>>>()); } /** * Constructs a {@link ClasspathScanDescriptorProvider} using a specified * {@link TaskRunner}. The taskrunner will be used to perform the classpath * scan, potentially in a parallel fashion. * * @param taskRunner * @param excludedRenderingFormats * rendering formats to exclude from loading into the descriptor * provider */ public ClasspathScanDescriptorProvider(TaskRunner taskRunner, Collection<Class<? extends RenderingFormat<?>>> excludedRenderingFormats) { this(taskRunner, createRenderingFormatPredicate(excludedRenderingFormats)); } /** * Constructs a {@link ClasspathScanDescriptorProvider} using a specified * {@link TaskRunner}. The taskrunner will be used to perform the classpath * scan, potentially in a parallel fashion. * * @param taskRunner * @param excludedRenderingFormats * rendering formats to exclude from loading into the descriptor * provider * @param autoLoadDescriptorClasses * whether or not to automatically load descriptors when they are * requested by class names. */ public ClasspathScanDescriptorProvider(TaskRunner taskRunner, Collection<Class<? extends RenderingFormat<?>>> excludedRenderingFormats, boolean autoLoadDescriptorClasses) { this(taskRunner, createRenderingFormatPredicate(excludedRenderingFormats), autoLoadDescriptorClasses); } /** * Constructs a {@link ClasspathScanDescriptorProvider} using a specified * {@link TaskRunner}. The taskrunner will be used to perform the classpath * scan, potentially in a parallel fashion. * * @param taskRunner * @param renderingFormatPredicate * predicate function to apply when evaluating if a particular * rendering format is of interest or not */ public ClasspathScanDescriptorProvider(TaskRunner taskRunner, Predicate<Class<? extends RenderingFormat<?>>> renderingFormatPredicate) { this(taskRunner, renderingFormatPredicate, false); } /** * Constructs a {@link ClasspathScanDescriptorProvider} using a specified * {@link TaskRunner}. The taskrunner will be used to perform the classpath * scan, potentially in a parallel fashion. * * @param taskRunner * @param renderingFormatPredicate * predicate function to apply when evaluating if a particular * rendering format is of interest or not * @param autoLoadDescriptorClasses * whether or not to automatically load descriptors when they are * requested by class names. */ public ClasspathScanDescriptorProvider(TaskRunner taskRunner, Predicate<Class<? extends RenderingFormat<?>>> renderingFormatPredicate, boolean autoLoadDescriptorClasses) { super(autoLoadDescriptorClasses); _taskRunner = taskRunner; _tasksPending = new AtomicInteger(0); _renderingFormatPredicate = renderingFormatPredicate; } private static Predicate<Class<? extends RenderingFormat<?>>> createRenderingFormatPredicate( Collection<Class<? extends RenderingFormat<?>>> excludedRenderingFormats) { if (excludedRenderingFormats == null || excludedRenderingFormats.isEmpty()) { return new TruePredicate<Class<? extends RenderingFormat<?>>>(); } return new ExclusionPredicate<Class<? extends RenderingFormat<?>>>(excludedRenderingFormats); } /** * Scans a package in the classpath (of the current thread's context * classloader) for annotated components. * * @param packageName * the package name to scan * @param recursive * whether or not to scan subpackages recursively * @return */ public ClasspathScanDescriptorProvider scanPackage(String packageName, boolean recursive) { return scanPackage(packageName, recursive, ClassLoaderUtils.getParentClassLoader(), false); } /** * Scans a package in the classpath (of a particular classloader) for * annotated components. * * @param packageName * the package name to scan * @param recursive * whether or not to scan subpackages recursively * @param classLoader * the classloader to use * @return */ public ClasspathScanDescriptorProvider scanPackage(final String packageName, final boolean recursive, final ClassLoader classLoader) { return scanPackage(packageName, recursive, classLoader, true); } /** * Scans a package in the classpath (of a particular classloader) for * annotated components. * * @param packageName * the package name to scan * @param recursive * whether or not to scan subpackages recursively * @param classLoader * the classloader to use for discovering resources in the * classpath * @param strictClassLoader * whether or not classes originating from other classloaders may * be included in scan (classloaders can sometimes discover * classes from parent classloaders which may or may not be * wanted for inclusion). * @return */ public ClasspathScanDescriptorProvider scanPackage(final String packageName, final boolean recursive, final ClassLoader classLoader, final boolean strictClassLoader) { return scanPackage(packageName, recursive, classLoader, strictClassLoader, null); } /** * Scans a package in the classpath (of a particular classloader) for * annotated components. Optionally restricted by a set of JAR files to look * in. * * @param packageName * the package name to scan * @param recursive * whether or not to scan subpackages recursively * @param classLoader * the classloader to use for discovering resources in the * classpath * @param strictClassLoader * whether or not classes originating from other classloaders may * be included in scan (classloaders can sometimes discover * classes from parent classloaders which may or may not be * wanted for inclusion). * @param jarFiles * optionally (nullable) array of JAR files or class directories * to scan. Note that if specified, the JAR files are assumed to * be included in the classloaders available resources. * @return */ public ClasspathScanDescriptorProvider scanPackage(final String packageName, final boolean recursive, final ClassLoader classLoader, final boolean strictClassLoader, final File[] jarFiles) { _tasksPending.incrementAndGet(); final TaskListener listener = new TaskListener() { @Override public void onBegin(Task task) { logger.info("Scan of '{}' beginning", packageName); } @Override public void onComplete(Task task) { logger.info("Scan of '{}' complete", packageName); taskDone(); } @Override public void onError(Task task, Throwable throwable) { logger.info("Scan of '{}' failed: {}", packageName, throwable.getMessage()); logger.warn("Exception occurred while scanning and installing package: " + packageName, throwable); taskDone(); } }; final Task task = new Task() { @Override public void execute() throws Exception { final String packagePath = packageName.replace('.', '/'); if (recursive) { logger.info("Scanning package path '{}' (and subpackages recursively)", packagePath); } else { logger.info("Scanning package path '{}'", packagePath); } logger.debug("Using ClassLoader: {}", classLoader); if (jarFiles != null && jarFiles.length > 0) { for (File file : jarFiles) { if (!file.exists()) { logger.debug("Omitting JAR file because it does not exist: {}", file); } else if (file.isDirectory()) { logger.info("Scanning subdirectory of: {}", file); final File packageDirectory = new File(file, packagePath); if (packageDirectory.exists()) { scanDirectory(packageDirectory, recursive, classLoader, strictClassLoader); } else { logger.debug("Omitting directory because it does not exist: {}", packageDirectory); } } else { logger.info("Scanning JAR file: {}", file); try (JarFile jarFile = new JarFile(file)) { scanJar(jarFile, classLoader, packagePath, recursive, strictClassLoader); } catch (Exception e) { logger.error("Failed to scan package '" + packageName + "' in file: " + file, e); } } } } else { final Enumeration<URL> resources = classLoader.getResources(packagePath); int count = 0; while (resources.hasMoreElements()) { count++; final URL resource = resources.nextElement(); logger.debug("Scanning resource/URL no. {}: {}", count, resource); try { scanUrl(resource, classLoader, packagePath, recursive, strictClassLoader); } catch (Exception e) { logger.error("Failed to scan package '" + packageName + "' in resource/URL: " + resource, e); } } logger.debug("Scanned resources of {}: {}", packageName, count); } } }; _taskRunner.run(task, listener); return this; } private void scanUrl(URL resource, final ClassLoader classLoader, final String packagePath, final boolean recursive, final boolean strictClassLoader) throws IOException { final String file = resource.getFile(); logger.debug("Resource file string: {}", file); final File dir = new File(file.replaceAll("\\%20", " ")); if (dir.isDirectory()) { logger.info("Resource is a directory, scanning for files: {}", dir.getAbsolutePath()); scanDirectory(dir, recursive, classLoader, strictClassLoader); } else { URLConnection connection = resource.openConnection(); if (connection instanceof JarURLConnection) { logger.info("Getting JarFile from JarURLConnection: {}", connection); JarFile jarFile = ((JarURLConnection) connection).getJarFile(); // note: We are NOT closing this JarFile, because it is still // used by the JarURLConnection scanJar(jarFile, classLoader, packagePath, recursive, strictClassLoader); } else { // We'll assume URLs of the format "jar:path!/entry", with the // protocol being arbitrary as long as following the entry // format. We'll also handle paths with and without leading // "file:" prefix. String rootEntryPath; final String jarFileUrl; final int separatorIndex = file.indexOf("!/"); JarFile jarFile = null; try { if (separatorIndex != -1) { jarFileUrl = file.substring(0, separatorIndex); rootEntryPath = file.substring(separatorIndex + "!/".length()); jarFile = getJarFile(jarFileUrl); } else { logger.info("Creating JarFile based on URI (without '!/'): {}", file); jarFile = new JarFile(file); jarFileUrl = file; rootEntryPath = ""; } if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) { // Root entry path must end with slash to allow for // proper matching. The Sun JRE does not return a slash // here, but BEA JRockit does. rootEntryPath = rootEntryPath + "/"; } scanJar(jarFile, classLoader, packagePath, recursive, strictClassLoader); } finally { if (jarFile != null) { jarFile.close(); } } } } } /** * Resolve the given jar file URL into a JarFile object. */ private JarFile getJarFile(String jarFileUrl) throws IOException { if (jarFileUrl.startsWith("file:")) { try { final URI uri = new URI(jarFileUrl.replaceAll(" ", "\\%20")); final String jarFileName = uri.getSchemeSpecificPart(); logger.info("Creating new JarFile based on URI-scheme filename: {}", jarFileName); return new JarFile(jarFileName); } catch (URISyntaxException ex) { // Fallback for URLs that are not valid URIs (should hardly ever // happen). final String jarFileName = jarFileUrl.substring("file:".length()); logger.info("Creating new JarFile based on alternative filename: {}", jarFileName); return new JarFile(jarFileName); } } else { logger.info("Creating new JarFile based on URI (with '!/'): {}", jarFileUrl); return new JarFile(jarFileUrl); } } private boolean isClass(String entryName) { return entryName.endsWith(".class"); } protected boolean isClassInPackage(String entryName, String packagePath, boolean recursive) { if (!entryName.startsWith(packagePath)) { return false; } if (!isClass(entryName)) { return false; } if (recursive) { return true; } String trailingPart = entryName.substring(packagePath.length()); if (trailingPart.startsWith("/")) { trailingPart = trailingPart.substring(1); } return trailingPart.indexOf('/') == -1; } private void scanJar(final JarFile jarFile, final ClassLoader classLoader, final String packagePath, final boolean recursive, final boolean strictClassLoader) throws IOException { Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { final JarEntry entry = entries.nextElement(); final Ref<InputStream> entryInputStream = new Ref<InputStream>() { @Override public InputStream get() { try { return jarFile.getInputStream(entry); } catch (IOException e) { throw new IllegalStateException("Failed to read JAR entry InputStream", e); } } }; scanEntry(entry, packagePath, recursive, classLoader, strictClassLoader, entryInputStream); } } private void scanEntry(JarEntry entry, String packagePath, boolean recursive, ClassLoader classLoader, boolean strictClassLoader, Ref<InputStream> entryInputStream) throws IOException { String entryName = entry.getName(); if (isClassInPackage(entryName, packagePath, recursive)) { logger.debug("Scanning JAR class file entry: {}", entryName); InputStream inputStream = entryInputStream.get(); try { scanInputStreamOfClassFile(inputStream, classLoader, strictClassLoader); } catch (RuntimeException e) { logger.error("Failed to scan JAR class file entry: " + entryName, e); } } else { if (logger.isInfoEnabled()) { // log omitted .class files if (isClass(entryName)) { logger.debug("Omitting JAR class file entry: {} (looking for package path: {})", entryName, packagePath); } else { logger.trace("Omitting JAR entry (not a class): {}", entryName); } } } } private void scanDirectory(File dir, boolean recursive, ClassLoader classLoader, final boolean strictClassLoader) { if (!dir.exists()) { throw new IllegalArgumentException("Directory '" + dir + "' does not exist"); } if (!dir.isDirectory()) { throw new IllegalArgumentException("The file '" + dir + "' is not a directory"); } logger.info("Scanning directory: {}", dir); final File[] classFiles = dir.listFiles(new FilenameFilter() { @Override public boolean accept(File file, String filename) { return filename.endsWith(".class"); } }); for (File file : classFiles) { final InputStream inputStream = FileHelper.getInputStream(file); try { scanInputStream(inputStream, classLoader, strictClassLoader); } catch (IOException e) { logger.error("Could not read file", e); } finally { FileHelper.safeClose(inputStream); } } if (recursive) { File[] subDirectories = dir.listFiles(new FileFilter() { @Override public boolean accept(File file) { return file.isDirectory(); } }); if (subDirectories != null) { if (logger.isInfoEnabled() && subDirectories.length > 0) { logger.info("Recursively scanning " + subDirectories.length + " subdirectories"); } for (File subDir : subDirectories) { scanDirectory(subDir, true, classLoader, strictClassLoader); } } } } /** * * @param inputStream * @param classLoader * @param strictClassLoader * @throws IOException * * @{@link deprecated} use * {@link #scanInputStreamOfClassFile(InputStream, ClassLoader, boolean)} * instead. */ @Deprecated protected void scanInputStream(final InputStream inputStream, final ClassLoader classLoader, final boolean strictClassLoader) throws IOException { scanInputStreamOfClassFile(inputStream, classLoader, strictClassLoader); } protected void scanInputStreamOfClassFile(final InputStream inputStream, final ClassLoader classLoader, final boolean strictClassLoader) throws IOException { try { final ClassReader classReader = new ClassReader(inputStream); final BeanClassVisitor visitor = new BeanClassVisitor(classLoader, _renderingFormatPredicate); classReader.accept(visitor, ClassReader.SKIP_CODE); Class<?> beanClass = visitor.getBeanClass(); if (beanClass == null) { return; } if (strictClassLoader && classLoader != null && beanClass.getClassLoader() != classLoader) { logger.warn("Scanned class did not belong to required classloader: " + beanClass + ", ignoring"); return; } if (visitor.isAnalyzer()) { @SuppressWarnings("unchecked") Class<? extends Analyzer<?>> analyzerClass = (Class<? extends Analyzer<?>>) beanClass; logger.info("Adding analyzer class: {}", beanClass); addAnalyzerClass(analyzerClass); } if (visitor.isTransformer()) { @SuppressWarnings("unchecked") Class<? extends Transformer<?>> transformerClass = (Class<? extends Transformer<?>>) beanClass; logger.info("Adding transformer class: {}", beanClass); addTransformerClass(transformerClass); } if (visitor.isFilter()) { @SuppressWarnings("unchecked") Class<? extends Filter<? extends Enum<?>>> filterClass = (Class<? extends Filter<?>>) beanClass; logger.info("Adding filter class: {}", beanClass); addFilterClass(filterClass); } if (visitor.isRenderer()) { @SuppressWarnings("unchecked") Class<? extends Renderer<?, ?>> rendererClass = (Class<? extends Renderer<?, ?>>) beanClass; logger.info("Adding renderer class: {}", beanClass); addRendererClass(rendererClass); } } finally { FileHelper.safeClose(inputStream); } } public ClasspathScanDescriptorProvider addAnalyzerClass(Class<? extends Analyzer<?>> clazz) { AnalyzerBeanDescriptor<?> descriptor = _analyzerBeanDescriptors.get(clazz.getName()); if (descriptor == null) { try { descriptor = Descriptors.ofAnalyzer(clazz); _analyzerBeanDescriptors.put(clazz.getName(), descriptor); } catch (Exception e) { logger.error("Unexpected error occurred while creating descriptor for: " + clazz, e); } } return this; } public ClasspathScanDescriptorProvider addTransformerClass(Class<? extends Transformer<?>> clazz) { TransformerBeanDescriptor<? extends Transformer<?>> descriptor = _transformerBeanDescriptors.get(clazz .getName()); if (descriptor == null) { try { descriptor = Descriptors.ofTransformer(clazz); _transformerBeanDescriptors.put(clazz.getName(), descriptor); } catch (Exception e) { logger.error("Unexpected error occurred while creating descriptor for: " + clazz, e); } } return this; } public ClasspathScanDescriptorProvider addFilterClass(Class<? extends Filter<?>> clazz) { FilterBeanDescriptor<? extends Filter<?>, ?> descriptor = _filterBeanDescriptors.get(clazz.getName()); if (descriptor == null) { try { descriptor = Descriptors.ofFilterUnbound(clazz); _filterBeanDescriptors.put(clazz.getName(), descriptor); } catch (Exception e) { logger.error("Unexpected error occurred while creating descriptor for: " + clazz, e); } } return this; } public ClasspathScanDescriptorProvider addRendererClass(Class<? extends Renderer<?, ?>> clazz) { RendererBeanDescriptor<?> descriptor = _rendererBeanDescriptors.get(clazz.getName()); if (descriptor == null) { try { descriptor = Descriptors.ofRenderer(clazz); _rendererBeanDescriptors.put(clazz.getName(), descriptor); } catch (Exception e) { logger.error("Unexpected error occurred while creating descriptor for: " + clazz, e); } } return this; } private void taskDone() { int tasks = _tasksPending.decrementAndGet(); if (tasks == 0) { synchronized (this) { notifyAll(); } } } /** * Waits for all pending tasks to finish */ private void awaitTasks() { if (_tasksPending.get() == 0) { return; } synchronized (this) { while (_tasksPending.get() != 0) { try { logger.info("Scan tasks still pending, waiting"); wait(); } catch (InterruptedException e) { logger.debug("Interrupted while awaiting task completion", e); } } } } @Override public Collection<FilterBeanDescriptor<?, ?>> getFilterBeanDescriptors() { awaitTasks(); return Collections.unmodifiableCollection(_filterBeanDescriptors.values()); } @Override public Collection<AnalyzerBeanDescriptor<?>> getAnalyzerBeanDescriptors() { awaitTasks(); return Collections.unmodifiableCollection(_analyzerBeanDescriptors.values()); } @Override public Collection<TransformerBeanDescriptor<?>> getTransformerBeanDescriptors() { awaitTasks(); return Collections.unmodifiableCollection(_transformerBeanDescriptors.values()); } @Override public Collection<RendererBeanDescriptor<?>> getRendererBeanDescriptors() { awaitTasks(); return Collections.unmodifiableCollection(_rendererBeanDescriptors.values()); } public Predicate<Class<? extends RenderingFormat<?>>> getRenderingFormatPredicate() { return _renderingFormatPredicate; } }