All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.weaver.Finder Maven / Gradle / Ivy

Go to download

Defines the Apache Commons Weaver SPI as well as the basic build-time (filesystem-based) processors that detect, configure, and invoke available modules.

The newest version!
/*
 * 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.commons.weaver;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.weaver.model.ScanRequest;
import org.apache.commons.weaver.model.ScanResult;
import org.apache.commons.weaver.model.Scanner;
import org.apache.commons.weaver.model.WeaveInterest;
import org.apache.commons.weaver.utils.Annotations;
import org.apache.xbean.asm6.AnnotationVisitor;
import org.apache.xbean.asm6.ClassReader;
import org.apache.xbean.asm6.ClassVisitor;
import org.apache.xbean.asm6.FieldVisitor;
import org.apache.xbean.asm6.MethodVisitor;
import org.apache.xbean.asm6.Opcodes;
import org.apache.xbean.asm6.Type;
import org.apache.xbean.finder.Annotated;
import org.apache.xbean.finder.AnnotationFinder;
import org.apache.xbean.finder.Parameter;
import org.apache.xbean.finder.archive.Archive;

/**
 * Scanner implementation.
 */
class Finder extends AnnotationFinder implements Scanner {

    private abstract class AnnotationInflater extends AnnotationCapturer {
        final Class annotationType;
        final Map elements = new LinkedHashMap<>();

        AnnotationInflater(final String desc, final AnnotationVisitor wrapped) {
            super(wrapped);
            this.annotationType = toClass(Type.getType(desc)).asSubclass(Annotation.class);
        }

        Annotation inflate() {
            return Annotations.instanceOf(annotationType, elements);
        }

        @Override
        protected void storeValue(final String name, final Object value) {
            Object toStore = value;
            Validate.notNull(toStore, "null annotation element");
            if (toStore.getClass().isArray()) {
                final Class requiredType;
                try {
                    requiredType = annotationType.getDeclaredMethod(name).getReturnType();
                } catch (final Exception e) {
                    throw new RuntimeException(e);
                }
                if (!requiredType.isInstance(toStore)) {
                    final int len = Array.getLength(toStore);
                    final Object typedArray = Array.newInstance(requiredType.getComponentType(), len);
                    for (int i = 0; i < len; i++) {
                        Object element = Array.get(toStore, i);
                        if (element instanceof Type) {
                            element = toClass((Type) element);
                        }
                        Array.set(typedArray, i, element);
                    }
                    toStore = typedArray;
                }
            } else if (toStore instanceof Type) {
                toStore = toClass((Type) toStore);
            }
            elements.put(name, toStore);
        }
    }

    private abstract class AnnotationCapturer extends AnnotationVisitor {
        AnnotationCapturer(final AnnotationVisitor wrapped) {
            super(ASM_VERSION, wrapped);
        }

        /**
         * Template method for storing an annotation value.
         * @param name
         * @param value
         */
        protected abstract void storeValue(String name, Object value);

        @Override
        public void visit(final String name, final Object value) {
            storeValue(name, value);
        }

        @Override
        public AnnotationVisitor visitAnnotation(final String name, final String desc) {
            final AnnotationCapturer owner = this;
            return new AnnotationInflater(desc, super.visitAnnotation(name, desc)) {

                @Override
                public void visitEnd() {
                    owner.storeValue(name, inflate());
                }
            };
        }

        @Override
        public AnnotationVisitor visitArray(final String name) {
            final AnnotationCapturer owner = this;
            final List values = new ArrayList<>();
            return new AnnotationCapturer(super.visitArray(name)) {

                @Override
                public void visitEnd() {
                    owner.storeValue(name, values.toArray());
                    super.visitEnd();
                }

                @Override
                protected void storeValue(final String name, final Object value) {
                    values.add(value);
                }
            };
        }

        @Override
        public void visitEnum(final String name, final String desc, final String value) {
            super.visitEnum(name, desc, value);
            @SuppressWarnings("rawtypes")
            final Class enumType;
            try {
                enumType = Class.forName(Type.getType(desc).getClassName()).asSubclass(Enum.class);
            } catch (final ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            @SuppressWarnings("unchecked")
            final Enum enumValue = Enum.valueOf(enumType, value);
            storeValue(name, enumValue);
        }

    }

    private class TopLevelAnnotationInflater extends AnnotationInflater {
        private final Info info;

        TopLevelAnnotationInflater(final String desc, final AnnotationVisitor wrapped, final Info info) {
            super(desc, wrapped);
            this.info = info;
        }

