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

org.testifyproject.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder Maven / Gradle / Ivy

The newest version!
package org.testifyproject.bytebuddy.implementation.bind.annotation;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.EqualsAndHashCode;
import org.testifyproject.bytebuddy.description.annotation.AnnotationDescription;
import org.testifyproject.bytebuddy.description.field.FieldDescription;
import org.testifyproject.bytebuddy.description.method.MethodDescription;
import org.testifyproject.bytebuddy.description.method.ParameterDescription;
import org.testifyproject.bytebuddy.description.type.TypeDescription;
import org.testifyproject.bytebuddy.dynamic.scaffold.FieldLocator;
import org.testifyproject.bytebuddy.implementation.Implementation;
import org.testifyproject.bytebuddy.implementation.bind.MethodDelegationBinder;
import org.testifyproject.bytebuddy.implementation.bytecode.StackManipulation;
import org.testifyproject.bytebuddy.implementation.bytecode.assign.Assigner;
import org.testifyproject.bytebuddy.implementation.bytecode.constant.*;
import org.testifyproject.bytebuddy.utility.JavaConstant;
import org.testifyproject.bytebuddy.utility.JavaType;

import java.lang.annotation.Annotation;
import java.util.*;

import static org.testifyproject.bytebuddy.matcher.ElementMatchers.isGetter;
import static org.testifyproject.bytebuddy.matcher.ElementMatchers.isSetter;

/**
 * This {@link org.testifyproject.bytebuddy.implementation.bind.MethodDelegationBinder} binds
 * method by analyzing annotations found on the target method that is subject to a method binding.
 */
@EqualsAndHashCode
public class TargetMethodAnnotationDrivenBinder implements MethodDelegationBinder {

    /**
     * The processor for performing an actual method delegation.
     */
    private final DelegationProcessor delegationProcessor;

    /**
     * Creates a new target method annotation-driven binder.
     *
     * @param delegationProcessor The delegation proessor to use.
     */
    protected TargetMethodAnnotationDrivenBinder(DelegationProcessor delegationProcessor) {
        this.delegationProcessor = delegationProcessor;
    }

    /**
     * Creates a new method delegation binder that binds method based on annotations found on the target method.
     *
     * @param parameterBinders A list of parameter binder delegates. Each such delegate is responsible for creating a
     *                         {@link org.testifyproject.bytebuddy.implementation.bind.MethodDelegationBinder.ParameterBinding}
     *                         for a specific annotation.
     * @return An appropriate method delegation binder.
     */
    public static MethodDelegationBinder of(List> parameterBinders) {
        return new TargetMethodAnnotationDrivenBinder(DelegationProcessor.of(parameterBinders));
    }

    @Override
    public MethodDelegationBinder.Record compile(MethodDescription candidate) {
        if (IgnoreForBinding.Verifier.check(candidate)) {
            return MethodDelegationBinder.Record.Illegal.INSTANCE;
        }
        List handlers = new ArrayList(candidate.getParameters().size());
        for (ParameterDescription parameterDescription : candidate.getParameters()) {
            handlers.add(delegationProcessor.prepare(parameterDescription));
        }
        return new Record(candidate, handlers, RuntimeType.Verifier.check(candidate));
    }

    /**
     * A compiled record of a target method annotation-driven binder.
     */
    @EqualsAndHashCode
    protected static class Record implements MethodDelegationBinder.Record {

        /**
         * The candidate method.
         */
        private final MethodDescription candidate;

        /**
         * A list of handlers for each parameter.
         */
        private final List handlers;

        /**
         * The typing to apply.
         */
        private final Assigner.Typing typing;

        /**
         * Creates a default compiled method delegation binder.
         *
         * @param candidate The candidate method.
         * @param handlers  A list of handlers for each parameter.
         * @param typing    The typing to apply.
         */
        protected Record(MethodDescription candidate, List handlers, Assigner.Typing typing) {
            this.candidate = candidate;
            this.handlers = handlers;
            this.typing = typing;
        }

