blob: 30e9113cee914e0f949be70bf35e5cb19276b973 [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.felix.scrplugin.helper;
import static org.objectweb.asm.ClassReader.SKIP_CODE;
import static org.objectweb.asm.ClassReader.SKIP_DEBUG;
import static org.objectweb.asm.ClassReader.SKIP_FRAMES;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.felix.scrplugin.Log;
import org.apache.felix.scrplugin.Project;
import org.apache.felix.scrplugin.SCRDescriptorException;
import org.apache.felix.scrplugin.SCRDescriptorFailureException;
import org.apache.felix.scrplugin.Source;
import org.apache.felix.scrplugin.annotations.AnnotationProcessor;
import org.apache.felix.scrplugin.annotations.ClassAnnotation;
import org.apache.felix.scrplugin.annotations.FieldAnnotation;
import org.apache.felix.scrplugin.annotations.MethodAnnotation;
import org.apache.felix.scrplugin.annotations.ScannedAnnotation;
import org.apache.felix.scrplugin.annotations.ScannedClass;
import org.apache.felix.scrplugin.description.ClassDescription;
import org.apache.felix.scrplugin.description.ComponentDescription;
import org.apache.felix.scrplugin.description.PropertyDescription;
import org.apache.felix.scrplugin.description.ReferenceDescription;
import org.apache.felix.scrplugin.description.ServiceDescription;
import org.apache.felix.scrplugin.xml.ComponentDescriptorIO;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
/**
* The class scanner scans class files for annotations and invokes
* the {@link AnnotationProcessor} on each scanned class file.
*/
public class ClassScanner {
/**
* The name of the Bundle manifest header providing the list of service
* component descriptor files.
*/
private static final String SERVICE_COMPONENT = "Service-Component";
/**
* The name of the file containing the scanned information from older
* SCR generator versions.
*/
private static final String ABSTRACT_DESCRIPTOR_ARCHIV_PATH = "OSGI-INF/scr-plugin/scrinfo.xml";
/** Source for all generated descriptions. */
private static final String GENERATED = "<generated>";
/** With this syntax array parameters names are returned by reflection API */
private static final Pattern ARRAY_PARAM_TYPE_NAME = Pattern.compile("^\\[L(.*);$");
/** Component descriptions loaded from dependencies*/
private Map<String, ClassDescription> loadedDependencies;
/** All used component descriptions. */
private final Map<String, ClassDescription> allDescriptions;
/** The log. */
private final Log log;
/** The issue log. */
private final IssueLog iLog;
/** The project. */
private final Project project;
/** The annotation processor. */
private final AnnotationProcessor aProcessor;
/**
* Create a new scanner.
*/
public ClassScanner(final Log log,
final IssueLog iLog,
final Project project,
final AnnotationProcessor aProcessor) {
// create map for all descriptions and dummy entry for Object
this.allDescriptions = new HashMap<String, ClassDescription>();
allDescriptions.put(Object.class.getName(), new ClassDescription(Object.class, GENERATED));
this.log = log;
this.iLog = iLog;
this.project = project;
this.aProcessor = aProcessor;
}
/**
* Scan all source class files for annotations and process them.
*/
public List<ClassDescription> scanSources()
throws SCRDescriptorFailureException, SCRDescriptorException {
final List<ClassDescription> result = new ArrayList<ClassDescription>();
for (final Source src : project.getSources()) {
if (src.getFile().getName().equals("package-info.java") || src.getClassName().equals("module-info")) {
log.debug("Skipping file " + src.getClassName());
continue;
}
log.debug("Scanning class " + src.getClassName());
try {
// load the class
final Class<?> annotatedClass = project.getClassLoader().loadClass(src.getClassName());
this.process(annotatedClass, src, result);
} catch ( final SCRDescriptorFailureException e ) {
throw e;
} catch ( final SCRDescriptorException e ) {
throw e;
} catch ( final ClassNotFoundException e ) {
log.warn("ClassNotFoundException: " + e.getMessage());
} catch ( final NoClassDefFoundError e ) {
log.warn("NoClassDefFoundError: " + e.getMessage());
} catch (final Throwable t) {
throw new SCRDescriptorException("Unable to load compiled class: " + src.getClassName(), src.getFile().toString(), t);
}
}
return result;
}
/**
* Process a class
* @throws SCRDescriptorException
* @throws SCRDescriptorFailureException
*/
private void process(final Class<?> annotatedClass, final Source src, final List<ClassDescription> result)
throws SCRDescriptorFailureException, SCRDescriptorException {
final ClassDescription desc = this.processClass(annotatedClass, src.getFile().toString());
if ( desc != null ) {
this.allDescriptions.put(annotatedClass.getName(), desc);
if ( desc.getDescriptions(ComponentDescription.class).size() > 0) {
result.add(desc);
log.debug("Found component description " + desc + " in " + annotatedClass.getName());
} else {
// check whether one of the other annotations is used and log a warning (FELIX-3636)
if ( desc.getDescription(PropertyDescription.class) != null
|| desc.getDescription(ReferenceDescription.class) != null
|| desc.getDescription(ServiceDescription.class) != null ) {
iLog.addWarning("Class '" + src.getClassName() + "' contains SCR annotations, but not a " +
"@Component (or equivalent) annotation. Therefore no component descriptor is created for this " +
"class. Please add a @Component annotation and consider making it abstract.",
src.getFile().toString());
}
}
} else {
this.allDescriptions.put(annotatedClass.getName(), new ClassDescription(annotatedClass, GENERATED));
}
// process inner classes
for(final Class<?> innerClass : annotatedClass.getDeclaredClasses()) {
if ( !innerClass.isAnnotation() && !innerClass.isInterface() ) {
process(innerClass, src, result);
}
}
}
/**
* Scan a single class.
*/
private ClassDescription processClass(final Class<?> annotatedClass, final String location)
throws SCRDescriptorFailureException, SCRDescriptorException {
log.debug("Processing " + annotatedClass.getName());
try {
// get the class file for ASM
final String pathToClassFile = annotatedClass.getName().replace('.', '/') + ".class";
final InputStream input = project.getClassLoader().getResourceAsStream(pathToClassFile);
final ClassReader classReader;
try {
classReader = new ClassReader(input);
} finally {
if ( input != null ) {
input.close();
}
}
final ClassNode classNode = new ClassNode();
classReader.accept(classNode, SKIP_CODE | SKIP_DEBUG | SKIP_FRAMES);
// create descriptions
final List<ScannedAnnotation> annotations = extractAnnotation(classNode, annotatedClass);
if (annotations.size() > 0) {
// process annotations and create descriptions
final ClassDescription desc = new ClassDescription(annotatedClass, location);
aProcessor.process(new ScannedClass(annotations, annotatedClass), desc);
log.debug("Found descriptions " + desc + " in " + annotatedClass.getName());
return desc;
}
} catch (final IllegalArgumentException ioe) {
throw new SCRDescriptorException("Unable to scan class files: " + annotatedClass.getName() + " (Class file format probably not supported by ASM ?)", location, ioe);
} catch (final IOException ioe) {
throw new SCRDescriptorException("Unable to scan class files: " + annotatedClass.getName(), location, ioe);
}
return null;
}
/**
* Extract annotations
*/
private final List<ScannedAnnotation> extractAnnotation(final ClassNode classNode, final Class<?> annotatedClass)
throws SCRDescriptorException {
final List<ScannedAnnotation> descriptions = new ArrayList<ScannedAnnotation>();
// first parse class annotations
@SuppressWarnings("unchecked")
final List<AnnotationNode> annotations = getAllAnnotations(classNode.invisibleAnnotations, classNode.visibleAnnotations);
if (annotations != null) {
for (final AnnotationNode annotation : annotations) {
this.parseAnnotation(descriptions, annotation, annotatedClass);
}
// second parse method annotations
@SuppressWarnings("unchecked")
final List<MethodNode> methods = classNode.methods;
if (methods != null) {
for (final MethodNode method : methods) {
final String name = method.name;
// check for constructor
if ( !"<init>".equals(name) ) {
@SuppressWarnings("unchecked")
final List<AnnotationNode> annos = getAllAnnotations(method.invisibleAnnotations, method.visibleAnnotations);
if (annos != null) {
final Type[] signature = Type.getArgumentTypes(method.desc);
final Method[] allMethods = annotatedClass.getDeclaredMethods();
Method found = null;
for (final Method m : allMethods) {
if (m.getName().equals(name)) {
if (m.getParameterTypes().length == 0 && (signature == null || signature.length == 0) ) {
found = m;
}
if (m.getParameterTypes().length > 0 && signature != null && m.getParameterTypes().length == signature.length) {
found = m;
for(int index = 0; index < m.getParameterTypes().length; index++ ) {
String parameterTypeName = m.getParameterTypes()[index].getName();
// Name of array parameters is returned with syntax [L<name>;, convert to <name>[]
Matcher matcher = ARRAY_PARAM_TYPE_NAME.matcher(parameterTypeName);
if (matcher.matches()) {
parameterTypeName = matcher.group(1) + "[]";
}
if (!parameterTypeName.equals(signature[index].getClassName()) &&
!m.getParameterTypes()[index].getSimpleName().equals(signature[index].getClassName())) {
found = null;
}
}
}
// if method is found return it now, to avoid resetting 'found' to null if next method has same name but different parameters
if (found != null) {
break;
}
}
}
if (found == null) {
throw new SCRDescriptorException("Annotated method " + name + " not found.",
annotatedClass.getName());
}
for (final AnnotationNode annotation : annos) {
parseAnnotation(descriptions, annotation, found);
}
}
}
}
}
// third parse field annotations
@SuppressWarnings("unchecked")
final List<FieldNode> fields = classNode.fields;
if (fields != null) {
for (final FieldNode field : fields) {
@SuppressWarnings("unchecked")
final List<AnnotationNode> annos = getAllAnnotations(field.invisibleAnnotations, field.visibleAnnotations);
if (annos != null) {
final String name = field.name;
final Field[] allFields = annotatedClass.getDeclaredFields();
Field found = null;
for (final Field f : allFields) {
if (f.getName().equals(name)) {
found = f;
break;
}
}
if (found == null) {
throw new SCRDescriptorException("Annotated field " + name + " not found.",
annotatedClass.getName());
}
for (final AnnotationNode annotation : annos) {
parseAnnotation(descriptions, annotation, found);
}
}
}
}
}
return descriptions;
}
/**
* Method is used to get both invisible (e.g. RetentionPolicy.CLASS) and visible (e.g. RetentionPolicy.RUNTIME) annotations.
* Although it is recommended to use RetentionPolicy.CLASS for SCR annotations, it may make sense to declae them with another
* RetentionPolicy if the same annotation is used for other usecases which require runtime access as well.
* @param annotationLists List of invisible and visible annotations.
* @return List with all annotations from all lists, or null if none found
*/
private List<AnnotationNode> getAllAnnotations(List<AnnotationNode>... annotationLists) {
List<AnnotationNode> resultList = null;
for (List<AnnotationNode> annotationList : annotationLists) {
if (annotationList!=null && annotationList.size()>0) {
if (resultList==null) {
resultList = new ArrayList<AnnotationNode>();
}
resultList.addAll(annotationList);
}
}
return resultList;
}
private <T> T[] convertToArray(final List<?> values, final Class<T> type) {
@SuppressWarnings("unchecked")
final T[] result = (T[]) Array.newInstance(type, values.size());
return values.toArray(result);
}
/**
* Parse annotation and create a description.
*/
private void parseAnnotation(final List<ScannedAnnotation> descriptions, final AnnotationNode annotation,
final Object annotatedObject) {
// desc has the format 'L' + className.replace('.', '/') + ';'
final String name = annotation.desc.substring(1, annotation.desc.length() - 1).replace('/', '.');
Map<String, Object> values = null;
if (annotation.values != null) {
values = new HashMap<String, Object>();
final Iterator<?> i = annotation.values.iterator();
while (i.hasNext()) {
final Object vName = i.next();
Object value = i.next();
// convert type to class name string
if (value instanceof Type) {
value = ((Type) value).getClassName();
} else if (value instanceof List<?>) {
final List<?> objects = (List<?>) value;
if (objects.size() > 0) {
if (objects.get(0) instanceof Type) {
final String[] classNames = new String[objects.size()];
int index = 0;
for (final Object v : objects) {
classNames[index] = ((Type) v).getClassName();
index++;
}
value = classNames;
} else if (objects.get(0) instanceof AnnotationNode) {
final List<ScannedAnnotation> innerDesc = new ArrayList<ScannedAnnotation>();
for (final Object v : objects) {
parseAnnotation(innerDesc, (AnnotationNode) v, annotatedObject);
}
if (annotatedObject instanceof Method) {
value = innerDesc.toArray(new MethodAnnotation[innerDesc.size()]);
} else if (annotatedObject instanceof Field) {
value = innerDesc.toArray(new FieldAnnotation[innerDesc.size()]);
} else {
value = innerDesc.toArray(new ClassAnnotation[innerDesc.size()]);
}
} else {
value = convertToArray(objects, objects.get(0).getClass());
}
} else {
value = null;
}
}
values.put(vName.toString(), value);
}
}
final ScannedAnnotation a;
if (annotatedObject instanceof Method) {
a = new MethodAnnotation(name, values, (Method) annotatedObject);
((Method) annotatedObject).setAccessible(true);
} else if (annotatedObject instanceof Field) {
a = new FieldAnnotation(name, values, (Field) annotatedObject);
((Field) annotatedObject).setAccessible(true);
} else {
a = new ClassAnnotation(name, values);
}
descriptions.add(a);
}
/**
* Get a description for the class
*/
public ClassDescription getDescription(final Class<?> clazz)
throws SCRDescriptorException, SCRDescriptorFailureException {
final String name = clazz.getName();
// we don't need to scan classes in the java. or javax. package namespace
if ( name.startsWith("java.") || name.startsWith("javax.") ) {
return null;
}
ClassDescription result = this.allDescriptions.get(name);
if ( result == null ) {
// use scanner first
result = this.processClass(clazz, GENERATED);
if ( result == null ) {
// now check loaded dependencies
result = this.getComponentDescriptors().get(name);
}
// not found, create dummy
if ( result == null ) {
result = new ClassDescription(clazz, GENERATED);
}
// and cache
allDescriptions.put(name, result);
}
return result.clone();
}
/**
* Returns a map of component descriptors which may be extended by the java
* sources.
* <p>
* This method calls the {@link #getDependencies()} method and checks for
* any Service-Component descriptors in the returned files.
* <p>
* This method may be overwritten by extensions of this class.
*
* @throws SCRDescriptorException May be thrown if an error occurs
* gathering the component descriptors.
*/
private Map<String, ClassDescription> getComponentDescriptors()
throws SCRDescriptorException {
if ( loadedDependencies == null ) {
loadedDependencies = new HashMap<String, ClassDescription>();
final Collection<File> dependencies = this.project.getDependencies();
for ( final File artifact : dependencies ) {
try {
this.log.debug( "Trying to get scrinfo from artifact " + artifact );
// First try to read the private scr info file from previous scr generator versions
InputStream scrInfoFile = null;
try {
scrInfoFile = this.getFile( artifact, ABSTRACT_DESCRIPTOR_ARCHIV_PATH );
if ( scrInfoFile != null ) {
this.readServiceComponentDescriptor( scrInfoFile, artifact.toString() + ':' + ABSTRACT_DESCRIPTOR_ARCHIV_PATH);
continue;
}
this.log.debug( "Artifact has no scrinfo file (it's optional): " + artifact );
} catch ( final IOException ioe ) {
throw new SCRDescriptorException( "Unable to get scrinfo from artifact", artifact.toString(),
ioe );
} finally {
if ( scrInfoFile != null ) {
try { scrInfoFile.close(); } catch ( final IOException ignore ) {}
}
}
this.log.debug( "Trying to get manifest from artifact " + artifact );
final Manifest manifest = this.getManifest( artifact );
if ( manifest != null ) {
// read Service-Component entry
if ( manifest.getMainAttributes().getValue( SERVICE_COMPONENT ) != null ) {
final String serviceComponent = manifest.getMainAttributes().getValue(SERVICE_COMPONENT );
this.log.debug( "Found Service-Component: " + serviceComponent + " in artifact " + artifact );
final StringTokenizer st = new StringTokenizer( serviceComponent, "," );
while ( st.hasMoreTokens() ) {
final String entry = st.nextToken().trim();
if ( entry.length() > 0 ) {
this.readServiceComponentDescriptor( artifact, entry );
}
}
} else {
this.log.debug( "Artifact has no service component entry in manifest " + artifact );
}
} else {
this.log.debug( "Unable to get manifest from artifact " + artifact );
}
} catch ( IOException ioe ) {
throw new SCRDescriptorException( "Unable to get manifest from artifact", artifact.toString(),
ioe );
}
}
}
return this.loadedDependencies;
}
/**
* Parses the descriptors read from the given input stream. This method may
* be called by the {@link #getComponentDescriptors()} method to parse the
* descriptors gathered in an implementation dependent way.
*
* @throws SCRDescriptorException If an error occurs reading the
* descriptors from the stream.
*/
private void readServiceComponentDescriptor(
final InputStream file, final String location )
throws SCRDescriptorException {
final List<ClassDescription> list = ComponentDescriptorIO.read( file, this.project.getClassLoader(), iLog, location );
if ( list != null ) {
for(final ClassDescription cd : list) {
final String name;
if ( cd.getDescribedClass() == null ) {
name = cd.getDescription(ComponentDescription.class).getName();
} else {
name = cd.getDescribedClass().getName();
}
loadedDependencies.put(name, cd);
}
}
}
/**
* Read the service component description.
*
* @param artifact
* @param entry
* @throws IOException
* @throws SCRDescriptorException
*/
private void readServiceComponentDescriptor( final File artifactFile, String entry ) {
this.log.debug( "Reading " + entry + " from " + artifactFile );
InputStream xml = null;
try {
xml = this.getFile( artifactFile, entry );
if ( xml == null ) {
throw new SCRDescriptorException( "Entry " + entry + " not contained in JAR File ", artifactFile.toString());
}
this.readServiceComponentDescriptor( xml, artifactFile.toString() + ':' + entry );
} catch ( final IOException mee ) {
this.log.warn( "Unable to read SCR descriptor file from JAR File " + artifactFile + " at " + entry );
this.log.debug( "Exception occurred during reading: " + mee.getMessage(), mee );
} catch ( final SCRDescriptorException mee ) {
this.log.warn( "Unable to read SCR descriptor file from JAR File " + artifactFile + " at " + entry );
this.log.debug( "Exception occurred during reading: " + mee.getMessage(), mee );
} finally {
if ( xml != null ) {
try { xml.close(); } catch (final IOException ignore) {}
}
}
}
/**
* Get the manifest from the artifact.
* The artifact can either be a jar or a directory.
*/
private Manifest getManifest( final File artifact ) throws IOException {
if ( artifact.isDirectory() ) {
// this is maybe a classes directory, try to read manifest file directly
final File dir = new File(artifact, "META-INF");
if ( !dir.exists() || !dir.isDirectory() ) {
return null;
}
final File mf = new File(dir, "MANIFEST.MF");
if ( !mf.exists() || !mf.isFile() ) {
return null;
}
final InputStream is = new FileInputStream(mf);
try {
return new Manifest(is);
} finally {
try { is.close(); } catch (final IOException ignore) { }
}
}
JarFile file = null;
try {
file = new JarFile( artifact );
return file.getManifest();
} finally {
if ( file != null ) {
try { file.close(); } catch ( final IOException ignore ) {}
}
}
}
private InputStream getFile( final File artifactFile, final String path ) throws IOException {
if ( artifactFile.isDirectory() ) {
final String filePath = path.replace('/', File.separatorChar).replace('\\', File.separatorChar);
final File file = new File(artifactFile, filePath);
if ( file.exists() && file.isFile() ) {
return new FileInputStream(file);
}
return null;
}
JarFile file = null;
try {
file = new JarFile( artifactFile );
final JarEntry entry = file.getJarEntry( path );
if ( entry != null ) {
final InputStream stream = new ArtifactFileInputStream( file, entry );
file = null; // prevent file from being closed now
return stream;
}
return null;
} finally {
if ( file != null ) {
try { file.close(); } catch ( final IOException ignore ) {}
}
}
}
private static class ArtifactFileInputStream extends FilterInputStream {
final JarFile jarFile;
ArtifactFileInputStream( JarFile jarFile, JarEntry jarEntry ) throws IOException {
super( jarFile.getInputStream( jarEntry ) );
this.jarFile = jarFile;
}
@Override
public void close() throws IOException {
try {
super.close();
} catch ( final IOException ioe ) {
// ignore
}
jarFile.close();
}
@Override
protected void finalize() throws Throwable {
try {
close();
} finally {
super.finalize();
}
}
}
}