| /* |
| * 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.drill.common.scanner; |
| |
| import static java.lang.String.format; |
| import static java.util.Collections.unmodifiableList; |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; |
| |
| import java.io.IOException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.drill.common.config.DrillConfig; |
| import org.apache.drill.common.exceptions.UserException; |
| import org.apache.drill.common.scanner.persistence.AnnotationDescriptor; |
| import org.apache.drill.common.scanner.persistence.AttributeDescriptor; |
| import org.apache.drill.common.scanner.persistence.ChildClassDescriptor; |
| import org.apache.drill.common.scanner.persistence.FieldDescriptor; |
| import org.apache.drill.common.scanner.persistence.AnnotatedClassDescriptor; |
| import org.apache.drill.common.scanner.persistence.ParentClassDescriptor; |
| import org.apache.drill.common.scanner.persistence.ScanResult; |
| import org.reflections.Reflections; |
| import org.reflections.adapters.JavassistAdapter; |
| import org.reflections.scanners.AbstractScanner; |
| import org.reflections.util.ConfigurationBuilder; |
| import org.reflections.util.FilterBuilder; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import com.google.common.base.Stopwatch; |
| import com.google.common.collect.HashMultimap; |
| import com.google.common.collect.Multimap; |
| |
| import javassist.bytecode.AccessFlag; |
| import javassist.bytecode.AnnotationsAttribute; |
| import javassist.bytecode.ClassFile; |
| import javassist.bytecode.FieldInfo; |
| import javassist.bytecode.annotation.AnnotationMemberValue; |
| import javassist.bytecode.annotation.ArrayMemberValue; |
| import javassist.bytecode.annotation.BooleanMemberValue; |
| import javassist.bytecode.annotation.ByteMemberValue; |
| import javassist.bytecode.annotation.CharMemberValue; |
| import javassist.bytecode.annotation.ClassMemberValue; |
| import javassist.bytecode.annotation.DoubleMemberValue; |
| import javassist.bytecode.annotation.EnumMemberValue; |
| import javassist.bytecode.annotation.FloatMemberValue; |
| import javassist.bytecode.annotation.IntegerMemberValue; |
| import javassist.bytecode.annotation.LongMemberValue; |
| import javassist.bytecode.annotation.MemberValue; |
| import javassist.bytecode.annotation.MemberValueVisitor; |
| import javassist.bytecode.annotation.ShortMemberValue; |
| import javassist.bytecode.annotation.StringMemberValue; |
| |
| /** |
| * Classpath scanning utility. |
| * The classpath should be scanned once at startup from a DrillConfig instance. {@link ClassPathScanner#fromPrescan(DrillConfig)} |
| * The DrillConfig provides: |
| * - the list of packages to scan. (drill.classpath.scanning.packages) {@link ClassPathScanner#IMPLEMENTATIONS_SCAN_PACKAGES} |
| * - the list of base classes to scan for implementations. (drill.classpath.scanning.base.classes) {@link ClassPathScanner#IMPLEMENTATIONS_SCAN_CLASSES} |
| * - the list of annotations to scan for. (drill.classpath.scanning.annotations) {@link ClassPathScanner#IMPLEMENTATIONS_SCAN_ANNOTATIONS} |
| * Only the class directories and jars containing a drill-module.conf will be scanned. |
| * Drill core packages are scanned at build time and the result is saved in a JSON file. |
| * At runtime only the locations that have not been scanned yet will be scanned. |
| */ |
| public final class ClassPathScanner { |
| private static final Logger logger = LoggerFactory.getLogger(ClassPathScanner.class); |
| private static final JavassistAdapter METADATA_ADAPTER = new JavassistAdapter(); |
| |
| /** Configuration pathname to list of names of packages to scan for implementations. */ |
| private static final String IMPLEMENTATIONS_SCAN_PACKAGES = "drill.classpath.scanning.packages"; |
| |
| /** Configuration pathname to list of names of base classes to scan for implementations. */ |
| private static final String IMPLEMENTATIONS_SCAN_CLASSES = "drill.classpath.scanning.base.classes"; |
| |
| /** Configuration pathname to list of names of annotations to scan for. */ |
| private static final String IMPLEMENTATIONS_SCAN_ANNOTATIONS = "drill.classpath.scanning.annotations"; |
| |
| /** Configuration pathname to turn off build time caching. */ |
| public static final String IMPLEMENTATIONS_SCAN_CACHE = "drill.classpath.scanning.cache.enabled"; |
| |
| /** |
| * scans the inheritance tree |
| */ |
| private static class SubTypesScanner extends AbstractScanner { |
| |
| private final Multimap<String, ChildClassDescriptor> parentsChildren = HashMultimap.create(); |
| private final Multimap<String, ChildClassDescriptor> children = HashMultimap.create(); |
| |
| public SubTypesScanner(List<ParentClassDescriptor> parentImplementations) { |
| for (ParentClassDescriptor parentClassDescriptor : parentImplementations) { |
| parentsChildren.putAll(parentClassDescriptor.getName(), parentClassDescriptor.getChildren()); |
| } |
| } |
| |
| @Override |
| public void scan(final Object cls) { |
| final ClassFile classFile = (ClassFile)cls; |
| String className = classFile.getName(); |
| String superclass = classFile.getSuperclass(); |
| boolean isAbstract = (classFile.getAccessFlags() & (AccessFlag.INTERFACE | AccessFlag.ABSTRACT)) != 0; |
| ChildClassDescriptor scannedClass = new ChildClassDescriptor(className, isAbstract); |
| if (!superclass.equals(Object.class.getName())) { |
| children.put(superclass, scannedClass); |
| } |
| for (String anInterface : classFile.getInterfaces()) { |
| children.put(anInterface, scannedClass); |
| } |
| } |
| |
| /** |
| * @param name the class name to get all the children of |
| * @return the class names of all children direct or indirect |
| */ |
| public Set<ChildClassDescriptor> getChildrenOf(String name) { |
| Collection<ChildClassDescriptor> scannedChildren = children.get(name); |
| // add all scanned children |
| Set<ChildClassDescriptor> result = new HashSet<>(scannedChildren); |
| // recursively add children's children |
| Collection<ChildClassDescriptor> allChildren = new ArrayList<>(); |
| allChildren.addAll(scannedChildren); |
| allChildren.addAll(parentsChildren.get(name)); |
| for (ChildClassDescriptor child : allChildren) { |
| result.addAll(getChildrenOf(child.getName())); |
| } |
| return result; |
| } |
| |
| } |
| |
| /** |
| * Converts the annotation attribute value into a list of string to simplify |
| */ |
| private static class ListingMemberValueVisitor implements MemberValueVisitor { |
| private final List<String> values; |
| |
| private ListingMemberValueVisitor(List<String> values) { |
| this.values = values; |
| } |
| |
| @Override |
| public void visitStringMemberValue(StringMemberValue node) { |
| values.add(node.getValue()); |
| } |
| |
| @Override |
| public void visitShortMemberValue(ShortMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitLongMemberValue(LongMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitIntegerMemberValue(IntegerMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitFloatMemberValue(FloatMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitEnumMemberValue(EnumMemberValue node) { |
| values.add(node.getValue()); |
| } |
| |
| @Override |
| public void visitDoubleMemberValue(DoubleMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitClassMemberValue(ClassMemberValue node) { |
| values.add(node.getValue()); |
| } |
| |
| @Override |
| public void visitCharMemberValue(CharMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitByteMemberValue(ByteMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitBooleanMemberValue(BooleanMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| |
| @Override |
| public void visitArrayMemberValue(ArrayMemberValue node) { |
| MemberValue[] nestedValues = node.getValue(); |
| for (MemberValue v : nestedValues) { |
| v.accept(new ListingMemberValueVisitor(values) { |
| @Override |
| public void visitArrayMemberValue(ArrayMemberValue node) { |
| values.add(Arrays.toString(node.getValue())); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void visitAnnotationMemberValue(AnnotationMemberValue node) { |
| values.add(String.valueOf(node.getValue())); |
| } |
| } |
| |
| /** |
| * scans functions annotated with configured annotations |
| * and keeps track of its annotations and fields |
| */ |
| private static final class AnnotationScanner extends AbstractScanner { |
| |
| private final List<AnnotatedClassDescriptor> functions = new ArrayList<>(); |
| |
| private final Set<String> annotationsToScan; |
| |
| AnnotationScanner(Collection<String> annotationsToScan) { |
| super(); |
| this.annotationsToScan = Collections.unmodifiableSet(new HashSet<>(annotationsToScan)); |
| } |
| |
| public List<AnnotatedClassDescriptor> getAnnotatedClasses() { |
| return unmodifiableList(functions); |
| } |
| |
| @Override |
| public void scan(final Object cls) { |
| final ClassFile classFile = (ClassFile)cls; |
| AnnotationsAttribute annotations = ((AnnotationsAttribute)classFile.getAttribute(AnnotationsAttribute.visibleTag)); |
| if (annotations != null) { |
| boolean isAnnotated = false; |
| for (javassist.bytecode.annotation.Annotation a : annotations.getAnnotations()) { |
| if (annotationsToScan.contains(a.getTypeName())) { |
| isAnnotated = true; |
| } |
| } |
| if (isAnnotated) { |
| List<AnnotationDescriptor> classAnnotations = getAnnotationDescriptors(annotations); |
| List<FieldInfo> classFields = classFile.getFields(); |
| List<FieldDescriptor> fieldDescriptors = new ArrayList<>(classFields.size()); |
| for (FieldInfo field : classFields) { |
| String fieldName = field.getName(); |
| AnnotationsAttribute fieldAnnotations = ((AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.visibleTag)); |
| fieldDescriptors.add(new FieldDescriptor(fieldName, field.getDescriptor(), getAnnotationDescriptors(fieldAnnotations))); |
| } |
| functions.add(new AnnotatedClassDescriptor(classFile.getName(), classAnnotations, fieldDescriptors)); |
| } |
| } |
| } |
| |
| private List<AnnotationDescriptor> getAnnotationDescriptors(AnnotationsAttribute annotationsAttr) { |
| if (annotationsAttr == null) { |
| return Collections.emptyList(); |
| } |
| List<AnnotationDescriptor> annotationDescriptors = new ArrayList<>(annotationsAttr.numAnnotations()); |
| for (javassist.bytecode.annotation.Annotation annotation : annotationsAttr.getAnnotations()) { |
| // Sigh: javassist uses raw collections (is this 2002?) |
| Set<String> memberNames = annotation.getMemberNames(); |
| List<AttributeDescriptor> attributes = new ArrayList<>(); |
| if (memberNames != null) { |
| for (String name : memberNames) { |
| MemberValue memberValue = annotation.getMemberValue(name); |
| final List<String> values = new ArrayList<>(); |
| memberValue.accept(new ListingMemberValueVisitor(values)); |
| attributes.add(new AttributeDescriptor(name, values)); |
| } |
| } |
| annotationDescriptors.add(new AnnotationDescriptor(annotation.getTypeName(), attributes)); |
| } |
| return annotationDescriptors; |
| } |
| } |
| |
| /** |
| * @return paths that have a drill config file in them |
| */ |
| static Set<URL> getMarkedPaths(String resourcePathName) { |
| return forResource(resourcePathName, true); |
| } |
| |
| public static Collection<URL> getConfigURLs(String resourcePathName) { |
| return forResource(resourcePathName, false); |
| } |
| |
| /** |
| * Gets URLs of any classpath resources with given resource pathname. |
| * |
| * @param resourcePathname resource pathname of classpath resource instances |
| * to scan for (relative to specified class loaders' classpath roots) |
| * @param returnRootPathname whether to collect classpath root portion of |
| * URL for each resource instead of full URL of each resource |
| * @return empty set if none |
| */ |
| public static Set<URL> forResource(final String resourcePathname, final boolean returnRootPathname) { |
| logger.debug("Scanning classpath for resources with pathname \"{}\".", |
| resourcePathname); |
| final Set<URL> resultUrlSet = new HashSet<>(); |
| final ClassLoader classLoader = ClassPathScanner.class.getClassLoader(); |
| try { |
| final Enumeration<URL> resourceUrls = classLoader.getResources(resourcePathname); |
| while (resourceUrls.hasMoreElements()) { |
| final URL resourceUrl = resourceUrls.nextElement(); |
| logger.trace("- found a(n) {} at {}.", resourcePathname, resourceUrl); |
| int index = resourceUrl.toExternalForm().lastIndexOf(resourcePathname); |
| if (index != -1 && returnRootPathname) { |
| final URL classpathRootUrl = new URL(resourceUrl.toExternalForm().substring(0, index)); |
| resultUrlSet.add(classpathRootUrl); |
| logger.debug("- collected resource's classpath root URL {}.", classpathRootUrl); |
| } else { |
| resultUrlSet.add(resourceUrl); |
| logger.debug("- collected resource URL {}.", resourceUrl); |
| } |
| } |
| } catch (IOException e) { |
| throw new RuntimeException("Error scanning for resources named " + resourcePathname, e); |
| } |
| return resultUrlSet; |
| } |
| |
| static List<String> getPackagePrefixes(DrillConfig config) { |
| return config.getStringList(IMPLEMENTATIONS_SCAN_PACKAGES); |
| } |
| |
| static List<String> getScannedBaseClasses(DrillConfig config) { |
| return config.getStringList(IMPLEMENTATIONS_SCAN_CLASSES); |
| } |
| |
| static List<String> getScannedAnnotations(DrillConfig config) { |
| if (config.hasPath(IMPLEMENTATIONS_SCAN_ANNOTATIONS)) { |
| return config.getStringList(IMPLEMENTATIONS_SCAN_ANNOTATIONS); |
| } else { |
| return Collections.emptyList(); |
| } |
| } |
| |
| static boolean isScanBuildTimeCacheEnabled(DrillConfig config) { |
| if (config.hasPath(IMPLEMENTATIONS_SCAN_CACHE)) { |
| return config.getBoolean(IMPLEMENTATIONS_SCAN_CACHE); |
| } else { |
| return true; // on by default |
| } |
| } |
| |
| /** |
| * |
| * @param pathsToScan the locations to scan for .class files |
| * @param packagePrefixes the whitelist of package prefixes to scan |
| * @param parentResult if there was a prescan, its result |
| * @return the merged scan |
| */ |
| static ScanResult scan(Collection<URL> pathsToScan, Collection<String> packagePrefixes, Collection<String> scannedClasses, Collection<String> scannedAnnotations, ScanResult parentResult) { |
| Stopwatch watch = Stopwatch.createStarted(); |
| try { |
| AnnotationScanner annotationScanner = new AnnotationScanner(scannedAnnotations); |
| SubTypesScanner subTypesScanner = new SubTypesScanner(parentResult.getImplementations()); |
| if (packagePrefixes.size() > 0) { |
| final FilterBuilder filter = new FilterBuilder(); |
| for (String prefix : packagePrefixes) { |
| filter.include(FilterBuilder.prefix(prefix)); |
| } |
| ConfigurationBuilder conf = new ConfigurationBuilder() |
| .setUrls(pathsToScan) |
| .setMetadataAdapter(METADATA_ADAPTER) // Scanners depend on this |
| .filterInputsBy(filter) |
| .setScanners(annotationScanner, subTypesScanner); |
| |
| // scans stuff, but don't use the funky storage layer |
| new Reflections(conf); |
| } |
| List<ParentClassDescriptor> implementations = new ArrayList<>(); |
| for (String baseTypeName: scannedClasses) { |
| implementations.add( |
| new ParentClassDescriptor( |
| baseTypeName, |
| new ArrayList<>(subTypesScanner.getChildrenOf(baseTypeName)))); |
| } |
| List<AnnotatedClassDescriptor> annotated = annotationScanner.getAnnotatedClasses(); |
| verifyClassUnicity(annotated, pathsToScan); |
| return new ScanResult( |
| packagePrefixes, |
| scannedClasses, |
| scannedAnnotations, |
| annotated, |
| implementations); |
| } finally { |
| logger.info( |
| format("Scanning packages %s in locations %s took %dms", |
| packagePrefixes, pathsToScan, watch.elapsed(MILLISECONDS))); |
| } |
| } |
| |
| private static void verifyClassUnicity(List<AnnotatedClassDescriptor> annotatedClasses, Collection<URL> pathsScanned) { |
| Set<String> scanned = new HashSet<>(); |
| for (AnnotatedClassDescriptor annotated : annotatedClasses) { |
| if (!scanned.add(annotated.getClassName())) { |
| throw UserException.functionError() |
| .message( |
| "function %s scanned twice in the following locations:\n" |
| + "%s\n" |
| + "Do you have conflicting jars on the classpath?", |
| annotated.getClassName(), pathsScanned |
| ) |
| .build(logger); |
| } |
| } |
| } |
| |
| static ScanResult emptyResult() { |
| return new ScanResult( |
| Collections.emptyList(), |
| Collections.emptyList(), |
| Collections.emptyList(), |
| Collections.emptyList(), |
| Collections.emptyList()); |
| } |
| |
| public static ScanResult fromPrescan(DrillConfig config) { |
| return RunTimeScan.fromPrescan(config); |
| } |
| } |