        @Override
        public MethodBinding bind(Implementation.Target implementationTarget,
                                  MethodDescription source,
                                  MethodDelegationBinder.TerminationHandler terminationHandler,
                                  MethodInvoker methodInvoker,
                                  Assigner assigner) {
            if (!candidate.isAccessibleTo(implementationTarget.getInstrumentedType())) {
                return MethodBinding.Illegal.INSTANCE;
            }
            StackManipulation methodTermination = terminationHandler.resolve(assigner, typing, source, candidate);
            if (!methodTermination.isValid()) {
                return MethodBinding.Illegal.INSTANCE;
            }
            MethodBinding.Builder methodDelegationBindingBuilder = new MethodBinding.Builder(methodInvoker, candidate);
            for (DelegationProcessor.Handler handler : handlers) {
                ParameterBinding parameterBinding = handler.bind(source, implementationTarget, assigner);
                if (!parameterBinding.isValid() || !methodDelegationBindingBuilder.append(parameterBinding)) {
                    return MethodBinding.Illegal.INSTANCE;
                }
            }
            return methodDelegationBindingBuilder.build(methodTermination);
        }

        @Override
        public String toString() {
            return candidate.toString();
        }
    }

    /**
     * A parameter binder is used as a delegate for binding a parameter according to a particular annotation type found
     * on this parameter.
     *
     * @param  The {@link java.lang.annotation.Annotation#annotationType()} handled by this parameter binder.
     */
    @SuppressFBWarnings(value = "IC_SUPERCLASS_USES_SUBCLASS_DURING_INITIALIZATION", justification = "Safe initialization is implied")
    public interface ParameterBinder {

        /**
         * The default parameter binders to be used.
         */
        List> DEFAULTS = Collections.unmodifiableList(Arrays.>asList(
                Argument.Binder.INSTANCE,
                AllArguments.Binder.INSTANCE,
                Origin.Binder.INSTANCE,
                This.Binder.INSTANCE,
                Super.Binder.INSTANCE,
                Default.Binder.INSTANCE,
                SuperCall.Binder.INSTANCE,
                DefaultCall.Binder.INSTANCE,
                SuperMethod.Binder.INSTANCE,
                DefaultMethod.Binder.INSTANCE,
                FieldValue.Binder.INSTANCE,
                StubValue.Binder.INSTANCE,
                Empty.Binder.INSTANCE));

        /**
         * The annotation type that is handled by this parameter binder.
         *
         * @return The {@link java.lang.annotation.Annotation#annotationType()} handled by this parameter binder.
         */
        Class getHandledType();

        /**
         * Creates a parameter binding for the given target parameter.
         *
         * @param annotation           The annotation that was cause for the delegation to this argument binder.
         * @param source               The intercepted source method.
         * @param target               Tge target parameter that is subject to be bound to
         *                             intercepting the {@code source} method.
         * @param implementationTarget The target of the current implementation that is subject to this binding.
         * @param assigner             An assigner that can be used for applying the binding.
         * @param typing               The typing to apply.
         * @return A parameter binding for the requested target method parameter.
         */
        ParameterBinding bind(AnnotationDescription.Loadable annotation,
                                 MethodDescription source,
                                 ParameterDescription target,
                                 Implementation.Target implementationTarget,
                                 Assigner assigner,
                                 Assigner.Typing typing);

