org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe Maven / Gradle / Ivy
package org.testifyproject.bytebuddy.implementation.bind.annotation;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.EqualsAndHashCode;
import org.testifyproject.bytebuddy.ByteBuddy;
import org.testifyproject.bytebuddy.ClassFileVersion;
import org.testifyproject.bytebuddy.description.annotation.AnnotationDescription;
import org.testifyproject.bytebuddy.description.field.FieldDescription;
import org.testifyproject.bytebuddy.description.field.FieldList;
import org.testifyproject.bytebuddy.description.method.MethodDescription;
import org.testifyproject.bytebuddy.description.method.MethodList;
import org.testifyproject.bytebuddy.description.method.ParameterDescription;
import org.testifyproject.bytebuddy.description.modifier.Visibility;
import org.testifyproject.bytebuddy.description.type.TypeDescription;
import org.testifyproject.bytebuddy.description.type.TypeList;
import org.testifyproject.bytebuddy.dynamic.DynamicType;
import org.testifyproject.bytebuddy.dynamic.scaffold.InstrumentedType;
import org.testifyproject.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import org.testifyproject.bytebuddy.implementation.Implementation;
import org.testifyproject.bytebuddy.implementation.MethodAccessorFactory;
import org.testifyproject.bytebuddy.implementation.auxiliary.AuxiliaryType;
import org.testifyproject.bytebuddy.implementation.bind.MethodDelegationBinder;
import org.testifyproject.bytebuddy.implementation.bytecode.ByteCodeAppender;
import org.testifyproject.bytebuddy.implementation.bytecode.Duplication;
import org.testifyproject.bytebuddy.implementation.bytecode.StackManipulation;
import org.testifyproject.bytebuddy.implementation.bytecode.TypeCreation;
import org.testifyproject.bytebuddy.implementation.bytecode.assign.Assigner;
import org.testifyproject.bytebuddy.implementation.bytecode.member.FieldAccess;
import org.testifyproject.bytebuddy.implementation.bytecode.member.MethodInvocation;
import org.testifyproject.bytebuddy.implementation.bytecode.member.MethodReturn;
import org.testifyproject.bytebuddy.implementation.bytecode.member.MethodVariableAccess;
import org.testifyproject.bytebuddy.matcher.ElementMatchers;
import org.testifyproject.bytebuddy.jar.asm.MethodVisitor;
import java.io.Serializable;
import java.lang.annotation.*;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.testifyproject.bytebuddy.matcher.ElementMatchers.*;
/**
*
* A target method parameter that is annotated with this annotation allows to forward an intercepted method
* invocation to another instance. The instance to which a method call is forwarded must be of the most specific
* type that declares the intercepted method on the intercepted type.
*
*
* Unfortunately, before Java 8, the Java Class Library does not define any interface type which takes a single
* {@link java.lang.Object} type and returns another {@link java.lang.Object} type. For this reason, a
* {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe.Binder} needs to be installed explicitly
* and registered on a {@link org.testifyproject.bytebuddy.implementation.MethodDelegation}. The installed type is allowed to be an
* interface without any super types that declares a single method which maps an {@link java.lang.Object} type to
* a another {@link java.lang.Object} type as a result value. It is however not prohibited to use generics in the
* process.
*
*
* @see org.testifyproject.bytebuddy.implementation.MethodDelegation
* @see org.testifyproject.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Pipe {
/**
* Determines if the generated proxy should be {@link java.io.Serializable}.
*
* @return {@code true} if the generated proxy should be {@link java.io.Serializable}.
*/
boolean serializableProxy() default false;
/**
* A {@link org.testifyproject.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder.ParameterBinder}
* for binding the {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe} annotation.
*/
@EqualsAndHashCode
class Binder implements TargetMethodAnnotationDrivenBinder.ParameterBinder {
/**
* The method which implements the behavior of forwarding a method invocation. This method needs to define
* a single non-static method with an {@link java.lang.Object} to {@link java.lang.Object} mapping.
*/
private final MethodDescription forwardingMethod;
/**
* Creates a new binder. This constructor is not doing any validation of the forwarding method and its
* declaring type. Such validation is normally performed by the
* {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe.Binder#install(Class)}
* method.
*
* @param forwardingMethod The method which implements the behavior of forwarding a method invocation. This
* method needs to define a single non-static method with an {@link java.lang.Object}
* to {@link java.lang.Object} mapping.
*/
protected Binder(MethodDescription forwardingMethod) {
this.forwardingMethod = forwardingMethod;
}
/**
* Installs a given type for use on a {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe}
* annotation. The given type must be an interface without any super interfaces and a single method which
* maps an {@link java.lang.Object} type to another {@link java.lang.Object} type. The use of generics is
* permitted.
*
* @param type The type to install.
* @return A binder for the {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe}
* annotation.
*/
public static TargetMethodAnnotationDrivenBinder.ParameterBinder install(Class> type) {
return install(new TypeDescription.ForLoadedType(type));
}
/**
* Installs a given type for use on a {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe}
* annotation. The given type must be an interface without any super interfaces and a single method which
* maps an {@link java.lang.Object} type to another {@link java.lang.Object} type. The use of generics is
* permitted.
*
* @param typeDescription The type to install.
* @return A binder for the {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe}
* annotation.
*/
public static TargetMethodAnnotationDrivenBinder.ParameterBinder install(TypeDescription typeDescription) {
return new Binder(onlyMethod(typeDescription));
}
/**
* Locates the only method of a type that is compatible to being overridden for invoking the proxy.
*
* @param typeDescription The type that is being installed.
* @return Its only method after validation.
*/
private static MethodDescription onlyMethod(TypeDescription typeDescription) {
if (!typeDescription.isInterface()) {
throw new IllegalArgumentException(typeDescription + " is not an interface");
} else if (!typeDescription.getInterfaces().isEmpty()) {
throw new IllegalArgumentException(typeDescription + " must not extend other interfaces");
} else if (!typeDescription.isPublic()) {
throw new IllegalArgumentException(typeDescription + " is mot public");
}
MethodList> methodCandidates = typeDescription.getDeclaredMethods().filter(isAbstract());
if (methodCandidates.size() != 1) {
throw new IllegalArgumentException(typeDescription + " must declare exactly one abstract method");
}
MethodDescription methodDescription = methodCandidates.getOnly();
if (!methodDescription.getReturnType().asErasure().represents(Object.class)) {
throw new IllegalArgumentException(methodDescription + " does not return an Object-type");
} else if (methodDescription.getParameters().size() != 1 || !methodDescription.getParameters().getOnly().getType().asErasure().represents(Object.class)) {
throw new IllegalArgumentException(methodDescription + " does not take a single Object-typed argument");
}
return methodDescription;
}
@Override
public Class getHandledType() {
return Pipe.class;
}
@Override
public MethodDelegationBinder.ParameterBinding> bind(AnnotationDescription.Loadable annotation,
MethodDescription source,
ParameterDescription target,
Implementation.Target implementationTarget,
Assigner assigner,
Assigner.Typing typing) {
if (!target.getType().asErasure().equals(forwardingMethod.getDeclaringType())) {
throw new IllegalStateException("Illegal use of @Pipe for " + target + " which was installed for " + forwardingMethod.getDeclaringType());
} else if (source.isStatic()) {
return MethodDelegationBinder.ParameterBinding.Illegal.INSTANCE;
}
return new MethodDelegationBinder.ParameterBinding.Anonymous(new Redirection(forwardingMethod.getDeclaringType().asErasure(),
source,
assigner,
annotation.loadSilent().serializableProxy()));
}
/**
* An auxiliary type for performing the redirection of a method invocation as requested by the
* {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe} annotation.
*/
@EqualsAndHashCode
protected static class Redirection implements AuxiliaryType, StackManipulation {
/**
* The prefix for naming fields to store method arguments.
*/
private static final String FIELD_NAME_PREFIX = "argument";
/**
* The type that declares the method for forwarding a method invocation.
*/
private final TypeDescription forwardingType;
/**
* The method that is to be forwarded.
*/
private final MethodDescription sourceMethod;
/**
* The assigner to use.
*/
private final Assigner assigner;
/**
* Determines if the generated proxy should be {@link java.io.Serializable}.
*/
private final boolean serializableProxy;
/**
* Creates a new redirection.
*
* @param forwardingType The type that declares the method for forwarding a method invocation.
* @param sourceMethod The method that is to be forwarded.
* @param assigner The assigner to use.
* @param serializableProxy Determines if the generated proxy should be {@link java.io.Serializable}.
*/
protected Redirection(TypeDescription forwardingType,
MethodDescription sourceMethod,
Assigner assigner,
boolean serializableProxy) {
this.forwardingType = forwardingType;
this.sourceMethod = sourceMethod;
this.assigner = assigner;
this.serializableProxy = serializableProxy;
}
/**
* Extracts all parameters of a method to fields.
*
* @param methodDescription The method to extract the parameters from.
* @return A linked hash map of field names to the types of these fields representing all parameters of the
* given method.
*/
private static LinkedHashMap extractFields(MethodDescription methodDescription) {
TypeList parameterTypes = methodDescription.getParameters().asTypeList().asErasures();
LinkedHashMap typeDescriptions = new LinkedHashMap();
int currentIndex = 0;
for (TypeDescription parameterType : parameterTypes) {
typeDescriptions.put(fieldName(currentIndex++), parameterType);
}
return typeDescriptions;
}
/**
* Creates a new field name.
*
* @param index The index of the field.
* @return The field name that corresponds to the index.
*/
private static String fieldName(int index) {
return String.format("%s%d", FIELD_NAME_PREFIX, index);
}
@Override
public DynamicType make(String auxiliaryTypeName,
ClassFileVersion classFileVersion,
MethodAccessorFactory methodAccessorFactory) {
LinkedHashMap parameterFields = extractFields(sourceMethod);
DynamicType.Builder> builder = new ByteBuddy(classFileVersion)
.subclass(forwardingType, ConstructorStrategy.Default.NO_CONSTRUCTORS)
.name(auxiliaryTypeName)
.modifiers(DEFAULT_TYPE_MODIFIER)
.implement(serializableProxy ? new Class>[]{Serializable.class} : new Class>[0])
.method(ElementMatchers.isAbstract().and(isDeclaredBy(forwardingType)))
.intercept(new MethodCall(sourceMethod, assigner))
.defineConstructor().withParameters(parameterFields.values())
.intercept(ConstructorCall.INSTANCE);
for (Map.Entry field : parameterFields.entrySet()) {
builder = builder.defineField(field.getKey(), field.getValue(), Visibility.PRIVATE);
}
return builder.make();
}
@Override
public boolean isValid() {
return true;
}
@Override
public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) {
TypeDescription forwardingType = implementationContext.register(this);
return new Compound(
TypeCreation.of(forwardingType),
Duplication.SINGLE,
MethodVariableAccess.allArgumentsOf(sourceMethod),
MethodInvocation.invoke(forwardingType.getDeclaredMethods().filter(isConstructor()).getOnly())
).apply(methodVisitor, implementationContext);
}
/**
* The implementation to implement a
* {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe.Binder.Redirection}'s
* constructor.
*/
@SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "Fields of enumerations are never serialized")
protected enum ConstructorCall implements Implementation {
/**
* The singleton instance.
*/
INSTANCE;
/**
* A reference of the {@link Object} type default constructor.
*/
private final MethodDescription.InDefinedShape objectTypeDefaultConstructor;
/**
* Creates the constructor call singleton.
*/
ConstructorCall() {
objectTypeDefaultConstructor = TypeDescription.OBJECT.getDeclaredMethods().filter(isConstructor()).getOnly();
}
@Override
public InstrumentedType prepare(InstrumentedType instrumentedType) {
return instrumentedType;
}
@Override
public ByteCodeAppender appender(Target implementationTarget) {
return new Appender(implementationTarget.getInstrumentedType());
}
/**
* The appender for implementing the
* {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe.Binder.Redirection.ConstructorCall}.
*/
@EqualsAndHashCode
private static class Appender implements ByteCodeAppender {
/**
* The instrumented type being created.
*/
private final TypeDescription instrumentedType;
/**
* Creates a new appender.
*
* @param instrumentedType The instrumented type that is being created.
*/
private Appender(TypeDescription instrumentedType) {
this.instrumentedType = instrumentedType;
}
@Override
public Size apply(MethodVisitor methodVisitor, Context implementationContext, MethodDescription instrumentedMethod) {
FieldList> fieldList = instrumentedType.getDeclaredFields();
StackManipulation[] fieldLoading = new StackManipulation[fieldList.size()];
int index = 0;
for (FieldDescription fieldDescription : fieldList) {
fieldLoading[index] = new StackManipulation.Compound(
MethodVariableAccess.loadThis(),
MethodVariableAccess.load(instrumentedMethod.getParameters().get(index)),
FieldAccess.forField(fieldDescription).write()
);
index++;
}
StackManipulation.Size stackSize = new StackManipulation.Compound(
MethodVariableAccess.loadThis(),
MethodInvocation.invoke(ConstructorCall.INSTANCE.objectTypeDefaultConstructor),
new StackManipulation.Compound(fieldLoading),
MethodReturn.VOID
).apply(methodVisitor, implementationContext);
return new Size(stackSize.getMaximalSize(), instrumentedMethod.getStackSize());
}
}
}
/**
* The implementation to implement a
* {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe.Binder.Redirection}'s
* forwarding method.
*/
@EqualsAndHashCode
protected static class MethodCall implements Implementation {
/**
* The method that is invoked by the implemented method.
*/
private final MethodDescription redirectedMethod;
/**
* The assigner to be used for invoking the forwarded method.
*/
private final Assigner assigner;
/**
* Creates a new method call implementation.
*
* @param redirectedMethod The method that is invoked by the implemented method.
* @param assigner The assigner to be used for invoking the forwarded method.
*/
private MethodCall(MethodDescription redirectedMethod, Assigner assigner) {
this.redirectedMethod = redirectedMethod;
this.assigner = assigner;
}
@Override
public InstrumentedType prepare(InstrumentedType instrumentedType) {
return instrumentedType;
}
@Override
public ByteCodeAppender appender(Target implementationTarget) {
if (!redirectedMethod.isAccessibleTo(implementationTarget.getInstrumentedType())) {
throw new IllegalStateException("Cannot invoke " + redirectedMethod + " from outside of class via @Pipe proxy");
}
return new Appender(implementationTarget.getInstrumentedType());
}
/**
* The appender for implementing the
* {@link org.testifyproject.bytebuddy.implementation.bind.annotation.Pipe.Binder.Redirection.MethodCall}.
*/
private class Appender implements ByteCodeAppender {
/**
* The instrumented type that is implemented.
*/
private final TypeDescription instrumentedType;
/**
* Creates a new appender.
*
* @param instrumentedType The instrumented type to be implemented.
*/
private Appender(TypeDescription instrumentedType) {
this.instrumentedType = instrumentedType;
}
@Override
public Size apply(MethodVisitor methodVisitor,
Context implementationContext,
MethodDescription instrumentedMethod) {
StackManipulation thisReference = MethodVariableAccess.of(instrumentedType).loadFrom(0);
FieldList> fieldList = instrumentedType.getDeclaredFields();
StackManipulation[] fieldLoading = new StackManipulation[fieldList.size()];
int index = 0;
for (FieldDescription fieldDescription : fieldList) {
fieldLoading[index++] = new StackManipulation.Compound(thisReference, FieldAccess.forField(fieldDescription).read());
}
StackManipulation.Size stackSize = new StackManipulation.Compound(
MethodVariableAccess.REFERENCE.loadFrom(1),
assigner.assign(TypeDescription.Generic.OBJECT, redirectedMethod.getDeclaringType().asGenericType(), Assigner.Typing.DYNAMIC),
new StackManipulation.Compound(fieldLoading),
MethodInvocation.invoke(redirectedMethod),
assigner.assign(redirectedMethod.getReturnType(), instrumentedMethod.getReturnType(), Assigner.Typing.DYNAMIC),
MethodReturn.REFERENCE
).apply(methodVisitor, implementationContext);
return new Size(stackSize.getMaximalSize(), instrumentedMethod.getStackSize());
}
/**
* Returns the outer instance.
*
* @return The outer instance.
*/
private MethodCall getMethodCall() {
return MethodCall.this;
}
@Override // HE: Remove when Lombok support for getOuter is added.
public boolean equals(Object other) {
return this == other || !(other == null || getClass() != other.getClass())
&& instrumentedType.equals(((Appender) other).instrumentedType)
&& MethodCall.this.equals(((Appender) other).getMethodCall());
}
@Override // HE: Remove when Lombok support for getOuter is added.
public int hashCode() {
return 31 * MethodCall.this.hashCode() + instrumentedType.hashCode();
}
}
}
}
}
}