        @Override
        public void visitEnd() {
            super.visitEnd();
            CLASSFILE_ANNOTATIONS.get().computeIfAbsent(info, k -> new ArrayList<>()).add(inflate());
        }
    }

    /**
     * Specialized {@link ClassVisitor} to inflate annotations for the info
     * objects built by a wrapped {@link InfoBuildingVisitor}.
     */
    public class Visitor extends ClassVisitor {
        private final InfoBuildingVisitor wrapped;

        Visitor(final InfoBuildingVisitor wrapped) {
            super(ASM_VERSION, wrapped);
            this.wrapped = wrapped;
        }

        @Override
        public FieldVisitor visitField(final int access, final String name, final String desc, final String signature,
            final Object value) {
            final FieldVisitor toWrap = wrapped.visitField(access, name, desc, signature, value);
            final ClassInfo classInfo = (ClassInfo) wrapped.getInfo();
            final Type fieldType = Type.getType(desc);
            FieldInfo testFieldInfo = null;
            // should be the most recently added field, so iterate backward:
            for (int i = classInfo.getFields().size() - 1; i >= 0; i--) {
                final FieldInfo atI = classInfo.getFields().get(i);
                if (atI.getName().equals(name)) {
                    final String type = atI.getType();
                    if (StringUtils.equals(type, fieldType.getClassName())
                        || StringUtils.equals(type, fieldType.getDescriptor())) {
                        testFieldInfo = atI;
                        break;
                    }
                }
            }
            if (testFieldInfo == null) {
                return toWrap;
            }
            final FieldInfo fieldInfo = testFieldInfo;
            return new FieldVisitor(ASM_VERSION, toWrap) {
                @Override
                public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
                    final AnnotationVisitor toWrap = super.visitAnnotation(desc, visible);
                    return visible ? toWrap : new TopLevelAnnotationInflater(desc, toWrap, fieldInfo);
                }
            };
        }

        @Override
        @SuppressWarnings("PMD.UseVarargs") // overridden method
        public MethodVisitor visitMethod(final int access, final String name, final String desc,
            final String signature, final String[] exceptions) {
            final MethodVisitor toWrap = wrapped.visitMethod(access, name, desc, signature, exceptions);
            final ClassInfo classInfo = (ClassInfo) wrapped.getInfo();

            // MethodInfo may not always come from a descriptor, so we must go by the
            // Member represented. Make sure the method either has a valid name or is a constructor:
            final MethodInfo compareMethodInfo = new MethodInfo(classInfo, name, desc);
            if (!compareMethodInfo.isConstructor() && !isJavaIdentifier(name)) {
                return toWrap;
            }
            MethodInfo testMethodInfo = null;
            // should be the most recently added method, so iterate backward:
            for (int i = classInfo.getMethods().size() - 1; i >= 0; i--) {
                final MethodInfo atI = classInfo.getMethods().get(i);
                if (atI.getName().equals(name) && StringUtils.equals(atI.getDescriptor(), desc)) {
                    testMethodInfo = atI;
                    break;
                }
            }
            if (testMethodInfo == null) {
                return toWrap;
            }
            final MethodInfo methodInfo = testMethodInfo;
            return new MethodVisitor(ASM_VERSION, toWrap) {
                @Override
                public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
                    final AnnotationVisitor toWrap = super.visitAnnotation(desc, visible);
                    return visible ? toWrap : new TopLevelAnnotationInflater(desc, toWrap, methodInfo);
                }

                @Override
                public AnnotationVisitor visitParameterAnnotation(final int param, final String desc,
                    final boolean visible) {
                    final AnnotationVisitor toWrap = super.visitParameterAnnotation(param, desc, visible);
                    if (visible) {
                        return toWrap;
                    }
                    ParameterInfo parameterInfo = null;

                    // should be the most recently added parameter, so iterate backward:
                    for (int i = methodInfo.getParameters().size() - 1; i >= 0; i--) {
                        final ParameterInfo atI = methodInfo.getParameters().get(i);
                        if (atI.getName().equals(Integer.toString(param))) {
                            parameterInfo = atI;
                            break;
                        }
                    }
                    return parameterInfo == null ? toWrap : new TopLevelAnnotationInflater(desc, toWrap, parameterInfo);
                }
            };
        }

