/* * 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") ) { 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 { 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(); } } } }