Package org.apache.felix.scrplugin.helper

Source Code of org.apache.felix.scrplugin.helper.ClassScanner$ArtifactFileInputStream

/*
* 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 ClassNotFoundException cnfe) {
                throw new SCRDescriptorException("Unable to load compiled class: " + src.getClassName(), src.getFile().toString(), cnfe);
            }
        }
        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();
            }
        }
    }
}
TOP

Related Classes of org.apache.felix.scrplugin.helper.ClassScanner$ArtifactFileInputStream

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.