org.testifyproject.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder Maven / Gradle / Ivy
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 extends ParameterBinder>> 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 extends TypeDescription, ? extends ParameterBinder>> parameterBinders;
/**
* Creates a new delegation processor.
*
* @param parameterBinders A mapping of parameter binders by their handling type.
*/
protected DelegationProcessor(Map extends TypeDescription, ? extends ParameterBinder>> 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 extends ParameterBinder>> 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);
}
}
}
}
}