blob: a5fafe71bc3791cf619b70e98e13119774564f0a [file] [log] [blame]
/*
* 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);
}
}