        @Override
        public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
            final AnnotationVisitor toWrap = super.visitAnnotation(desc, visible);
            return visible ? toWrap : new TopLevelAnnotationInflater(desc, toWrap, wrapped.getInfo());
        }

        private boolean isJavaIdentifier(final String toCheck) {
            if (toCheck.isEmpty() || !Character.isJavaIdentifierStart(toCheck.charAt(0))) {
                return false;
            }
            for (final char chr : toCheck.substring(1).toCharArray()) {
                if (!Character.isJavaIdentifierPart(chr)) {
                    return false;
                }
            }
            return true;
        }
    }

    private static class IncludesClassfile implements Annotated {
        private final T annotatedElement;
        private final Annotation[] annotations;

        IncludesClassfile(final T annotatedElement, final List classfileAnnotations) {
            this(annotatedElement, classfileAnnotations.toArray(new Annotation[0]));
        }

        @SuppressWarnings("PMD.UseVarargs") // varargs not necessary here
        IncludesClassfile(final T annotatedElement, final Annotation[] classfileAnnotations) {
            super();
            this.annotatedElement = annotatedElement;
            this.annotations = ArrayUtils.addAll(annotatedElement.getAnnotations(), classfileAnnotations);
        }

        @Override
        public  A getAnnotation(final Class annotationType) {
            for (final Annotation prospect : annotations) {
                if (prospect.annotationType().equals(annotationType)) {
                    @SuppressWarnings("unchecked")
                    final A result = (A) prospect;
                    return result;
                }
            }
            return null;
        }

        @Override
        public Annotation[] getAnnotations() {
            final Annotation[] result = new Annotation[annotations.length];
            System.arraycopy(annotations, 0, result, 0, annotations.length);
            return result;
        }

        @Override
        public Annotation[] getDeclaredAnnotations() {
            return getAnnotations();
        }

        @Override
        public boolean isAnnotationPresent(final Class annotationType) {
            return getAnnotation(annotationType) != null;
        }

        @Override
        public T get() {
            return annotatedElement;
        }
    }

    /**
     * Helper class for finding elements with annotations (including those with classfile-level retention).
     */
    public final class WithAnnotations {
        private WithAnnotations() {
        }

        public List> findAnnotatedPackages(final Class annotation) {
            Finder.this.findAnnotatedPackages(annotation);

            return typed(PackageInfo.class, getAnnotationInfos(annotation.getName())::stream).map(packageInfo -> {
                try {
                    return new IncludesClassfile<>(packageInfo.get(), classfileAnnotationsFor(packageInfo));
                } catch (ClassNotFoundException e) {
                    return null;
                }
            }).filter(hasAnnotation(annotation)).collect(Collectors.toList());
        }

        /**
         * Get the list of objects representing all scanned classes.
         * @since 1.3
         * @return {@link List} of {@link Annotated}{@code >}
         */
        public List>> getAllClasses() {
            return annotate(originalInfos.values());
        }

        public List>> findAnnotatedClasses(final Class annotation) {
            Finder.this.findAnnotatedClasses(annotation);

            return annotate(getAnnotationInfos(annotation.getName())).stream().filter(hasAnnotation(annotation))
                .collect(Collectors.toList());
        }

        private List>> annotate(final Collection infos) {
            return typed(ClassInfo.class, infos::stream).map(classInfo -> {
                try {
                    return new IncludesClassfile>(classInfo.get(), classfileAnnotationsFor(classInfo));
                } catch (ClassNotFoundException e1) {
                    return null;
                }
            }).filter(Objects::nonNull).collect(Collectors.toList());
        }

        public List>> findAssignableTypes(final Class supertype) {
            @SuppressWarnings({ "unchecked", "rawtypes" })
            final List> assignableTypes = (List) (supertype.isInterface()
                ? Finder.this.findImplementations(supertype) : Finder.this.findSubclasses(supertype));

            return assignableTypes.stream().map(Class::getName).map(classInfos::get)
                .>> map(classInfo -> {
                    try {
                        return new IncludesClassfile<>(classInfo.get(), classfileAnnotationsFor(classInfo));
                    } catch (final ClassNotFoundException e) {
                        return null;
                    }
                }).filter(Objects::nonNull).collect(Collectors.toList());
        }

        public List> findAnnotatedMethods(final Class annotation) {
            Finder.this.findAnnotatedMethods(annotation);

            return typed(MethodInfo.class, getAnnotationInfos(annotation.getName())::stream).filter(CTOR.negate())
                .map(methodInfo -> {
                    try {
                        return new IncludesClassfile<>((Method) methodInfo.get(),
                            classfileAnnotationsFor(methodInfo));
                    } catch (final ClassNotFoundException e) {
                        return null;
                    }
                }).filter(hasAnnotation(annotation)).collect(Collectors.toList());
        }

        public List>> findAnnotatedMethodParameters(
            final Class annotationType) {
            Finder.this.findAnnotatedMethodParameters(annotationType);

            return typed(ParameterInfo.class, getAnnotationInfos(annotationType.getName())::stream)
                .filter(isCtor(ParameterInfo::getDeclaringMethod).negate()).map(parameterInfo -> {
                    try {
                        @SuppressWarnings("unchecked")
                        final Parameter parameter = (Parameter) parameterInfo.get();
                        return new IncludesClassfile<>(parameter, classfileAnnotationsFor(parameterInfo));
                    } catch (final ClassNotFoundException e) {
                        return null;
                    }
                }).filter(hasAnnotation(annotationType)).collect(Collectors.toList());
        }

        public List>> findAnnotatedConstructors(final Class annotation) {
            Finder.this.findAnnotatedConstructors(annotation);

            return typed(MethodInfo.class, getAnnotationInfos(annotation.getName())::stream).filter(CTOR)
                .map(methodInfo -> {
                    try {
                        final IncludesClassfile> annotated = new IncludesClassfile<>(
                            (Constructor) methodInfo.get(), classfileAnnotationsFor(methodInfo));
                        return annotated;
                    } catch (final ClassNotFoundException e) {
                        return null;
                    }
                }).filter(hasAnnotation(annotation)).collect(Collectors.toList());
        }

        public List>>> findAnnotatedConstructorParameters(
            final Class annotation) {
            Finder.this.findAnnotatedConstructorParameters(annotation);

            return typed(ParameterInfo.class, getAnnotationInfos(annotation.getName())::stream)
                .filter(isCtor(ParameterInfo::getDeclaringMethod)).map(parameterInfo -> {
                    try {
                        @SuppressWarnings("unchecked")
                        final Parameter> parameter = (Parameter>) parameterInfo.get();
                        return new IncludesClassfile<>(parameter, classfileAnnotationsFor(parameterInfo));
                    } catch (final ClassNotFoundException e) {
                        return null;
                    }
                }).filter(hasAnnotation(annotation)).collect(Collectors.toList());
        }

        public List> findAnnotatedFields(final Class annotation) {
            Finder.this.findAnnotatedFields(annotation);

            return typed(FieldInfo.class, getAnnotationInfos(annotation.getName())::stream).map(fieldInfo -> {
                try {
                    return new IncludesClassfile<>((Field) fieldInfo.get(), classfileAnnotationsFor(fieldInfo));
                } catch (final ClassNotFoundException e) {
                    return null;
                }
            }).filter(hasAnnotation(annotation)).collect(Collectors.toList());
        }

        private List classfileAnnotationsFor(final Info info) {
            return classfileAnnotations.computeIfAbsent(info, k -> new ArrayList<>());
        }
    }

    private static final int ASM_FLAGS = ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES;

    private static final String INIT = "";

    /**
     * ASM version in use.
     */
    static final int ASM_VERSION = Opcodes.ASM6;

    /**
     * Ctor {@link Predicate}.
     */
    static final Predicate CTOR = methodInfo -> INIT.equals(methodInfo.getName());

    /**
     * The {@link #classfileAnnotations} member stores these; however the scanning takes place in the scope of the super
     * constructor call, thus there is no opportunity to set the reference beforehand. To work around this, we use a
     * static ThreadLocal with an initializer and pull/clear its value when we return from the super constructor. :P
     */
    static final ThreadLocal>> CLASSFILE_ANNOTATIONS =
        ThreadLocal.withInitial(IdentityHashMap::new);

    /**
     * Filter and cast {@code stream}.
     * @param type
     * @param stream
     * @return {@link Stream}
     */
    static  Stream typed(final Class type, final Supplier> stream) {
        return stream.get().filter(type::isInstance).map(type::cast);
    }

    /**
     * Obtain a {@link Predicate} to test whether an {@link Annotated} instance
     * hosts annotations of the specified type.
     *
     * @param annotation
     * @return {@link Predicate}
     */
    static Predicate> hasAnnotation(final Class annotation) {
        return annotated -> annotated != null && annotated.isAnnotationPresent(annotation);
    }

    /**
     * Obtain a {@link Predicate} to test whether an argument, once transformed
     * by the specified {@link Function}, represents a Java constructor.
     *
     * @param xform
     * @return {@link Predicate}
     */
    static  Predicate isCtor(final Function xform) {
        return t -> CTOR.test(xform.apply(t));
    }

    /**
     * Map of {@link Info} to {@link List} of classfile {@link Annotation}s.
     */
    final Map> classfileAnnotations;

    private final WithAnnotations withAnnotations = new WithAnnotations();
    private final Inflater inflater;

    /**
     * Create a new {@link Finder} instance.
     * @param archive
     */
    Finder(final Archive archive) {
        super(archive, false);
        classfileAnnotations = CLASSFILE_ANNOTATIONS.get();
        CLASSFILE_ANNOTATIONS.remove();
        inflater = new Inflater(classfileAnnotations);
        enableFindImplementations();
        enableFindSubclasses();
    }

    /**
     * Fluent "finder with annotations".
     * @return {@link WithAnnotations}
     */
    public WithAnnotations withAnnotations() {
        return withAnnotations;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void readClassDef(final InputStream bytecode) throws IOException {
        try {
            new ClassReader(bytecode).accept(new Visitor(new InfoBuildingVisitor()), ASM_FLAGS);
        } finally {
            bytecode.close();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AnnotationFinder select(final Class... arg0) {
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AnnotationFinder select(final Iterable clazz) {
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AnnotationFinder select(final String... clazz) {
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ScanResult scan(final ScanRequest request) {
        final ScanResult result = new ScanResult();

        if (!request.isConstrained() || request.getSupertypes().contains(Object.class)) {
            for (final Annotated> type : this.withAnnotations().getAllClasses()) {
                result.getWeavable(type.get()).addAnnotations(type.getAnnotations());
            }
        } else {
            for (final WeaveInterest interest : request.getInterests()) {
                final ElementType target = interest.target;
                switch (target) {
                case PACKAGE:
                    for (final Annotated pkg : this.withAnnotations().findAnnotatedPackages(
                        interest.annotationType)) {
                        result.getWeavable(pkg.get()).addAnnotations(pkg.getAnnotations());
                    }
                    break;
                case TYPE:
                    for (final Annotated> type : this.withAnnotations().findAnnotatedClasses(
                        interest.annotationType)) {
                        result.getWeavable(type.get()).addAnnotations(type.getAnnotations());
                    }
                    break;
                case METHOD:
                    for (final Annotated method : this.withAnnotations().findAnnotatedMethods(
                        interest.annotationType)) {
                        result.getWeavable(method.get()).addAnnotations(method.getAnnotations());
                    }
                    break;
                case CONSTRUCTOR:
                    for (final Annotated> ctor : this.withAnnotations().findAnnotatedConstructors(
                        interest.annotationType)) {
                        result.getWeavable(ctor.get()).addAnnotations(ctor.getAnnotations());
                    }
                    break;
                case FIELD:
                        for (final Annotated fld : this.withAnnotations()
                            .findAnnotatedFields(interest.annotationType)) {
                            result.getWeavable(fld.get()).addAnnotations(fld.getAnnotations());
                        }
                        break;
                case PARAMETER:
                    for (final Annotated> parameter : this.withAnnotations()
                        .findAnnotatedMethodParameters(interest.annotationType)) {
                            result.getWeavable(parameter.get().getDeclaringExecutable())
                                .getWeavableParameter(parameter.get().getIndex())
                                .addAnnotations(parameter.getAnnotations());
                        }
                    for (final Annotated>> parameter : this.withAnnotations()
                        .findAnnotatedConstructorParameters(interest.annotationType)) {
                            result.getWeavable(parameter.get().getDeclaringExecutable())
                                .getWeavableParameter(parameter.get().getIndex())
                                .addAnnotations(parameter.getAnnotations());
                        }
                    break;
                default:
                    // should we log something?
                    break;
                }
            }
            request.getSupertypes().stream().map(this.withAnnotations()::findAssignableTypes)
                .flatMap(Collection::stream)
                .forEach(type -> result.getWeavable(type.get()).addAnnotations(type.getAnnotations()));
        }
        return inflater.inflate(result);
    }

    /**
     * Transform a {@link java.lang.reflect.Type} instance to a {@link Class}.
     * @param type
     * @return {@link Class}
     */
    Class toClass(final Type type) {
        final String className;
        if (type.getSort() == Type.ARRAY) {
            className = type.getElementType().getClassName();
        } else {
            className = type.getClassName();
        }
        Class result;
        try {
            result = Class.forName(className);
        } catch (final ClassNotFoundException e) {
            try {
                result = getArchive().loadClass(className);
            } catch (final ClassNotFoundException e1) {
                throw new RuntimeException(e1);
            }
        }
        if (type.getSort() == Type.ARRAY) {
            final int[] dims = new int[type.getDimensions()];
            Arrays.fill(dims, 0);
            result = Array.newInstance(result, dims).getClass();
        }
        return result;
    }
}