        /**
         * 

* Implements a parameter binder that binds a fixed value to a parameter with a given annotation. *

*

* This binder is only capable to store values that can either be expressed as Java byte code or as a constant pool value. This * includes primitive types, {@link String} values, {@link Class} values which can also be expressed as {@link TypeDescription} * instances or method handles and method types for classes of a version at least of Java 7. The latter instances can also be * expressed as unloaded {@link JavaConstant} representations. *

*

* Important: When supplying a method handle or a method type, all types that are implied must be visible to the instrumented * type or an {@link IllegalAccessException} will be thrown at runtime. *

* * @param The bound annotation's type. */ abstract class ForFixedValue implements ParameterBinder { @Override public ParameterBinding bind(AnnotationDescription.Loadable annotation, MethodDescription source, ParameterDescription target, Implementation.Target implementationTarget, Assigner assigner, Assigner.Typing typing) { Object value = bind(annotation, source, target); if (value == null) { return new ParameterBinding.Anonymous(DefaultValue.of(target.getType())); } StackManipulation stackManipulation; TypeDescription suppliedType; if (value instanceof Boolean) { stackManipulation = IntegerConstant.forValue((Boolean) value); suppliedType = new TypeDescription.ForLoadedType(boolean.class); } else if (value instanceof Byte) { stackManipulation = IntegerConstant.forValue((Byte) value); suppliedType = new TypeDescription.ForLoadedType(byte.class); } else if (value instanceof Short) { stackManipulation = IntegerConstant.forValue((Short) value); suppliedType = new TypeDescription.ForLoadedType(short.class); } else if (value instanceof Character) { stackManipulation = IntegerConstant.forValue((Character) value); suppliedType = new TypeDescription.ForLoadedType(char.class); } else if (value instanceof Integer) { stackManipulation = IntegerConstant.forValue((Integer) value); suppliedType = new TypeDescription.ForLoadedType(int.class); } else if (value instanceof Long) { stackManipulation = LongConstant.forValue((Long) value); suppliedType = new TypeDescription.ForLoadedType(long.class); } else if (value instanceof Float) { stackManipulation = FloatConstant.forValue((Float) value); suppliedType = new TypeDescription.ForLoadedType(float.class); } else if (value instanceof Double) { stackManipulation = DoubleConstant.forValue((Double) value); suppliedType = new TypeDescription.ForLoadedType(double.class); } else if (value instanceof String) { stackManipulation = new TextConstant((String) value); suppliedType = TypeDescription.STRING; } else if (value instanceof Class) { stackManipulation = ClassConstant.of(new TypeDescription.ForLoadedType((Class) value)); suppliedType = TypeDescription.CLASS; } else if (value instanceof TypeDescription) { stackManipulation = ClassConstant.of((TypeDescription) value); suppliedType = TypeDescription.CLASS; } else if (JavaType.METHOD_HANDLE.getTypeStub().isInstance(value)) { stackManipulation = JavaConstant.MethodHandle.ofLoaded(value).asStackManipulation(); suppliedType = JavaType.METHOD_HANDLE.getTypeStub(); } else if (value instanceof JavaConstant.MethodHandle) { stackManipulation = new JavaConstantValue((JavaConstant.MethodHandle) value); suppliedType = JavaType.METHOD_HANDLE.getTypeStub(); } else if (JavaType.METHOD_TYPE.getTypeStub().isInstance(value)) { stackManipulation = new JavaConstantValue(JavaConstant.MethodType.ofLoaded(value)); suppliedType = JavaType.METHOD_HANDLE.getTypeStub(); } else if (value instanceof JavaConstant.MethodType) { stackManipulation = new JavaConstantValue((JavaConstant.MethodType) value); suppliedType = JavaType.METHOD_HANDLE.getTypeStub(); } else { throw new IllegalStateException("Not able to save in class's constant pool: " + value); } return new ParameterBinding.Anonymous(new StackManipulation.Compound( stackManipulation, assigner.assign(suppliedType.asGenericType(), target.getType(), typing) )); } /** * Resolves a value for the given annotation on a parameter that is processed by a {@link org.testifyproject.bytebuddy.implementation.MethodDelegation}. * * @param annotation The annotation that triggered this binding. * @param source The method for which a delegation is currently bound. * @param target The parameter for which a value is bound. * @return The constant pool value that is bound to this parameter or {@code null} for binding this value. */ protected abstract Object bind(AnnotationDescription.Loadable annotation, MethodDescription source, ParameterDescription target); /** *

* A parameter binder that binds a fixed value to a parameter annotation when using a {@link org.testifyproject.bytebuddy.implementation.MethodDelegation}. *

*

* This binder is only capable to store * values that can either be expressed as Java byte code or as a constant pool value. This includes primitive types, {@link String} values, * {@link Class} values which can also be expressed as {@link TypeDescription} instances or method handles and method types for classes of * a version at least of Java 7. The latter instances can also be expressed as unloaded {@link JavaConstant} representations. *

* * @param The bound annotation's type. */ @EqualsAndHashCode(callSuper = false) public static class OfConstant extends ForFixedValue { /** * The type of the annotation that is bound by this binder. */ private final Class type; /** * The value that is assigned to any annotated parameter. */ private final Object value; /** * Creates a binder for binding a fixed value to a parameter annotated with the given annotation. * * @param type The type of the annotation that is bound by this binder. * @param value The value that is assigned to any annotated parameter. */ protected OfConstant(Class type, Object value) { this.type = type; this.value = value; } /** * Creates a binder for binding a fixed value to a given annotation. * * @param type The type of the annotation that is bound by this binder. * @param value The value that is assigned to any annotated parameter. * @param The bound annotation's type. * @return A parameter binder that binds the given annotation to the supplied value. */ public static ParameterBinder of(Class type, Object value) { return new OfConstant(type, value); } @Override public Class getHandledType() { return type; } @Override protected Object bind(AnnotationDescription.Loadable annotation, MethodDescription source, ParameterDescription target) { return value; } } } /** * A parameter binder that binds a field's value. * * @param The {@link java.lang.annotation.Annotation#annotationType()} handled by this parameter binder. */ abstract class ForFieldBinding implements ParameterBinder { /** * Indicates that a name should be extracted from an accessor method. */ protected static final String BEAN_PROPERTY = ""; /** * Resolves a field locator for a potential accessor method. * * @param fieldLocator The field locator to use. * @param methodDescription The method description that is the potential accessor. * @return A resolution for a field locator. */ private static FieldLocator.Resolution resolveAccessor(FieldLocator fieldLocator, MethodDescription methodDescription) { String fieldName; if (isSetter().matches(methodDescription)) { fieldName = methodDescription.getInternalName().substring(3); } else if (isGetter().matches(methodDescription)) { fieldName = methodDescription.getInternalName().substring(methodDescription.getInternalName().startsWith("is") ? 2 : 3); } else { return FieldLocator.Resolution.Illegal.INSTANCE; } return fieldLocator.locate(Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1)); } @Override public ParameterBinding bind(AnnotationDescription.Loadable annotation, MethodDescription source, ParameterDescription target, Implementation.Target implementationTarget, Assigner assigner, Assigner.Typing typing) { if (!declaringType(annotation).represents(void.class)) { if (declaringType(annotation).isPrimitive() || declaringType(annotation).isArray()) { throw new IllegalStateException("A primitive type or array type cannot declare a field: " + source); } else if (!implementationTarget.getInstrumentedType().isAssignableTo(declaringType(annotation))) { return MethodDelegationBinder.ParameterBinding.Illegal.INSTANCE; } } FieldLocator fieldLocator = declaringType(annotation).represents(void.class) ? new FieldLocator.ForClassHierarchy(implementationTarget.getInstrumentedType()) : new FieldLocator.ForExactType(declaringType(annotation), implementationTarget.getInstrumentedType()); FieldLocator.Resolution resolution = fieldName(annotation).equals(BEAN_PROPERTY) ? resolveAccessor(fieldLocator, source) : fieldLocator.locate(fieldName(annotation)); return resolution.isResolved() && !(source.isStatic() && !resolution.getField().isStatic()) ? bind(resolution.getField(), annotation, source, target, implementationTarget, assigner) : ParameterBinding.Illegal.INSTANCE; } /** * Extracts the field name from an annotation. * * @param annotation The annotation from which to extract the field name. * @return The field name defined by the handled annotation. */ protected abstract String fieldName(AnnotationDescription.Loadable annotation); /** * Extracts the declaring type from an annotation. * * @param annotation The annotation from which to extract the declaring type. * @return The declaring type defined by the handled annotation. */ protected abstract TypeDescription declaringType(AnnotationDescription.Loadable annotation); /** * Creates a parameter binding for the given target parameter. * * @param fieldDescription The field for which this binder binds a value. * @param annotation The annotation that was cause for the delegation to this argument binder. * @param source The intercepted source method. * @param target Tge target parameter that is subject to be bound to * intercepting the {@code source} method. * @param implementationTarget The target of the current implementation that is subject to this binding. * @param assigner An assigner that can be used for applying the binding. * @return A parameter binding for the requested target method parameter. */ protected abstract ParameterBinding bind(FieldDescription fieldDescription, AnnotationDescription.Loadable annotation, MethodDescription source, ParameterDescription target, Implementation.Target implementationTarget, Assigner assigner); } } /** * A delegation processor is a helper class for a * {@link org.testifyproject.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder} * for performing its actual logic. By outsourcing this logic to this helper class, a cleaner implementation * can be provided. */ @EqualsAndHashCode protected static class DelegationProcessor { /** * A map of registered annotation types to the binder that is responsible for binding a parameter * that is annotated with the given annotation. */ private final Map> parameterBinders; /** * Creates a new delegation processor. * * @param parameterBinders A mapping of parameter binders by their handling type. */ protected DelegationProcessor(Map> parameterBinders) { this.parameterBinders = parameterBinders; } /** * Creates a new delegation processor. * * @param parameterBinders A list of parameter binder delegates. Each such delegate is responsible for creating * a {@link org.testifyproject.bytebuddy.implementation.bind.MethodDelegationBinder.ParameterBinding} * for a specific annotation. * @return A corresponding delegation processor. */ protected static DelegationProcessor of(List> parameterBinders) { Map> parameterBinderMap = new HashMap>(); for (ParameterBinder parameterBinder : parameterBinders) { if (parameterBinderMap.put(new TypeDescription.ForLoadedType(parameterBinder.getHandledType()), parameterBinder) != null) { throw new IllegalArgumentException("Attempt to bind two handlers to " + parameterBinder.getHandledType()); } } return new DelegationProcessor(parameterBinderMap); } /** * Locates a handler which is responsible for processing the given parameter. If no explicit handler can * be located, a fallback handler is provided. * * @param target The target parameter being handled. * @return A handler for processing the parameter with the given annotations. */ protected Handler prepare(ParameterDescription target) { Assigner.Typing typing = RuntimeType.Verifier.check(target); Handler handler = new Handler.Unbound(target, typing); for (AnnotationDescription annotation : target.getDeclaredAnnotations()) { ParameterBinder parameterBinder = parameterBinders.get(annotation.getAnnotationType()); if (parameterBinder != null && handler.isBound()) { throw new IllegalStateException("Ambiguous binding for parameter annotated with two handled annotation types"); } else if (parameterBinder != null /* && !handler.isBound() */) { handler = Handler.Bound.of(target, parameterBinder, annotation, typing); } } return handler; } /** * A handler is responsible for processing a parameter's binding. */ protected interface Handler { /** * Indicates if this handler was explicitly bound. * * @return {@code true} if this handler was explicitly bound. */ boolean isBound(); /** * Handles a parameter binding. * * @param source The intercepted source method. * @param implementationTarget The target of the current implementation. * @param assigner The assigner to use. * @return A parameter binding that reflects the given arguments. */ ParameterBinding bind(MethodDescription source, Implementation.Target implementationTarget, Assigner assigner); /** * An unbound handler is a fallback for returning an illegal binding for parameters for which no parameter * binder could be located. */ @EqualsAndHashCode class Unbound implements Handler { /** * The target parameter being handled. */ private final ParameterDescription target; /** * The typing to apply. */ private final Assigner.Typing typing; /** * Creates a new unbound handler. * * @param target The target parameter being handled. * @param typing The typing to apply. */ protected Unbound(ParameterDescription target, Assigner.Typing typing) { this.target = target; this.typing = typing; } @Override public boolean isBound() { return false; } @Override public ParameterBinding bind(MethodDescription source, Implementation.Target implementationTarget, Assigner assigner) { return Argument.Binder.INSTANCE.bind(AnnotationDescription.ForLoadedAnnotation.of(new DefaultArgument(target.getIndex())), source, target, implementationTarget, assigner, typing); } /** * A default implementation of an {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Argument} annotation. */ protected static class DefaultArgument implements Argument { /** * The name of the value annotation parameter. */ private static final String VALUE = "value"; /** * The name of the value binding mechanic parameter. */ private static final String BINDING_MECHANIC = "bindingMechanic"; /** * The index of the source method parameter to be bound. */ private final int parameterIndex; /** * Creates a new instance of an argument annotation. * * @param parameterIndex The index of the source method parameter to be bound. */ protected DefaultArgument(int parameterIndex) { this.parameterIndex = parameterIndex; } @Override public int value() { return parameterIndex; } @Override public BindingMechanic bindingMechanic() { return BindingMechanic.UNIQUE; } @Override public Class annotationType() { return Argument.class; } @Override public boolean equals(Object other) { return this == other || other instanceof Argument && parameterIndex == ((Argument) other).value(); } @Override public int hashCode() { return ((127 * BINDING_MECHANIC.hashCode()) ^ BindingMechanic.UNIQUE.hashCode()) + ((127 * VALUE.hashCode()) ^ parameterIndex); } @Override public String toString() { return "@" + Argument.class.getName() + "(bindingMechanic=" + BindingMechanic.UNIQUE.toString() + ", value=" + parameterIndex + ")"; } } } /** * A bound handler represents an unambiguous parameter binder that was located for a given array of * annotations. * * @param The annotation type of a given handler. */ @EqualsAndHashCode class Bound implements Handler { /** * The target parameter being handled. */ private final ParameterDescription target; /** * The parameter binder that is actually responsible for binding the parameter. */ private final ParameterBinder parameterBinder; /** * The annotation value that lead to the binding of this handler. */ private final AnnotationDescription.Loadable annotation; /** * The typing to apply. */ private final Assigner.Typing typing; /** * Creates a new bound handler. * * @param target The target parameter being handled. * @param parameterBinder The parameter binder that is actually responsible for binding the parameter. * @param annotation The annotation value that lead to the binding of this handler. * @param typing The typing to apply. */ protected Bound(ParameterDescription target, ParameterBinder parameterBinder, AnnotationDescription.Loadable annotation, Assigner.Typing typing) { this.target = target; this.parameterBinder = parameterBinder; this.annotation = annotation; this.typing = typing; } /** * Creates a handler for a given annotation. * * @param target The target parameter being handled. * @param parameterBinder The parameter binder that should process an annotation. * @param annotation An annotation instance that can be understood by this parameter binder. * @param typing The typing to apply. * @return A handler for processing the given annotation. */ @SuppressWarnings("unchecked") protected static Handler of(ParameterDescription target, ParameterBinder parameterBinder, AnnotationDescription annotation, Assigner.Typing typing) { return new Bound(target, (ParameterBinder) parameterBinder, (AnnotationDescription.Loadable) annotation.prepare(parameterBinder.getHandledType()), typing); } @Override public boolean isBound() { return true; } @Override public ParameterBinding bind(MethodDescription source, Implementation.Target implementationTarget, Assigner assigner) { return parameterBinder.bind(annotation, source, target, implementationTarget, assigner, typing); } } } } }