org.testifyproject.bytebuddy.implementation.FieldAccessor Maven / Gradle / Ivy
The newest version!
package org.testifyproject.bytebuddy.implementation;
import lombok.EqualsAndHashCode;
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.dynamic.scaffold.InstrumentedType;
import org.testifyproject.bytebuddy.implementation.bytecode.ByteCodeAppender;
import org.testifyproject.bytebuddy.implementation.bytecode.StackManipulation;
import org.testifyproject.bytebuddy.implementation.bytecode.assign.Assigner;
import org.testifyproject.bytebuddy.implementation.bytecode.member.FieldAccess;
import org.testifyproject.bytebuddy.implementation.bytecode.member.MethodReturn;
import org.testifyproject.bytebuddy.implementation.bytecode.member.MethodVariableAccess;
import org.testifyproject.bytebuddy.jar.asm.MethodVisitor;
import java.lang.reflect.Field;
/**
*
* Defines a method to access a given field by following the Java bean conventions for getters and setters:
*
*
* - Getter: A method named {@code getFoo()} will be instrumented to read and return the value of a field {@code foo}
* or another field if one was specified explicitly. If a property is of type {@link java.lang.Boolean} or
* {@code boolean}, the name {@code isFoo()} is also permitted.
* - Setter: A method named {@code setFoo(value)} will be instrumented to write the given argument {@code value}
* to a field {@code foo} or to another field if one was specified explicitly.
*
*
* Field accessors always implement a getter if a non-{@code void} value is returned from a method and attempt to define a setter
* otherwise. If a field accessor is not explicitly defined as a setter via {@link PropertyConfigurable}, an instrumented
* method must define exactly one parameter. Using the latter API, an explicit parameter index can be defined and a return
* value can be specified explicitly when {@code void} is not returned.
*
*/
@EqualsAndHashCode
public abstract class FieldAccessor implements Implementation {
/**
* The field's location.
*/
protected final FieldLocation fieldLocation;
/**
* The assigner to use.
*/
protected final Assigner assigner;
/**
* Indicates if dynamic type castings should be attempted for incompatible assignments.
*/
protected final Assigner.Typing typing;
/**
* Creates a new field accessor.
*
* @param fieldLocation The field's location.
* @param assigner The assigner to use.
* @param typing Indicates if dynamic type castings should be attempted for incompatible assignments.
*/
protected FieldAccessor(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing) {
this.fieldLocation = fieldLocation;
this.assigner = assigner;
this.typing = typing;
}
/**
* Defines a field accessor where any access is targeted to a field named {@code name}.
*
* @param name The name of the field to be accessed.
* @return A field accessor for a field of a given name.
*/
public static OwnerTypeLocatable ofField(String name) {
return of(new FieldNameExtractor.ForFixedValue(name));
}
/**
* Defines a field accessor where any access is targeted to a field that matches the methods
* name with the Java specification for bean properties, i.e. a method {@code getFoo} or {@code setFoo(value)}
* will either read or write a field named {@code foo}.
*
* @return A field accessor that follows the Java naming conventions for bean properties.
*/
public static OwnerTypeLocatable ofBeanProperty() {
return of(FieldNameExtractor.ForBeanProperty.INSTANCE);
}
/**
* Defines a custom strategy for determining the field that is accessed by this field accessor.
*
* @param fieldNameExtractor The field name extractor to use.
* @return A field accessor using the given field name extractor.
*/
public static OwnerTypeLocatable of(FieldNameExtractor fieldNameExtractor) {
return new ForImplicitProperty(new FieldLocation.Relative(fieldNameExtractor));
}
/**
* Defines a field accessor where the specified field is accessed. The field must be within the hierarchy of the instrumented type.
*
* @param field The field being accessed.
* @return A field accessor for the given field.
*/
public static AssignerConfigurable of(Field field) {
return of(new FieldDescription.ForLoadedField(field));
}
/**
* Defines a field accessor where the specified field is accessed. The field must be within the hierarchy of the instrumented type.
*
* @param fieldDescription The field being accessed.
* @return A field accessor for the given field.
*/
public static AssignerConfigurable of(FieldDescription fieldDescription) {
return new ForImplicitProperty(new FieldLocation.Absolute(fieldDescription));
}
/**
* Creates a getter getter.
*
* @param fieldDescription The field to read the value from.
* @param instrumentedMethod The getter method.
* @return A stack manipulation that gets the field's value.
*/
protected StackManipulation getter(FieldDescription fieldDescription, MethodDescription instrumentedMethod) {
return access(fieldDescription, instrumentedMethod, new StackManipulation.Compound(FieldAccess.forField(fieldDescription).read(),
assigner.assign(fieldDescription.getType(), instrumentedMethod.getReturnType(), typing)));
}
/**
* Creates a setter instruction.
*
* @param fieldDescription The field to set a value for.
* @param parameterDescription The parameter for what value is to be set.
* @return A stack manipulation that sets the field's value.
*/
protected StackManipulation setter(FieldDescription fieldDescription, ParameterDescription parameterDescription) {
if (fieldDescription.isFinal() && parameterDescription.getDeclaringMethod().isMethod()) {
throw new IllegalArgumentException("Cannot set final field " + fieldDescription + " from " + parameterDescription.getDeclaringMethod());
}
return access(fieldDescription,
parameterDescription.getDeclaringMethod(),
new StackManipulation.Compound(MethodVariableAccess.load(parameterDescription),
assigner.assign(parameterDescription.getType(), fieldDescription.getType(), typing),
FieldAccess.forField(fieldDescription).write()));
}
/**
* Checks a field access and loads the {@code this} instance if necessary.
*
* @param fieldDescription The field to get a value
* @param instrumentedMethod The instrumented method.
* @param fieldAccess A stack manipulation describing the field access.
* @return An appropriate stack manipulation.
*/
private StackManipulation access(FieldDescription fieldDescription, MethodDescription instrumentedMethod, StackManipulation fieldAccess) {
if (!fieldAccess.isValid()) {
throw new IllegalStateException("Incompatible type of " + fieldDescription + " and " + instrumentedMethod);
} else if (instrumentedMethod.isStatic() && !fieldDescription.isStatic()) {
throw new IllegalArgumentException("Cannot call instance field " + fieldDescription + " from static method " + instrumentedMethod);
}
return new StackManipulation.Compound(fieldDescription.isStatic()
? StackManipulation.Trivial.INSTANCE
: MethodVariableAccess.loadThis(), fieldAccess);
}
@Override
public InstrumentedType prepare(InstrumentedType instrumentedType) {
return instrumentedType;
}
/**
* A field location represents an identified field description which depends on the instrumented type and method.
*/
protected interface FieldLocation {
/**
* Specifies a field locator factory to use.
*
* @param fieldLocatorFactory The field locator factory to use.
* @return An appropriate field location.
*/
FieldLocation with(FieldLocator.Factory fieldLocatorFactory);
/**
* A prepared field location.
*
* @param instrumentedType The instrumented type.
* @return A prepared field location.
*/
Prepared prepare(TypeDescription instrumentedType);
/**
* A prepared field location.
*/
interface Prepared {
/**
* Resolves the field description to use.
*
* @param instrumentedMethod The instrumented method.
* @return The resolved field description.
*/
FieldDescription resolve(MethodDescription instrumentedMethod);
}
/**
* An absolute field description representing a previously resolved field.
*/
@EqualsAndHashCode
class Absolute implements FieldLocation, Prepared {
/**
* The field description.
*/
private final FieldDescription fieldDescription;
/**
* Creates an absolute field location.
*
* @param fieldDescription The field description.
*/
protected Absolute(FieldDescription fieldDescription) {
this.fieldDescription = fieldDescription;
}
@Override
public FieldLocation with(FieldLocator.Factory fieldLocatorFactory) {
throw new IllegalStateException("Cannot specify a field locator factory for an absolute field location");
}
@Override
public Prepared prepare(TypeDescription instrumentedType) {
if (!instrumentedType.isAssignableTo(fieldDescription.getDeclaringType().asErasure())) {
throw new IllegalStateException(fieldDescription + " is not declared by " + instrumentedType);
} else if (!fieldDescription.isVisibleTo(instrumentedType)) {
throw new IllegalStateException("Cannot access " + fieldDescription + " from " + instrumentedType);
}
return this;
}
@Override
public FieldDescription resolve(MethodDescription instrumentedMethod) {
return fieldDescription;
}
}
/**
* A relative field location where a field is located dynamically.
*/
@EqualsAndHashCode
class Relative implements FieldLocation {
/**
* The field name extractor to use.
*/
private final FieldNameExtractor fieldNameExtractor;
/**
* The field locator factory to use.
*/
private final FieldLocator.Factory fieldLocatorFactory;
/**
* Creates a new relative field location.
*
* @param fieldNameExtractor The field name extractor to use.
*/
protected Relative(FieldNameExtractor fieldNameExtractor) {
this(fieldNameExtractor, FieldLocator.ForClassHierarchy.Factory.INSTANCE);
}
/**
* Creates a new relative field location.
*
* @param fieldNameExtractor The field name extractor to use.
* @param fieldLocatorFactory The field locator factory to use.
*/
private Relative(FieldNameExtractor fieldNameExtractor, FieldLocator.Factory fieldLocatorFactory) {
this.fieldNameExtractor = fieldNameExtractor;
this.fieldLocatorFactory = fieldLocatorFactory;
}
@Override
public FieldLocation with(FieldLocator.Factory fieldLocatorFactory) {
return new Relative(fieldNameExtractor, fieldLocatorFactory);
}
@Override
public FieldLocation.Prepared prepare(TypeDescription instrumentedType) {
return new Prepared(fieldNameExtractor, fieldLocatorFactory.make(instrumentedType));
}
/**
* A prepared version of a field location.
*/
@EqualsAndHashCode
protected static class Prepared implements FieldLocation.Prepared {
/**
* The field name extractor to use.
*/
private final FieldNameExtractor fieldNameExtractor;
/**
* The field locator factory to use.
*/
private final FieldLocator fieldLocator;
/**
* Creates a new relative field location.
*
* @param fieldNameExtractor The field name extractor to use.
* @param fieldLocator The field locator to use.
*/
protected Prepared(FieldNameExtractor fieldNameExtractor, FieldLocator fieldLocator) {
this.fieldNameExtractor = fieldNameExtractor;
this.fieldLocator = fieldLocator;
}
@Override
public FieldDescription resolve(MethodDescription instrumentedMethod) {
FieldLocator.Resolution resolution = fieldLocator.locate(fieldNameExtractor.resolve(instrumentedMethod));
if (!resolution.isResolved()) {
throw new IllegalStateException("Cannot resolve field for " + instrumentedMethod + " using " + fieldLocator);
}
return resolution.getField();
}
}
}
}
/**
* A field name extractor is responsible for determining a field name to a method that is implemented
* to access this method.
*/
public interface FieldNameExtractor {
/**
* Extracts a field name to be accessed by a getter or setter method.
*
* @param methodDescription The method for which a field name is to be determined.
* @return The name of the field to be accessed by this method.
*/
String resolve(MethodDescription methodDescription);
/**
* A {@link org.testifyproject.bytebuddy.implementation.FieldAccessor.FieldNameExtractor} that determines a field name
* according to the rules of Java bean naming conventions.
*/
enum ForBeanProperty implements FieldNameExtractor {
/**
* The singleton instance.
*/
INSTANCE;
@Override
public String resolve(MethodDescription methodDescription) {
String name = methodDescription.getInternalName();
int crop;
if (name.startsWith("get") || name.startsWith("set")) {
crop = 3;
} else if (name.startsWith("is")) {
crop = 2;
} else {
throw new IllegalArgumentException(methodDescription + " does not follow Java bean naming conventions");
}
name = name.substring(crop);
if (name.length() == 0) {
throw new IllegalArgumentException(methodDescription + " does not specify a bean name");
}
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}
}
/**
* A field name extractor that returns a fixed value.
*/
@EqualsAndHashCode
class ForFixedValue implements FieldNameExtractor {
/**
* The name to return.
*/
private final String name;
/**
* Creates a new field name extractor for a fixed value.
*
* @param name The name to return.
*/
protected ForFixedValue(String name) {
this.name = name;
}
@Override
public String resolve(MethodDescription methodDescription) {
return name;
}
}
}
/**
* A field accessor that allows to define the access to be a field write of a given argument.
*/
public interface PropertyConfigurable extends Implementation {
/**
* Creates a field accessor for the described field that serves as a setter for the supplied parameter index. The instrumented
* method must return {@code void} or a chained instrumentation must be supplied.
*
* @param index The index of the parameter for which to set the field's value.
* @return An instrumentation that sets the parameter's value to the described field.
*/
Implementation.Composable setsArgumentAt(int index);
}
/**
* A field accessor that can be configured to use a given assigner and runtime type use configuration.
*/
public interface AssignerConfigurable extends PropertyConfigurable {
/**
* Returns a field accessor that is identical to this field accessor but uses the given assigner
* and runtime type use configuration.
*
* @param assigner The assigner to use.
* @param typing Indicates if dynamic type castings should be attempted for incompatible assignments.
* @return This field accessor with the given assigner and runtime type use configuration.
*/
PropertyConfigurable withAssigner(Assigner assigner, Assigner.Typing typing);
}
/**
* A field accessor that can be configured to locate a field in a specific manner.
*/
public interface OwnerTypeLocatable extends AssignerConfigurable {
/**
* Determines that a field should only be considered when it was defined in a given type.
*
* @param type The type to be considered.
* @return This field accessor which will only considered fields that are defined in the given type.
*/
AssignerConfigurable in(Class> type);
/**
* Determines that a field should only be considered when it was defined in a given type.
*
* @param typeDescription A description of the type to be considered.
* @return This field accessor which will only considered fields that are defined in the given type.
*/
AssignerConfigurable in(TypeDescription typeDescription);
/**
* Determines that a field should only be considered when it was identified by a field locator that is
* produced by the given factory.
*
* @param fieldLocatorFactory A factory that will produce a field locator that will be used to find locate
* a field to be accessed.
* @return This field accessor which will only considered fields that are defined in the given type.
*/
AssignerConfigurable in(FieldLocator.Factory fieldLocatorFactory);
}
/**
* A field accessor for an implicit property where a getter or setter property is infered from the signature.
*/
protected static class ForImplicitProperty extends FieldAccessor implements OwnerTypeLocatable {
/**
* Creates a field accessor for an implicit property.
*
* @param fieldLocation The field's location.
*/
protected ForImplicitProperty(FieldLocation fieldLocation) {
this(fieldLocation, Assigner.DEFAULT, Assigner.Typing.STATIC);
}
/**
* Creates a field accessor for an implicit property.
*
* @param fieldLocation The field's location.
* @param assigner The assigner to use.
* @param typing The typing to use.
*/
private ForImplicitProperty(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing) {
super(fieldLocation, assigner, typing);
}
@Override
public ByteCodeAppender appender(Target implementationTarget) {
return new Appender(fieldLocation.prepare(implementationTarget.getInstrumentedType()));
}
@Override
public Composable setsArgumentAt(int index) {
if (index < 0) {
throw new IllegalArgumentException("A parameter index cannot be negative: " + index);
}
return new ForParameterSetter(fieldLocation, assigner, typing, index);
}
@Override
public PropertyConfigurable withAssigner(Assigner assigner, Assigner.Typing typing) {
return new ForImplicitProperty(fieldLocation, assigner, typing);
}
@Override
public AssignerConfigurable in(Class> type) {
return in(new TypeDescription.ForLoadedType(type));
}
@Override
public AssignerConfigurable in(TypeDescription typeDescription) {
return in(new FieldLocator.ForExactType.Factory(typeDescription));
}
@Override
public AssignerConfigurable in(FieldLocator.Factory fieldLocatorFactory) {
return new ForImplicitProperty(fieldLocation.with(fieldLocatorFactory), assigner, typing);
}
/**
* An byte code appender for an field accessor implementation.
*/
protected class Appender implements ByteCodeAppender {
/**
* The field's location.
*/
private final FieldLocation.Prepared fieldLocation;
/**
* Creates a new byte code appender for a field accessor implementation.
*
* @param fieldLocation The field's location.
*/
protected Appender(FieldLocation.Prepared fieldLocation) {
this.fieldLocation = fieldLocation;
}
@Override
public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext, MethodDescription instrumentedMethod) {
if (!instrumentedMethod.isMethod()) {
throw new IllegalArgumentException(instrumentedMethod + " does not describe a field getter or setter");
}
FieldDescription fieldDescription = fieldLocation.resolve(instrumentedMethod);
StackManipulation implementation;
if (!instrumentedMethod.getReturnType().represents(void.class)) {
implementation = new StackManipulation.Compound(getter(fieldDescription, instrumentedMethod), MethodReturn.of(instrumentedMethod.getReturnType()));
} else if (instrumentedMethod.getReturnType().represents(void.class) && instrumentedMethod.getParameters().size() == 1) {
implementation = new StackManipulation.Compound(setter(fieldDescription, instrumentedMethod.getParameters().get(0)), MethodReturn.VOID);
} else {
throw new IllegalArgumentException("Method " + implementationContext + " is no bean property");
}
return new Size(implementation.apply(methodVisitor, implementationContext).getMaximalSize(), instrumentedMethod.getStackSize());
}
/**
* Returns the outer instance.
*
* @return The outer instance.
*/
private ForImplicitProperty getOuter() {
return ForImplicitProperty.this;
}
@Override // HE: Remove when Lombok support for getOuter is added.
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Appender appender = (Appender) object;
return fieldLocation.equals(appender.fieldLocation) && ForImplicitProperty.this.equals(appender.getOuter());
}
@Override // HE: Remove when Lombok support for getOuter is added.
public int hashCode() {
return fieldLocation.hashCode() + 31 * ForImplicitProperty.this.hashCode();
}
}
}
/**
* A field accessor that sets a parameters value of a given index.
*/
@EqualsAndHashCode(callSuper = true)
protected static class ForParameterSetter extends FieldAccessor implements Implementation.Composable {
/**
* The targeted parameter index.
*/
private final int index;
/**
* The termination handler to apply.
*/
private final TerminationHandler terminationHandler;
/**
* Creates a new field accessor.
*
* @param fieldLocation The field's location.
* @param assigner The assigner to use.
* @param typing Indicates if dynamic type castings should be attempted for incompatible assignments.
* @param index The targeted parameter index.
*/
protected ForParameterSetter(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing, int index) {
this(fieldLocation, assigner, typing, index, TerminationHandler.RETURNING);
}
/**
* Creates a new field accessor.
*
* @param fieldLocation The field's location.
* @param assigner The assigner to use.
* @param typing Indicates if dynamic type castings should be attempted for incompatible assignments.
* @param index The targeted parameter index.
* @param terminationHandler The termination handler to apply.
*/
private ForParameterSetter(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing, int index, TerminationHandler terminationHandler) {
super(fieldLocation, assigner, typing);
this.index = index;
this.terminationHandler = terminationHandler;
}
@Override
public ByteCodeAppender appender(Target implementationTarget) {
return new Appender(fieldLocation.prepare(implementationTarget.getInstrumentedType()));
}
@Override
public Implementation andThen(Implementation implementation) {
return new Compound(new ForParameterSetter(fieldLocation,
assigner,
typing,
index, TerminationHandler.NON_OPERATIONAL), implementation);
}
/**
* A termination handler is responsible for handling a field accessor's return.
*/
protected enum TerminationHandler {
/**
* Returns {@code void} or throws an exception if this is not the return type of the instrumented method.
*/
RETURNING {
@Override
protected StackManipulation resolve(MethodDescription instrumentedMethod) {
if (!instrumentedMethod.getReturnType().represents(void.class)) {
throw new IllegalStateException("Cannot implement setter with return value for " + instrumentedMethod);
}
return MethodReturn.VOID;
}
},
/**
* Does not return from the method at all.
*/
NON_OPERATIONAL {
@Override
protected StackManipulation resolve(MethodDescription instrumentedMethod) {
return StackManipulation.Trivial.INSTANCE;
}
};
/**
* Resolves the return instruction.
*
* @param instrumentedMethod The instrumented method.
* @return An appropriate stack manipulation.
*/
protected abstract StackManipulation resolve(MethodDescription instrumentedMethod);
}
/**
* An appender for a field accessor that sets a parameter of a given index.
*/
protected class Appender implements ByteCodeAppender {
/**
* The field's location.
*/
private final FieldLocation.Prepared fieldLocation;
/**
* Creates a new byte code appender for a field accessor implementation.
*
* @param fieldLocation The field's location.
*/
protected Appender(FieldLocation.Prepared fieldLocation) {
this.fieldLocation = fieldLocation;
}
@Override
public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext, MethodDescription instrumentedMethod) {
if (instrumentedMethod.getParameters().size() <= index) {
throw new IllegalStateException(instrumentedMethod + " does not define a parameter with index " + index);
} else {
return new Size(new StackManipulation.Compound(
setter(fieldLocation.resolve(instrumentedMethod), instrumentedMethod.getParameters().get(index)),
terminationHandler.resolve(instrumentedMethod)
).apply(methodVisitor, implementationContext).getMaximalSize(), instrumentedMethod.getStackSize());
}
}
/**
* Returns the outer instance.
*
* @return The outer instance.
*/
private ForParameterSetter getOuter() {
return ForParameterSetter.this;
}
@Override // HE: Remove when Lombok support for getOuter is added.
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
ForParameterSetter.Appender appender = (ForParameterSetter.Appender) object;
return fieldLocation.equals(appender.fieldLocation) && ForParameterSetter.this.equals(appender.getOuter());
}
@Override // HE: Remove when Lombok support for getOuter is added.
public int hashCode() {
return fieldLocation.hashCode() + 31 * ForParameterSetter.this.hashCode();
}
}
}
}