PojomatorByteCodeGenerator.java
package org.pojomatic.internal;
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.pojomatic.Pojomator;
import org.pojomatic.PropertyElement;
import org.pojomatic.annotations.PojoFormat;
import org.pojomatic.annotations.SkipArrayCheck;
import org.pojomatic.diff.Differences;
import org.pojomatic.diff.NoDifferences;
import org.pojomatic.diff.PropertyDifferences;
import org.pojomatic.diff.ValueDifference;
import org.pojomatic.formatter.DefaultEnhancedPojoFormatter;
import org.pojomatic.formatter.EnhancedPojoFormatter;
import org.pojomatic.formatter.EnhancedPropertyFormatter;
import static org.objectweb.asm.Opcodes.*;
class PojomatorByteCodeGenerator {
@Deprecated
private static final String ENHANCED_POJO_FORMATTER_WRAPPER_INTERNAL_NAME =
internalName(org.pojomatic.internal.EnhancedPojoFormatterWrapper.class);
private static final Object[] NO_STACK = new Object[] {};
private static final String OBJECT_INTERNAL_NAME = internalName(Object.class);
private static final String BASE_POJOMATOR_INTERNAL_NAME = internalName(BasePojomator.class);
static final String POJO_CLASS_FIELD_NAME = "pojoClass";
private static final String BOOTSTRAP_METHOD_NAME = "bootstrap";
private static final AtomicLong counter = new AtomicLong();
final String pojomatorClassName;
private final String pojomatorInternalClassName;
private final String pojomatorInternalClassDesc;
private final Class<?> pojoClass;
private final String pojoDescriptor;
private final ClassProperties classProperties;
private final Handle bootstrapMethod;
private final Map<PropertyElement, Integer> propertyNumbers = new HashMap<>();
private MethodVisitor mv; // the active method visitor
/**
* Class for tracking adjustments to be made to the max stack and/or localvariable size
*/
private static class StackAdjustments {
/**
* At least one "wide" property (long or double) has been encountered
*/
boolean wideProperty;
int adjustments(int widePropertyWeight) {
return (wideProperty ? widePropertyWeight : 0);
}
}
PojomatorByteCodeGenerator(Class<?> pojoClass, ClassProperties classProperties) {
this.pojomatorClassName = PojomatorStub.class.getName() + "$" + counter.incrementAndGet();
this.pojomatorInternalClassName = internalName(pojomatorClassName);
this.pojomatorInternalClassDesc = "L" + pojomatorInternalClassName + ";";
this.pojoClass = pojoClass;
this.pojoDescriptor = classDesc(pojoClass);
this.classProperties = classProperties;
this.bootstrapMethod = new Handle(
H_INVOKESTATIC,
BASE_POJOMATOR_INTERNAL_NAME,
BOOTSTRAP_METHOD_NAME,
methodDesc(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, Class.class), false);
int propertyNumber = 1;
for (PropertyElement property: classProperties.getAllProperties()) {
propertyNumbers.put(property, propertyNumber++);
}
}
byte[] makeClassBytes() {
ClassWriter classWriter = new ClassWriter(0);
// acceptClassVisitor(new CheckClassAdapter(classWriter));
acceptClassVisitor(classWriter);
return classWriter.toByteArray();
}
private void acceptClassVisitor(ClassVisitor classWriter) {
classWriter.visit(V1_7, ACC_PUBLIC + ACC_SUPER + ACC_SYNTHETIC, pojomatorInternalClassName, null,
BASE_POJOMATOR_INTERNAL_NAME, new String[] { internalName(Pojomator.class) });
classWriter.visitSource("Look for visitLineNumber", null);
makeFields(classWriter);
makeConstructor(classWriter);
for (PropertyElement propertyElement: classProperties.getAllProperties()) {
makeAccessor(classWriter, propertyElement);
}
makeDoEquals(classWriter);
makeDoHashCode(classWriter);
makeDoToString(classWriter);
makeDoDiff(classWriter);
classWriter.visitEnd();
}
private void makeFields(ClassVisitor classVisitor) {
//visitField(classVisitor, ACC_STATIC, POJO_CLASS_FIELD_NAME, classDesc(Class.class));
for (PropertyElement property: classProperties.getToStringProperties()) {
visitField(
classVisitor, ACC_STATIC, propertyFormatterName(property), classDesc(EnhancedPropertyFormatter.class));
}
for (PropertyElement property: classProperties.getAllProperties()) {
visitField(
classVisitor, ACC_STATIC, propertyElementName(property), classDesc(PropertyElement.class));
}
}
private static void visitField(ClassVisitor classVisitor, int flags, String name, String classDescriptor) {
classVisitor.visitField(flags, name, classDescriptor, null, null).visitEnd();
}
/**
* Generate an accessor method for a property. The generated method uses InvokeDynamic, calling the method generated
* by {@link #makeBootstrapMethod(ClassVisitor)}
* @param classWriter
* @param propertyElement the property to generate the accessor for
*/
private void makeAccessor(ClassVisitor classWriter, PropertyElement propertyElement) {
LocalVariable pojo = new LocalVariable("pojo", Object.class, null, 0);
int maxStackSize = 1;
String accessorName = propertyAccessorName(propertyElement);
mv = classWriter.visitMethod(
ACC_PRIVATE | ACC_STATIC, accessorName, accessorMethodDescription(propertyElement), null, null);
mv.visitCode();
Label start = visitNewLabel();
pojo.acceptLoad(mv);
visitLineNumber(4, propertyElement);
mv.visitInvokeDynamicInsn(
accessorName, accessorMethodDescription(propertyElement), bootstrapMethod, Type.getType(pojomatorInternalClassDesc));
visitLineNumber(5, propertyElement);
// return using the appropriate return byte code, based on type
Class<?> propertyType = propertyElement.getPropertyType();
if (propertyType.isPrimitive()) {
if (propertyType == float.class) {
mv.visitInsn(FRETURN);
}
else if (propertyType == long.class) {
maxStackSize++;
mv.visitInsn(LRETURN);
}
else if (propertyType == double.class) {
maxStackSize++;
mv.visitInsn(DRETURN);
}
else {
mv.visitInsn(IRETURN);
}
}
else {
mv.visitInsn(ARETURN);
}
Label end = visitNewLabel();
pojo.withScope(start, end).acceptLocalVariable(mv);
mv.visitMaxs(maxStackSize, 2);
mv.visitEnd();
}
private void makeConstructor(ClassVisitor cw) {
LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
LocalVariable varPojoClass = new LocalVariable(POJO_CLASS_FIELD_NAME, Class.class, null, 1);
LocalVariable varClassProperties = new LocalVariable("classProperties", ClassProperties.class, null, 2);
mv = cw.visitMethod(ACC_PUBLIC, "<init>", methodDesc(void.class, Class.class, ClassProperties.class), null, null);
mv.visitCode();
Label start = visitNewLabel();
varThis.acceptLoad(mv);
varPojoClass.acceptLoad(mv);
varClassProperties.acceptLoad(mv);
visitLineNumber(6, null);
construct(BasePojomator.class, Class.class, ClassProperties.class);
mv.visitInsn(RETURN);
Label end = visitNewLabel();
varThis.withScope(start, end).acceptLocalVariable(mv);
varPojoClass.withScope(start, end).acceptLocalVariable(mv);
varClassProperties.withScope(start, end).acceptLocalVariable(mv);
mv.visitMaxs(3, 3);
mv.visitEnd();
}
/**
* Generate the {@link Pojomator#doEquals(Object, Object)} method.
* @param cw
*/
private void makeDoEquals(ClassVisitor cw) {
LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
LocalVariable varPojo1 = new LocalVariable("pojo1", pojoClass, pojoDescriptor, 1);
LocalVariable varPojo2 = new LocalVariable("pojo2", pojoClass, pojoDescriptor, 2);
StackAdjustments stackAdjustments = new StackAdjustments();
Object[] localVars = new Object[] {pojomatorInternalClassName, OBJECT_INTERNAL_NAME, OBJECT_INTERNAL_NAME};
mv = cw.visitMethod(ACC_PUBLIC, "doEquals", methodDesc(boolean.class, Object.class, Object.class), null, null);
// where to jump if we should return false
Label returnFalse = new Label();
// where to jump if we determine that pojo1 and pojo2 have types which are compatible for equality
Label compatibleTypes = new Label();
mv.visitCode();
Label start = visitNewLabel();
varPojo1.acceptLoad(mv);
visitLineNumber(7, null);
checkNotNull();
varPojo2.acceptLoad(mv);
visitLineNumber(8, null);
Label notSameInstance = new Label();
mv.visitJumpInsn(IF_ACMPNE, notSameInstance);
// same instance; return true
mv.visitInsn(ICONST_1);
mv.visitInsn(IRETURN);
mv.visitLabel(notSameInstance);
// if other is null, return false.
mv.visitFrame(F_FULL, 3, localVars, 0, NO_STACK);
visitLineNumber(9, null);
varPojo2.acceptLoad(mv);
mv.visitJumpInsn(IFNULL, returnFalse);
// common case: if both types are the same, they are compatible for equality
varThis.acceptLoad(mv);
visitLineNumber(10, null);
invokeVirtual(Object.class, "getClass", Class.class);
varPojo1.acceptLoad(mv);
visitLineNumber(11, null);
invokeVirtual(Object.class, "getClass", Class.class);
mv.visitJumpInsn(IF_ACMPEQ, compatibleTypes);
// types are not the same; check for compatibility
varThis.acceptLoad(mv);
varPojo2.acceptLoad(mv);
visitLineNumber(12, null);
invokeVirtual(Object.class, "getClass", Class.class);
visitLineNumber(13, null);
invokeVirtual(BasePojomator.class, "isCompatibleForEquality", boolean.class, Class.class);
mv.visitJumpInsn(IFEQ, returnFalse);
// types are compatible, so start comparing properties
mv.visitLabel(compatibleTypes);
mv.visitFrame(F_FULL, 3, localVars, 0, NO_STACK);
// Compare properties
for(PropertyElement propertyElement: classProperties.getEqualsProperties()) {
visitLineNumber(14, propertyElement);
visitAccessorAndConvert(varPojo1, propertyElement);
visitLineNumber(15, propertyElement);
visitAccessorAndConvert(varPojo2, propertyElement);
visitLineNumber(16, propertyElement);
compareProperties(mv, returnFalse, propertyElement, stackAdjustments);
}
// If we have gotten this far, all properties are equal, so return true.
mv.visitInsn(ICONST_1);
mv.visitInsn(IRETURN);
mv.visitLabel(returnFalse);
mv.visitFrame(F_FULL, 3, localVars, 0, NO_STACK);
mv.visitInsn(ICONST_0);
mv.visitInsn(IRETURN);
Label end = visitNewLabel();
varThis.withScope(start, end).acceptLocalVariable(mv);
varPojo1.withScope(start, end).acceptLocalVariable(mv);
varPojo2.withScope(start, end).acceptLocalVariable(mv);
mv.visitMaxs(2 + stackAdjustments.adjustments(2), 3);
mv.visitEnd();
}
/**
* Compare a property from each pojo. It is assumed when this method is called that both property values have been
* loaded onto the stack. In the event the property value is a float or double, it is further assumed that it has
* been converted to an int or long.
* @param mv
* @param notEqualLabel where to jump if the property values are not equal
* @param propertyElement the property being compared
* @param stackAdjustments adjustments to be made to the max stack size, based on property type
*/
private void compareProperties(
MethodVisitor mv, Label notEqualLabel, PropertyElement propertyElement, StackAdjustments stackAdjustments) {
Class<?> propertyType = propertyElement.getPropertyType();
if (propertyType.isPrimitive()) {
if (isWide(propertyElement)) {
stackAdjustments.wideProperty = true;
mv.visitInsn(LCMP);
mv.visitJumpInsn(IFNE, notEqualLabel);
}
else {
mv.visitJumpInsn(IF_ICMPNE, notEqualLabel);
}
}
else {
if(propertyType.isArray()) {
Class<?> componentType = propertyType.getComponentType();
if (componentType.isPrimitive()) {
visitLineNumber(17, propertyElement);
invokeStatic(Arrays.class, "equals",boolean.class, propertyType, propertyType);
}
else {
visitLineNumber(18, propertyElement);
invokeStatic(BasePojomator.class, "compareArrays", boolean.class, Object.class, Object.class);
}
}
else {
if (isObjectPossiblyHoldingArray(propertyElement)) {
visitLineNumber(19, propertyElement);
invokeStatic(BasePojomator.class, "areObjectValuesEqual", boolean.class, Object.class, Object.class);
}
else {
visitLineNumber(20, propertyElement);
invokeStatic(Objects.class, "equals", boolean.class, Object.class, Object.class);
}
}
mv.visitJumpInsn(IFEQ, notEqualLabel);
}
}
/**
* Generate the {@link Pojomator#doHashCode(Object)} method.
* @param cw
*/
private void makeDoHashCode(ClassVisitor cw) {
LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
LocalVariable varPojo = new LocalVariable("pojo", pojoClass, pojoDescriptor, 1);
int longOrDoubleStackAdjustment = 0;
Object[] localVars = new Object[] {pojomatorInternalClassName, OBJECT_INTERNAL_NAME};
mv = cw.visitMethod(ACC_PUBLIC, "doHashCode", methodDesc(int.class, Object.class), null, null);
mv.visitCode();
Label start = visitNewLabel();
visitLineNumber(21, null);
varPojo.acceptLoad(mv);
visitLineNumber(22, null);
checkNotNullPop();
//algorithm:
// hashCode(prop_n) + 31 * (hashCode(prop_n-1) + 31 * ( ... (hashCode(prop_1) + 31 * 1) ... ))
mv.visitInsn(ICONST_1); // this will just be multiplied by 31; let the optimizer take care of it
for(PropertyElement propertyElement: classProperties.getHashCodeProperties()) {
// multiply what we have so far by 31.
visitLineNumber(23, propertyElement);
mv.visitIntInsn(BIPUSH, 31);
visitLineNumber(24, propertyElement);
mv.visitInsn(IMUL);
visitLineNumber(25, propertyElement);
visitAccessorAndConvert(varPojo, propertyElement); // grab the property value, converting a float or double
Class<?> propertyType = propertyElement.getPropertyType();
if (propertyType.isPrimitive()) {
// need to compute the hash code for this primitive value, based on its type
switch (propertyType.getName()) {
case "boolean":
visitLineNumber(26, propertyElement);
Label ifeq = new Label();
mv.visitJumpInsn(IFEQ, ifeq);
mv.visitIntInsn(SIPUSH, Boolean.TRUE.hashCode());
Label hashCodeDetermined = new Label();
mv.visitJumpInsn(GOTO, hashCodeDetermined);
mv.visitLabel(ifeq);
mv.visitFrame(F_FULL, 2, localVars, 1, new Object[] {INTEGER});
mv.visitIntInsn(SIPUSH, Boolean.FALSE.hashCode());
mv.visitLabel(hashCodeDetermined);
mv.visitFrame(F_FULL, 2, localVars, 2, new Object[] {INTEGER, INTEGER});
break;
case "byte":
case "char":
case "int":
case "short":
case "float":
break; // already an int (from the JVM's point of view)
case "double":
case "long":
longOrDoubleStackAdjustment = 3; // one extra for the field, two extra for the dup to do an xor
// compute bits ^ (bits >> 32)
visitLineNumber(27, propertyElement);
// we'll need a second copy to do the xor:
mv.visitInsn(DUP2);
// bitshift 32 right:
mv.visitIntInsn(BIPUSH, 32);
mv.visitInsn(LUSHR);
// xor with the original
mv.visitInsn(LXOR);
// chop of the high 32 bits
mv.visitInsn(L2I);
break;
default:
throw new IllegalStateException("unknown primitive type " + propertyType.getName());
}
}
else {
Label ifNonNull = new Label();
Label hashCodeDetermined = new Label();
mv.visitInsn(DUP); // if it is non-null, let's not have to get it a second time.
mv.visitJumpInsn(IFNONNULL, ifNonNull);
// it's null
mv.visitInsn(POP); // won't need that duped copy after all
mv.visitInsn(ICONST_0);
mv.visitJumpInsn(GOTO, hashCodeDetermined);
// it's not null
mv.visitLabel(ifNonNull);
mv.visitFrame(
F_FULL, 2, localVars, 2, new Object[] {INTEGER, Type.getInternalName(effectiveType(propertyType))});
if(propertyType.isArray()) {
visitLineNumber(28, propertyElement);
invokeStatic(
Arrays.class,
isDeepArray(propertyType) ? "deepHashCode" : "hashCode",
int.class,
propertyType.getComponentType().isPrimitive() ? propertyType : Object[].class);
}
else if (isObjectPossiblyHoldingArray(propertyElement)) {
// it *could* be an array; if so, we want to do an array hashCode.
mv.visitInsn(DUP); // we'll still want the property value handy after calling getClass().isArray()
invokeVirtual(Object.class, "getClass", Class.class);
visitLineNumber(29, propertyElement);
invokeVirtual(Class.class, "isArray", boolean.class);
Label isArray = new Label();
mv.visitJumpInsn(IFNE, isArray); // if true
// regular old hashCode
visitLineNumber(30, propertyElement);
invokeVirtual(Object.class, "hashCode", int.class);
mv.visitJumpInsn(GOTO, hashCodeDetermined);
// add a deep parameter to arrayHashCode, like we did for compareProperties
mv.visitLabel(isArray);
mv.visitFrame(F_FULL, 2, localVars, 2, new Object[] { INTEGER, Type.getInternalName(propertyType) });
mv.visitInsn(ICONST_1);
visitLineNumber(31, propertyElement);
invokeStatic(BasePojomator.class, "arrayHashCode", int.class, Object.class, boolean.class);
}
else {
visitLineNumber(32, propertyElement);
invokeVirtual(Object.class, "hashCode", int.class);
}
mv.visitLabel(hashCodeDetermined);
mv.visitFrame(F_FULL, 2, localVars, 2, new Object[] {INTEGER, INTEGER});
}
// add result to what we have so far
mv.visitInsn(IADD);
}
mv.visitInsn(IRETURN);
Label end = visitNewLabel();
varThis.withScope(start, end).acceptLocalVariable(mv);
varPojo.withScope(start, end).acceptLocalVariable(mv);
mv.visitMaxs(3 + longOrDoubleStackAdjustment, 2);
mv.visitEnd();
}
/**
* Generate {@link Pojomator#doToString(Object)}
* @param cw
*/
private void makeDoToString(ClassVisitor cw) {
int longOrDoubleStackAdjustment = 1;
LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
LocalVariable varPojo = new LocalVariable("pojo", pojoClass, null, 1);
LocalVariable varPojoFormatter=
new LocalVariable("pojoFormattor", classDesc(EnhancedPojoFormatter.class), null, 2);
LocalVariable varBuilder= new LocalVariable("builder", classDesc(String.class), null, 3);
mv = cw.visitMethod(ACC_PUBLIC, "doToString", methodDesc(String.class, Object.class), null, null);
mv.visitCode();
Label start = visitNewLabel();
varPojo.acceptLoad(mv);
checkNotNullPop();
constructEnhancedPojoFormatter();
varPojoFormatter.acceptStore(mv);
visitLineNumber(33, null);
mv.visitTypeInsn(NEW, internalName(StringBuilder.class));
mv.visitInsn(DUP);
construct(StringBuilder.class);
varBuilder.acceptStore(mv);
varPojoFormatter.acceptLoad(mv);
varBuilder.acceptLoad(mv);
loadPojoClass(varThis);
visitLineNumber(34, null);
invokeInterface(EnhancedPojoFormatter.class, "appendToStringPrefix", void.class, StringBuilder.class, Class.class);
for(PropertyElement propertyElement: classProperties.getToStringProperties()) {
if (isWide(propertyElement)) {
longOrDoubleStackAdjustment = 1; // having any double-wide values on our stack increases max stack depth by one
}
// append the property prefix
varPojoFormatter.acceptLoad(mv);
varBuilder.acceptLoad(mv);
visitLineNumber(35, propertyElement);
loadPropertyElementField(propertyElement);
visitLineNumber(36, propertyElement);
invokeInterface(
EnhancedPojoFormatter.class, "appendPropertyPrefix", void.class, StringBuilder.class, PropertyElement.class);
// get the propertyFormatter for this property
visitLineNumber(37, propertyElement);
mv.visitFieldInsn(
GETSTATIC,
pojomatorInternalClassName,
propertyFormatterName(propertyElement),
classDesc(EnhancedPropertyFormatter.class));
// The propertyFormatter will format the property value and append the results to our StringBuilder
varBuilder.acceptLoad(mv);
visitLineNumber(38, propertyElement);
visitAccessor(varPojo, propertyElement);
Class<?> appendType = appendFormattedType(propertyElement.getPropertyType());
if (isObjectPossiblyHoldingArray(propertyElement)) {
visitLineNumber(39, propertyElement);
invokeInterface(
EnhancedPropertyFormatter.class, "appendFormattedPossibleArray", void.class, StringBuilder.class, appendType);
}
else {
visitLineNumber(40, propertyElement);
invokeInterface(
EnhancedPropertyFormatter.class, "appendFormatted", void.class, StringBuilder.class, appendType);
}
// have any property suffix appended to the StringBuilder
varPojoFormatter.acceptLoad(mv);
varBuilder.acceptLoad(mv);
visitLineNumber(41, propertyElement);
loadPropertyElementField(propertyElement);
visitLineNumber(42, propertyElement);
invokeInterface(
EnhancedPojoFormatter.class, "appendPropertySuffix", void.class, StringBuilder.class, PropertyElement.class);
}
// Have any toString suffix appended
varPojoFormatter.acceptLoad(mv);
varBuilder.acceptLoad(mv);
loadPojoClass(varThis);
visitLineNumber(43, null);
invokeInterface(EnhancedPojoFormatter.class, "appendToStringSuffix", void.class, StringBuilder.class, Class.class);
// invoke toString and return the result
varBuilder.acceptLoad(mv);
visitLineNumber(44, null);
invokeVirtual(StringBuilder.class, "toString", String.class);
mv.visitInsn(ARETURN);
Label end = visitNewLabel();
varThis.withScope(start, end).acceptLocalVariable(mv);
varPojo.withScope(start, end).acceptLocalVariable(mv);
varPojoFormatter.withScope(start, end).acceptLocalVariable(mv);
varBuilder.withScope(start, end).acceptLocalVariable(mv);
mv.visitMaxs(3 + longOrDoubleStackAdjustment, 4);
mv.visitEnd();
}
private static Class<?> appendFormattedType(Class<?> propertyType) {
if (propertyType.isPrimitive()) {
return propertyType;
}
else if (propertyType.isArray()) {
return propertyType.getComponentType().isPrimitive() ? propertyType : Object[].class;
}
else {
return Object.class;
}
}
private void loadPropertyElementField(PropertyElement propertyElement) {
mv.visitFieldInsn(
GETSTATIC,
pojomatorInternalClassName,
propertyElementName(propertyElement),
classDesc(PropertyElement.class));
}
/**
* Construct the pojoFormatter to use. This method will contribute 2 or 4 to the max stack depth,
* depending on whether the pojoFormatter implements {@link EnhancedPojoFormatter} or not.
*/
@SuppressWarnings("deprecation")
private void constructEnhancedPojoFormatter() {
PojoFormat format = pojoClass.getAnnotation(PojoFormat.class);
if (format == null) {
mv.visitTypeInsn(NEW, internalName(DefaultEnhancedPojoFormatter.class));
mv.visitInsn(DUP);
visitLineNumber(45, null);
construct(DefaultEnhancedPojoFormatter.class);
}
else {
Class<? extends org.pojomatic.formatter.PojoFormatter> pojoFormatterClass = format.value();
// if it isn't an enhanced formatter, we'll need to wrap it a EnhancedPojoFormatter. If we do this, we'll first
// invoke new on the wrapper, then construct the underlying formatter, then call the constructor on the wrapper.
boolean isEnhancedFormatter = EnhancedPojoFormatter.class.isAssignableFrom(pojoFormatterClass);
if (! isEnhancedFormatter) {
visitLineNumber(46, null);
mv.visitTypeInsn(NEW, ENHANCED_POJO_FORMATTER_WRAPPER_INTERNAL_NAME);
mv.visitInsn(DUP);
}
mv.visitTypeInsn(NEW, internalName(pojoFormatterClass));
mv.visitInsn(DUP);
visitLineNumber(47, null);
construct(pojoFormatterClass);
if (! isEnhancedFormatter) {
visitLineNumber(48, null);
construct(
org.pojomatic.internal.EnhancedPojoFormatterWrapper.class, org.pojomatic.formatter.PojoFormatter.class);
}
}
}
/**
* Load a reference to the pojo class. We cannot refer to this directly, since the class may not be visible to us,
* so instead, we refer to the instance variable in {@link BasePojomator}.
* @param varThis the "this" local variable.
*/
private void loadPojoClass(LocalVariable varThis) {
visitLineNumber(49, null);
varThis.acceptLoad(mv);
mv.visitFieldInsn(GETFIELD, BASE_POJOMATOR_INTERNAL_NAME, POJO_CLASS_FIELD_NAME, classDesc(Class.class));
//mv.visitFieldInsn(GETSTATIC, pojomatorInternalClassName, POJO_CLASS_FIELD_NAME, classDesc(Class.class));
}
/**
* Generate {@link Pojomator#doDiff(Object, Object)}
* @param cw
*/
private void makeDoDiff(ClassVisitor cw) {
LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
LocalVariable varPojo1 = new LocalVariable("instance", pojoClass, pojoDescriptor, 1);
LocalVariable varPojo2 = new LocalVariable("other", pojoClass, pojoDescriptor, 2);
LocalVariable varDifferencesList = new LocalVariable(
"differences", List.class, "Ljava/util/List<Lorg/pojomatic/diff/Difference;>;", 3);
StackAdjustments stackAdjustments = new StackAdjustments();
Object[] localVarTypes = new Object[] {
pojomatorInternalClassName, OBJECT_INTERNAL_NAME, OBJECT_INTERNAL_NAME, internalName(List.class), null, null };
mv = cw.visitMethod(ACC_PUBLIC, "doDiff", methodDesc(Differences.class, Object.class, Object.class), null, null);
mv.visitCode();
Label start = visitNewLabel();
varPojo1.acceptLoad(mv);
visitLineNumber(50, null);
checkNotNull("instance is null");
varPojo2.acceptLoad(mv);
checkNotNull("other is null");
// If instance and other are the same object, then return NoDifferences.getInstance();
Label notSameInstance = new Label();
mv.visitJumpInsn(IF_ACMPNE, notSameInstance);
invokeStatic(NoDifferences.class, "getInstance", NoDifferences.class);
mv.visitInsn(ARETURN);
// not the same instance, some work to do
mv.visitLabel(notSameInstance);
mv.visitFrame(F_FULL, 3, localVarTypes, 0, NO_STACK);
visitLineNumber(51, null);
checkCompatibleForEquality(varThis, varPojo1, "instance");
visitLineNumber(52, null);
checkCompatibleForEquality(varThis, varPojo2, "other");
Label makeDiferences = notSameInstance;
mv.visitTypeInsn(NEW, "java/util/ArrayList");
mv.visitInsn(DUP);
construct(ArrayList.class);
varDifferencesList.acceptStore(mv);
List<LocalVariable> propertyVariables = new ArrayList<>(); // these will occur in a block scope
// compare properties
for(PropertyElement propertyElement: classProperties.getHashCodeProperties()) {
int width = isWide(propertyElement) ? 2 : 1;
Class<?> propertyType = propertyElement.getPropertyType();
LocalVariable varProp1 = new LocalVariable(
"property_" + propertyElement.getName() + "_1", propertyType, null, 4);
//If the type is long or double, we need to store the next var at 6, not 5.
LocalVariable varProp2 = new LocalVariable(
"property_" + propertyElement.getName() + "_2", propertyType, null, 4 + width);
propertyVariables.add(varProp1);
propertyVariables.add(varProp2);
Label blockStart = visitNewLabel();
visitLineNumber(53, propertyElement);
visitAccessor(varPojo1, propertyElement);
varProp1.acceptStore(mv);
visitLineNumber(54, propertyElement);
visitAccessor(varPojo2, propertyElement);
varProp2.acceptStore(mv);
visitLineNumber(55, propertyElement);
visitAccessorAndConvert(varPojo1, propertyElement);
visitLineNumber(56, propertyElement);
visitAccessorAndConvert(varPojo2, propertyElement);
Label propertiesNotEqual = new Label();
Label next = new Label();
visitLineNumber(57, propertyElement);
compareProperties(mv, propertiesNotEqual, propertyElement, stackAdjustments);
mv.visitJumpInsn(GOTO, next); // there were no differences.
mv.visitLabel(propertiesNotEqual);
localVarTypes[5] = localVarTypes[4] = propertyType.isPrimitive()
? Primitives.getOpcode(propertyType)
: internalName(effectiveType(propertyType));
mv.visitFrame(F_FULL, 6, localVarTypes, 0, NO_STACK);
// Create a ValueDifference instance, initialized with the property name and the two values, and add it to our list
varDifferencesList.acceptLoad(mv); // we'll need this to add to the list
mv.visitTypeInsn(NEW, "org/pojomatic/diff/ValueDifference");
mv.visitInsn(DUP);
mv.visitLdcInsn(propertyElement.getName());
varProp1.acceptLoad(mv);
visitLineNumber(58, propertyElement);
convertToObject(propertyType);
varProp2.acceptLoad(mv);
visitLineNumber(59, propertyElement);
convertToObject(propertyType);
visitLineNumber(60, propertyElement);
construct(ValueDifference.class, String.class, Object.class, Object.class);
// add the ValueDifference instance to our list
visitLineNumber(61, propertyElement);
invokeInterface(List.class, "add", boolean.class, Object.class);
mv.visitInsn(POP); // ignore the return value of List#add
mv.visitLabel(next);
mv.visitFrame(F_FULL, 4, localVarTypes, 0, NO_STACK);
varProp1.withScope(blockStart, next);
varProp2.withScope(blockStart, next);
}
// if our list is empty, return the NoDifferences instance
varDifferencesList.acceptLoad(mv);
visitLineNumber(62, null);
invokeInterface(List.class, "isEmpty", boolean.class);
Label hasDifferences = new Label();
mv.visitJumpInsn(IFEQ, hasDifferences);
visitLineNumber(63, null);
invokeStatic(NoDifferences.class, "getInstance", NoDifferences.class);
mv.visitInsn(ARETURN);
// our list is not empty, so wrap it in a PropertyDiferences instance
mv.visitLabel(hasDifferences);
mv.visitFrame(F_FULL, 4, localVarTypes, 0, NO_STACK);
visitLineNumber(64, null);
mv.visitTypeInsn(NEW, internalName(PropertyDifferences.class));
mv.visitInsn(DUP);
varDifferencesList.acceptLoad(mv);
visitLineNumber(65, null);
construct(PropertyDifferences.class, List.class);
mv.visitInsn(ARETURN);
Label end = visitNewLabel();
varThis.withScope(start, end).acceptLocalVariable(mv);
varPojo1.withScope(start, end).acceptLocalVariable(mv);
varPojo2.withScope(start, end).acceptLocalVariable(mv);
varDifferencesList.withScope(makeDiferences, end).acceptLocalVariable(mv);
for (LocalVariable var: propertyVariables) {
var.acceptLocalVariable(mv);
}
mv.visitMaxs(6 + stackAdjustments.adjustments(2), 6 + stackAdjustments.adjustments(2));
mv.visitEnd();
}
/**
* Invoke {@link BasePojomator#checkCompatibleForEquality(Object, String)} on the specified variable
* @param message the message to include in the {@link IllegalArgumentException} if the variable fails the
* class test
* @param varNumber the variable number to check
*/
private void checkCompatibleForEquality(LocalVariable varThis, LocalVariable var, String message) {
varThis.acceptLoad(mv);
var.acceptLoad(mv);
mv.visitLdcInsn(message);
visitLineNumber(66, null);
invokeVirtual(BasePojomator.class, "checkCompatibleForEquality", void.class, Object.class, String.class);
}
/**
* Determine if the given propertyElement of array type should be treated as possibly containing a multi-level array.
* This will be the case if it is:
* <ul>
* <li>of type Object and is not annotated with @{@link SkipArrayCheck}</li>
* <li>of type Object[]</li>
* <li>of array type with a component type of array type</li>
* </ul>
* @param propertyElement
* @return {@code true} if the given propertyElement should be treated as possibly containing a multi-level array,
* or {@code false} otherwise.
*/
private boolean isDeepArray(Class<?> propertyType) {
return propertyType.equals(Object[].class) || propertyType.getComponentType().isArray();
}
/**
* Determine if the given propertyElement should be treated as one that could be an array.
* @param propertyElement
* @return {@code true} if the given propertyElement is either of array type, or is of type Object and not annotated
* with @{@link SkipArrayCheck}
*/
private boolean isObjectPossiblyHoldingArray(PropertyElement propertyElement) {
return Object.class.equals(propertyElement.getPropertyType())
&& ! propertyElement.getElement().isAnnotationPresent(SkipArrayCheck.class);
}
/**
* If the parameter on the stack (of type propertyType) is primitive, convert it to the appropriate wrapper object.
* Otherwise, leave it alone
* @param propertyType the type of the parameter on the stack
*/
private void convertToObject(Class<?> propertyType) {
if (propertyType.isPrimitive()) {
Class<?> wrapperClass = Primitives.getWrapperClass(propertyType);
invokeStatic(wrapperClass, "valueOf", wrapperClass, propertyType);
}
}
/**
* Visit an accessor, converting floats or doubles to int bits or long bits respectively.
* @param propertyElement the property to access
* @param variableNumber the index of the local variable holding a the pojo instance to access
*/
private void visitAccessorAndConvert(LocalVariable var, PropertyElement propertyElement) {
visitAccessor(var, propertyElement);
if (propertyElement.getPropertyType().equals(float.class)) {
invokeStatic(Float.class, "floatToIntBits", int.class, float.class);
}
else if (propertyElement.getPropertyType().equals(double.class)) {
invokeStatic(Double.class, "doubleToLongBits", long.class, double.class);
}
}
/**
* Visit an accessor
* @param var the index of the local variable holding a the pojo instance to access
* @param propertyElement the property to access
*/
private void visitAccessor(LocalVariable var, PropertyElement propertyElement) {
var.acceptLoad(mv);
mv.visitMethodInsn(
INVOKESTATIC, pojomatorInternalClassName, propertyAccessorName(propertyElement),
accessorMethodDescription(propertyElement), false);
}
private String accessorMethodDescription(PropertyElement propertyElement) {
return methodDesc(effectiveType(propertyElement.getPropertyType()), Object.class);
}
/**
* Determine what, for our purposes, is the effective type of a property. Since a property type may be a class which
* is not visible to us, we'll treat any type which is neither primitive nor an array to be of type Object.
* A primitive type or array of primitives will be returned as is. All other array types will be returned as Object[].
* @param propertyClass the class to determine the effective type for.
* @return the effective type of {@code propertyClass}
*/
private Class<?> effectiveType(Class<?> propertyClass) {
if (propertyClass.isArray()) {
return propertyClass.getComponentType().isPrimitive() ? propertyClass : Object[].class;
}
else {
return propertyClass.isPrimitive() ? propertyClass : Object.class;
}
}
/**
* Create a new label and visit it.
* @return the new label
*/
private Label visitNewLabel() {
Label label = new Label();
mv.visitLabel(label);
return label;
}
/**
* Visit a line number, based on a provided number, and a propertyElement (possibly null). The propertyElement will
* be used to distinguish line numbers generated from the same place in the code of this class, but for different
* properties.
* </p>
* To ensure unique line numbers, run the following:
* <code>
* perl -pi -e 'if (/visitLineNumber\(/) { $i++; s/visitLineNumber\(mv, \d+/visitLineNumber\(mv, $i/; }' \
* src/main/java/org/pojomatic/internal/PojomatorByteCodeGenerator.java
* </code>
* @param lineNumberBase
* @param propertyElement
*/
private void visitLineNumber(int lineNumberBase, PropertyElement propertyElement) {
Integer offset = propertyNumbers.get(propertyElement);
mv.visitLineNumber(lineNumberBase + 100 * (offset == null ? 0 : offset), visitNewLabel());
}
/**
* Determine if the type of a property is "wide" - i.e. is a long or double.
* @param propertyElement
* @return
*/
private static boolean isWide(PropertyElement propertyElement) {
Class<?> type = propertyElement.getPropertyType();
return type == long.class || type == double.class;
}
/**
* Pop the top element off of the stack and invoke {@link BasePojomator#checkNotNull(Object)} on it.
*/
private void checkNotNullPop() {
invokeStatic(BasePojomator.class, "checkNotNullPop", void.class, Object.class);
}
/**
* Invoke {@link BasePojomator#checkNotNull(Object)} on the top element of the stack, leaving that element there.
*/
private void checkNotNull() {
invokeStatic(BasePojomator.class, "checkNotNull", Object.class, Object.class);
}
/**
* Invoke {@link BasePojomator#checkNotNull(Object, String)} on the top element of the stack, leaving that element there.
* @param message the message to include in the {@link NullPointerException} if the top element is null
*/
private void checkNotNull(String message) {
mv.visitLdcInsn(message);
invokeStatic(BasePojomator.class, "checkNotNull", Object.class, Object.class, String.class);
}
private void invokeStatic(Class<?> ownerClass, String methodName, Class<?> returnType, Class<?>... parameterTypes) {
mv.visitMethodInsn(
INVOKESTATIC, internalName(ownerClass), methodName, methodDesc(returnType, parameterTypes), false);
}
private void invokeInterface(
Class<?> ownerClass, String methodName, Class<?> returnType, Class<?>... parameterTypes) {
mv.visitMethodInsn(
INVOKEINTERFACE, internalName(ownerClass), methodName, methodDesc(returnType, parameterTypes), true);
}
private void invokeVirtual(
Class<?> ownerClass, String methodName, Class<?> returnType, Class<?>... parameterTypes) {
mv.visitMethodInsn(
INVOKEVIRTUAL, internalName(ownerClass), methodName, methodDesc(returnType, parameterTypes), false);
}
private void construct(
Class<?> ownerClass, Class<?>... parameterTypes) {
mv.visitMethodInsn(
INVOKESPECIAL, internalName(ownerClass), "<init>", methodDesc(void.class, parameterTypes), false);
}
private static String internalName(Class<?> clazz) {
return internalName(clazz.getName());
}
private static String internalName(String className) {
return className.replace('.', '/');
}
private static String classDesc(Class<?> clazz) {
return Type.getDescriptor(clazz);
}
private static String methodDesc(Class<?> returnType, Class<?>... parameterTypes) {
return MethodType.methodType(returnType, parameterTypes).toMethodDescriptorString();
}
private static String propertyAccessorName(PropertyElement property) {
return "get_" + qualifiedPropertyName(property);
}
static String propertyElementName(PropertyElement property) {
return "element_" + qualifiedPropertyName(property);
}
static String propertyFormatterName(PropertyElement property) {
return "formatter_" + qualifiedPropertyName(property);
}
private static String qualifiedPropertyName(PropertyElement property) {
return property.getType()
+ "_" + property.getDeclaringClass().getName().replace('.', '$')
+ "_" + property.getElementName();
}
}