View Javadoc
1   package org.pojomatic.internal;
2   
3   import java.lang.invoke.CallSite;
4   import java.lang.invoke.MethodHandles;
5   import java.lang.invoke.MethodType;
6   import java.util.ArrayList;
7   import java.util.Arrays;
8   import java.util.HashMap;
9   import java.util.List;
10  import java.util.Map;
11  import java.util.Objects;
12  import java.util.concurrent.atomic.AtomicLong;
13  
14  import org.objectweb.asm.ClassWriter;
15  import org.objectweb.asm.ClassVisitor;
16  import org.objectweb.asm.Handle;
17  import org.objectweb.asm.Label;
18  import org.objectweb.asm.MethodVisitor;
19  import org.objectweb.asm.Type;
20  import org.pojomatic.Pojomator;
21  import org.pojomatic.PropertyElement;
22  import org.pojomatic.annotations.PojoFormat;
23  import org.pojomatic.annotations.SkipArrayCheck;
24  import org.pojomatic.diff.Differences;
25  import org.pojomatic.diff.NoDifferences;
26  import org.pojomatic.diff.PropertyDifferences;
27  import org.pojomatic.diff.ValueDifference;
28  import org.pojomatic.formatter.DefaultEnhancedPojoFormatter;
29  import org.pojomatic.formatter.EnhancedPojoFormatter;
30  import org.pojomatic.formatter.EnhancedPropertyFormatter;
31  
32  import static org.objectweb.asm.Opcodes.*;
33  
34  class PojomatorByteCodeGenerator {
35    @Deprecated
36    private static final String ENHANCED_POJO_FORMATTER_WRAPPER_INTERNAL_NAME =
37      internalName(org.pojomatic.internal.EnhancedPojoFormatterWrapper.class);
38    private static final Object[] NO_STACK = new Object[] {};
39    private static final String OBJECT_INTERNAL_NAME = internalName(Object.class);
40    private static final String BASE_POJOMATOR_INTERNAL_NAME = internalName(BasePojomator.class);
41    static final String POJO_CLASS_FIELD_NAME = "pojoClass";
42    private static final String BOOTSTRAP_METHOD_NAME = "bootstrap";
43  
44    private static final AtomicLong counter = new AtomicLong();
45  
46    final String pojomatorClassName;
47    private final String pojomatorInternalClassName;
48    private final String pojomatorInternalClassDesc;
49    private final Class<?> pojoClass;
50    private final String pojoDescriptor;
51    private final ClassProperties classProperties;
52    private final Handle bootstrapMethod;
53    private final Map<PropertyElement, Integer> propertyNumbers = new HashMap<>();
54  
55    private MethodVisitor mv; // the active method visitor
56  
57    /**
58     * Class for tracking adjustments to be made to the max stack and/or localvariable size
59     */
60    private static class StackAdjustments {
61      /**
62       * At least one "wide" property (long or double) has been encountered
63       */
64      boolean wideProperty;
65  
66      int adjustments(int widePropertyWeight) {
67        return (wideProperty ? widePropertyWeight : 0);
68      }
69    }
70  
71    PojomatorByteCodeGenerator(Class<?> pojoClass, ClassProperties classProperties) {
72      this.pojomatorClassName = PojomatorStub.class.getName() + "$" + counter.incrementAndGet();
73      this.pojomatorInternalClassName = internalName(pojomatorClassName);
74      this.pojomatorInternalClassDesc = "L" + pojomatorInternalClassName + ";";
75      this.pojoClass = pojoClass;
76      this.pojoDescriptor = classDesc(pojoClass);
77      this.classProperties = classProperties;
78      this.bootstrapMethod = new Handle(
79        H_INVOKESTATIC,
80        BASE_POJOMATOR_INTERNAL_NAME,
81        BOOTSTRAP_METHOD_NAME,
82        methodDesc(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, Class.class), false);
83      int propertyNumber = 1;
84      for (PropertyElement property: classProperties.getAllProperties()) {
85        propertyNumbers.put(property, propertyNumber++);
86      }
87    }
88  
89    byte[] makeClassBytes() {
90      ClassWriter classWriter = new ClassWriter(0);
91      // acceptClassVisitor(new CheckClassAdapter(classWriter));
92      acceptClassVisitor(classWriter);
93      return classWriter.toByteArray();
94    }
95  
96    private void acceptClassVisitor(ClassVisitor classWriter) {
97      classWriter.visit(V1_7, ACC_PUBLIC + ACC_SUPER + ACC_SYNTHETIC, pojomatorInternalClassName, null,
98          BASE_POJOMATOR_INTERNAL_NAME, new String[] { internalName(Pojomator.class) });
99  
100     classWriter.visitSource("Look for visitLineNumber", null);
101 
102     makeFields(classWriter);
103 
104     makeConstructor(classWriter);
105 
106     for (PropertyElement propertyElement: classProperties.getAllProperties()) {
107       makeAccessor(classWriter, propertyElement);
108     }
109 
110     makeDoEquals(classWriter);
111     makeDoHashCode(classWriter);
112     makeDoToString(classWriter);
113     makeDoDiff(classWriter);
114 
115     classWriter.visitEnd();
116   }
117 
118   private void makeFields(ClassVisitor classVisitor) {
119     //visitField(classVisitor, ACC_STATIC, POJO_CLASS_FIELD_NAME, classDesc(Class.class));
120     for (PropertyElement property: classProperties.getToStringProperties()) {
121       visitField(
122         classVisitor, ACC_STATIC, propertyFormatterName(property), classDesc(EnhancedPropertyFormatter.class));
123     }
124     for (PropertyElement property: classProperties.getAllProperties()) {
125       visitField(
126         classVisitor, ACC_STATIC, propertyElementName(property), classDesc(PropertyElement.class));
127     }
128   }
129 
130   private static void visitField(ClassVisitor classVisitor, int flags, String name, String classDescriptor) {
131     classVisitor.visitField(flags, name, classDescriptor, null, null).visitEnd();
132   }
133 
134   /**
135    * Generate an accessor method for a property. The generated method uses InvokeDynamic, calling the method generated
136    * by {@link #makeBootstrapMethod(ClassVisitor)}
137    * @param classWriter
138    * @param propertyElement the property to generate the accessor for
139    */
140   private void makeAccessor(ClassVisitor classWriter, PropertyElement propertyElement) {
141     LocalVariable pojo = new LocalVariable("pojo", Object.class, null, 0);
142     int maxStackSize = 1;
143     String accessorName = propertyAccessorName(propertyElement);
144     mv = classWriter.visitMethod(
145       ACC_PRIVATE | ACC_STATIC, accessorName, accessorMethodDescription(propertyElement), null, null);
146     mv.visitCode();
147     Label start = visitNewLabel();
148     pojo.acceptLoad(mv);
149     visitLineNumber(4, propertyElement);
150     mv.visitInvokeDynamicInsn(
151       accessorName, accessorMethodDescription(propertyElement), bootstrapMethod, Type.getType(pojomatorInternalClassDesc));
152     visitLineNumber(5, propertyElement);
153 
154     // return using the appropriate return byte code, based on type
155     Class<?> propertyType = propertyElement.getPropertyType();
156     if (propertyType.isPrimitive()) {
157       if (propertyType == float.class) {
158         mv.visitInsn(FRETURN);
159       }
160       else if (propertyType == long.class) {
161         maxStackSize++;
162         mv.visitInsn(LRETURN);
163       }
164       else if (propertyType == double.class) {
165         maxStackSize++;
166         mv.visitInsn(DRETURN);
167       }
168       else {
169         mv.visitInsn(IRETURN);
170       }
171     }
172     else {
173       mv.visitInsn(ARETURN);
174     }
175 
176     Label end = visitNewLabel();
177     pojo.withScope(start, end).acceptLocalVariable(mv);
178     mv.visitMaxs(maxStackSize, 2);
179     mv.visitEnd();
180   }
181 
182   private void makeConstructor(ClassVisitor cw) {
183     LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
184     LocalVariable varPojoClass = new LocalVariable(POJO_CLASS_FIELD_NAME, Class.class, null, 1);
185     LocalVariable varClassProperties = new LocalVariable("classProperties", ClassProperties.class, null, 2);
186     mv = cw.visitMethod(ACC_PUBLIC, "<init>", methodDesc(void.class, Class.class, ClassProperties.class), null, null);
187     mv.visitCode();
188     Label start = visitNewLabel();
189     varThis.acceptLoad(mv);
190     varPojoClass.acceptLoad(mv);
191     varClassProperties.acceptLoad(mv);
192     visitLineNumber(6, null);
193     construct(BasePojomator.class, Class.class, ClassProperties.class);
194     mv.visitInsn(RETURN);
195     Label end = visitNewLabel();
196     varThis.withScope(start, end).acceptLocalVariable(mv);
197     varPojoClass.withScope(start, end).acceptLocalVariable(mv);
198     varClassProperties.withScope(start, end).acceptLocalVariable(mv);
199     mv.visitMaxs(3, 3);
200     mv.visitEnd();
201   }
202 
203   /**
204    * Generate the {@link Pojomator#doEquals(Object, Object)} method.
205    * @param cw
206    */
207   private void makeDoEquals(ClassVisitor cw) {
208     LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
209     LocalVariable varPojo1 = new LocalVariable("pojo1", pojoClass, pojoDescriptor, 1);
210     LocalVariable varPojo2 = new LocalVariable("pojo2", pojoClass, pojoDescriptor, 2);
211 
212     StackAdjustments stackAdjustments = new StackAdjustments();
213 
214     Object[] localVars = new Object[] {pojomatorInternalClassName, OBJECT_INTERNAL_NAME, OBJECT_INTERNAL_NAME};
215 
216     mv = cw.visitMethod(ACC_PUBLIC, "doEquals", methodDesc(boolean.class, Object.class, Object.class), null, null);
217 
218     // where to jump if we should return false
219     Label returnFalse = new Label();
220     // where to jump if we determine that pojo1 and pojo2 have types which are compatible for equality
221     Label compatibleTypes = new Label();
222 
223     mv.visitCode();
224     Label start = visitNewLabel();
225     varPojo1.acceptLoad(mv);
226     visitLineNumber(7, null);
227     checkNotNull();
228     varPojo2.acceptLoad(mv);
229     visitLineNumber(8, null);
230     Label notSameInstance = new Label();
231     mv.visitJumpInsn(IF_ACMPNE, notSameInstance);
232 
233     // same instance; return true
234     mv.visitInsn(ICONST_1);
235     mv.visitInsn(IRETURN);
236 
237     mv.visitLabel(notSameInstance);
238 
239     // if other is null, return false.
240     mv.visitFrame(F_FULL, 3, localVars, 0, NO_STACK);
241     visitLineNumber(9, null);
242     varPojo2.acceptLoad(mv);
243     mv.visitJumpInsn(IFNULL, returnFalse);
244 
245     // common case: if both types are the same, they are compatible for equality
246     varThis.acceptLoad(mv);
247     visitLineNumber(10, null);
248     invokeVirtual(Object.class, "getClass", Class.class);
249     varPojo1.acceptLoad(mv);
250     visitLineNumber(11, null);
251     invokeVirtual(Object.class, "getClass", Class.class);
252     mv.visitJumpInsn(IF_ACMPEQ, compatibleTypes);
253 
254     // types are not the same; check for compatibility
255     varThis.acceptLoad(mv);
256     varPojo2.acceptLoad(mv);
257     visitLineNumber(12, null);
258     invokeVirtual(Object.class, "getClass", Class.class);
259     visitLineNumber(13, null);
260     invokeVirtual(BasePojomator.class, "isCompatibleForEquality", boolean.class, Class.class);
261     mv.visitJumpInsn(IFEQ, returnFalse);
262 
263     // types are compatible, so start comparing properties
264     mv.visitLabel(compatibleTypes);
265     mv.visitFrame(F_FULL, 3, localVars, 0, NO_STACK);
266 
267     // Compare properties
268     for(PropertyElement propertyElement: classProperties.getEqualsProperties()) {
269       visitLineNumber(14, propertyElement);
270       visitAccessorAndConvert(varPojo1, propertyElement);
271       visitLineNumber(15, propertyElement);
272       visitAccessorAndConvert(varPojo2, propertyElement);
273       visitLineNumber(16, propertyElement);
274       compareProperties(mv, returnFalse, propertyElement, stackAdjustments);
275     }
276     // If we have gotten this far, all properties are equal, so return true.
277     mv.visitInsn(ICONST_1);
278     mv.visitInsn(IRETURN);
279 
280     mv.visitLabel(returnFalse);
281     mv.visitFrame(F_FULL, 3, localVars, 0, NO_STACK);
282     mv.visitInsn(ICONST_0);
283     mv.visitInsn(IRETURN);
284 
285     Label end = visitNewLabel();
286     varThis.withScope(start, end).acceptLocalVariable(mv);
287     varPojo1.withScope(start, end).acceptLocalVariable(mv);
288     varPojo2.withScope(start, end).acceptLocalVariable(mv);
289     mv.visitMaxs(2 + stackAdjustments.adjustments(2), 3);
290     mv.visitEnd();
291   }
292 
293   /**
294    * Compare a property from each pojo. It is assumed when this method is called that both property values have been
295    * loaded onto the stack. In the event the property value is a float or double, it is further assumed that it has
296    * been converted to an int or long.
297    * @param mv
298    * @param notEqualLabel where to jump if the property values are not equal
299    * @param propertyElement the property being compared
300    * @param stackAdjustments adjustments to be made to the max stack size, based on property type
301    */
302   private void compareProperties(
303       MethodVisitor mv, Label notEqualLabel, PropertyElement propertyElement, StackAdjustments stackAdjustments) {
304     Class<?> propertyType = propertyElement.getPropertyType();
305     if (propertyType.isPrimitive()) {
306       if (isWide(propertyElement)) {
307         stackAdjustments.wideProperty = true;
308         mv.visitInsn(LCMP);
309         mv.visitJumpInsn(IFNE, notEqualLabel);
310       }
311       else {
312         mv.visitJumpInsn(IF_ICMPNE, notEqualLabel);
313       }
314     }
315     else {
316       if(propertyType.isArray()) {
317         Class<?> componentType = propertyType.getComponentType();
318         if (componentType.isPrimitive()) {
319           visitLineNumber(17, propertyElement);
320           invokeStatic(Arrays.class, "equals",boolean.class, propertyType, propertyType);
321         }
322         else {
323           visitLineNumber(18, propertyElement);
324           invokeStatic(BasePojomator.class, "compareArrays", boolean.class, Object.class, Object.class);
325         }
326       }
327       else {
328         if (isObjectPossiblyHoldingArray(propertyElement)) {
329           visitLineNumber(19, propertyElement);
330           invokeStatic(BasePojomator.class, "areObjectValuesEqual", boolean.class, Object.class, Object.class);
331         }
332         else {
333           visitLineNumber(20, propertyElement);
334           invokeStatic(Objects.class,  "equals", boolean.class, Object.class, Object.class);
335         }
336       }
337       mv.visitJumpInsn(IFEQ, notEqualLabel);
338     }
339   }
340 
341   /**
342    * Generate the {@link Pojomator#doHashCode(Object)} method.
343    * @param cw
344    */
345   private void makeDoHashCode(ClassVisitor cw) {
346     LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
347     LocalVariable varPojo = new LocalVariable("pojo", pojoClass, pojoDescriptor, 1);
348 
349     int longOrDoubleStackAdjustment = 0;
350     Object[] localVars = new Object[] {pojomatorInternalClassName, OBJECT_INTERNAL_NAME};
351 
352     mv = cw.visitMethod(ACC_PUBLIC, "doHashCode", methodDesc(int.class, Object.class), null, null);
353     mv.visitCode();
354     Label start = visitNewLabel();
355     visitLineNumber(21, null);
356     varPojo.acceptLoad(mv);
357     visitLineNumber(22, null);
358     checkNotNullPop();
359 
360     //algorithm:
361     // hashCode(prop_n) + 31 * (hashCode(prop_n-1) + 31 * ( ... (hashCode(prop_1) + 31 * 1) ... ))
362 
363     mv.visitInsn(ICONST_1); // this will just be multiplied by 31; let the optimizer take care of it
364 
365     for(PropertyElement propertyElement: classProperties.getHashCodeProperties()) {
366       // multiply what we have so far by 31.
367       visitLineNumber(23, propertyElement);
368       mv.visitIntInsn(BIPUSH, 31);
369       visitLineNumber(24, propertyElement);
370       mv.visitInsn(IMUL);
371 
372       visitLineNumber(25, propertyElement);
373       visitAccessorAndConvert(varPojo, propertyElement); // grab the property value, converting a float or double
374       Class<?> propertyType = propertyElement.getPropertyType();
375       if (propertyType.isPrimitive()) {
376         // need to compute the hash code for this primitive value, based on its type
377         switch (propertyType.getName()) {
378           case "boolean":
379             visitLineNumber(26, propertyElement);
380             Label ifeq = new Label();
381             mv.visitJumpInsn(IFEQ, ifeq);
382             mv.visitIntInsn(SIPUSH, Boolean.TRUE.hashCode());
383             Label hashCodeDetermined = new Label();
384             mv.visitJumpInsn(GOTO, hashCodeDetermined);
385             mv.visitLabel(ifeq);
386             mv.visitFrame(F_FULL, 2, localVars, 1, new Object[] {INTEGER});
387             mv.visitIntInsn(SIPUSH, Boolean.FALSE.hashCode());
388             mv.visitLabel(hashCodeDetermined);
389             mv.visitFrame(F_FULL, 2, localVars, 2, new Object[] {INTEGER, INTEGER});
390             break;
391           case "byte":
392           case "char":
393           case "int":
394           case "short":
395           case "float":
396             break; // already an int (from the JVM's point of view)
397           case "double":
398           case "long":
399             longOrDoubleStackAdjustment = 3; // one extra for the field, two extra for the dup to do an xor
400 
401             // compute bits ^ (bits >> 32)
402 
403             visitLineNumber(27, propertyElement);
404             // we'll need a second copy to do the xor:
405             mv.visitInsn(DUP2);
406             // bitshift 32 right:
407             mv.visitIntInsn(BIPUSH, 32);
408             mv.visitInsn(LUSHR);
409             // xor with the original
410             mv.visitInsn(LXOR);
411             // chop of the high 32 bits
412             mv.visitInsn(L2I);
413             break;
414           default:
415             throw new IllegalStateException("unknown primitive type " + propertyType.getName());
416         }
417       }
418       else {
419         Label ifNonNull = new Label();
420         Label hashCodeDetermined = new Label();
421 
422         mv.visitInsn(DUP); // if it is non-null, let's not have to get it a second time.
423         mv.visitJumpInsn(IFNONNULL, ifNonNull);
424         // it's null
425         mv.visitInsn(POP); // won't need that duped copy after all
426         mv.visitInsn(ICONST_0);
427         mv.visitJumpInsn(GOTO, hashCodeDetermined);
428 
429         // it's not null
430         mv.visitLabel(ifNonNull);
431         mv.visitFrame(
432           F_FULL, 2, localVars, 2, new Object[] {INTEGER, Type.getInternalName(effectiveType(propertyType))});
433 
434         if(propertyType.isArray()) {
435           visitLineNumber(28, propertyElement);
436 
437           invokeStatic(
438             Arrays.class,
439             isDeepArray(propertyType) ? "deepHashCode" : "hashCode",
440               int.class,
441               propertyType.getComponentType().isPrimitive() ? propertyType : Object[].class);
442         }
443         else if (isObjectPossiblyHoldingArray(propertyElement)) {
444           // it *could* be an array; if so, we want to do an array hashCode.
445 
446           mv.visitInsn(DUP); // we'll still want the property value handy after calling getClass().isArray()
447           invokeVirtual(Object.class, "getClass", Class.class);
448           visitLineNumber(29, propertyElement);
449           invokeVirtual(Class.class, "isArray", boolean.class);
450           Label isArray = new Label();
451           mv.visitJumpInsn(IFNE, isArray); // if true
452 
453           // regular old hashCode
454           visitLineNumber(30, propertyElement);
455           invokeVirtual(Object.class, "hashCode", int.class);
456           mv.visitJumpInsn(GOTO, hashCodeDetermined);
457 
458           // add a deep parameter to arrayHashCode, like we did for compareProperties
459           mv.visitLabel(isArray);
460           mv.visitFrame(F_FULL, 2, localVars, 2, new Object[] { INTEGER, Type.getInternalName(propertyType) });
461 
462           mv.visitInsn(ICONST_1);
463           visitLineNumber(31, propertyElement);
464           invokeStatic(BasePojomator.class, "arrayHashCode", int.class, Object.class, boolean.class);
465         }
466         else {
467           visitLineNumber(32, propertyElement);
468           invokeVirtual(Object.class, "hashCode", int.class);
469         }
470 
471         mv.visitLabel(hashCodeDetermined);
472         mv.visitFrame(F_FULL, 2, localVars, 2, new Object[] {INTEGER, INTEGER});
473       }
474       // add result to what we have so far
475       mv.visitInsn(IADD);
476     }
477     mv.visitInsn(IRETURN);
478     Label end = visitNewLabel();
479     varThis.withScope(start, end).acceptLocalVariable(mv);
480     varPojo.withScope(start, end).acceptLocalVariable(mv);
481     mv.visitMaxs(3 + longOrDoubleStackAdjustment, 2);
482     mv.visitEnd();
483   }
484 
485   /**
486    * Generate {@link Pojomator#doToString(Object)}
487    * @param cw
488    */
489   private void makeDoToString(ClassVisitor cw) {
490     int longOrDoubleStackAdjustment = 1;
491     LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
492     LocalVariable varPojo = new LocalVariable("pojo", pojoClass, null, 1);
493     LocalVariable varPojoFormatter=
494       new LocalVariable("pojoFormattor", classDesc(EnhancedPojoFormatter.class), null, 2);
495     LocalVariable varBuilder= new LocalVariable("builder", classDesc(String.class), null, 3);
496 
497     mv = cw.visitMethod(ACC_PUBLIC, "doToString", methodDesc(String.class, Object.class), null, null);
498     mv.visitCode();
499     Label start = visitNewLabel();
500     varPojo.acceptLoad(mv);
501     checkNotNullPop();
502 
503     constructEnhancedPojoFormatter();
504     varPojoFormatter.acceptStore(mv);
505 
506     visitLineNumber(33, null);
507     mv.visitTypeInsn(NEW, internalName(StringBuilder.class));
508     mv.visitInsn(DUP);
509     construct(StringBuilder.class);
510     varBuilder.acceptStore(mv);
511 
512     varPojoFormatter.acceptLoad(mv);
513     varBuilder.acceptLoad(mv);
514     loadPojoClass(varThis);
515 
516     visitLineNumber(34, null);
517 
518     invokeInterface(EnhancedPojoFormatter.class,  "appendToStringPrefix", void.class, StringBuilder.class, Class.class);
519 
520     for(PropertyElement propertyElement: classProperties.getToStringProperties()) {
521       if (isWide(propertyElement)) {
522         longOrDoubleStackAdjustment = 1; // having any double-wide values on our stack increases max stack depth by one
523       }
524 
525       // append the property prefix
526       varPojoFormatter.acceptLoad(mv);
527       varBuilder.acceptLoad(mv);
528       visitLineNumber(35, propertyElement);
529       loadPropertyElementField(propertyElement);
530       visitLineNumber(36, propertyElement);
531       invokeInterface(
532         EnhancedPojoFormatter.class, "appendPropertyPrefix", void.class, StringBuilder.class, PropertyElement.class);
533 
534       // get the propertyFormatter for this property
535       visitLineNumber(37, propertyElement);
536       mv.visitFieldInsn(
537         GETSTATIC,
538         pojomatorInternalClassName,
539         propertyFormatterName(propertyElement),
540         classDesc(EnhancedPropertyFormatter.class));
541 
542       // The propertyFormatter will format the property value and append the results to our StringBuilder
543       varBuilder.acceptLoad(mv);
544       visitLineNumber(38, propertyElement);
545       visitAccessor(varPojo, propertyElement);
546       Class<?> appendType = appendFormattedType(propertyElement.getPropertyType());
547       if (isObjectPossiblyHoldingArray(propertyElement)) {
548         visitLineNumber(39, propertyElement);
549         invokeInterface(
550           EnhancedPropertyFormatter.class, "appendFormattedPossibleArray", void.class, StringBuilder.class, appendType);
551       }
552       else {
553         visitLineNumber(40, propertyElement);
554         invokeInterface(
555           EnhancedPropertyFormatter.class,  "appendFormatted", void.class, StringBuilder.class, appendType);
556       }
557 
558       // have any property suffix appended to the StringBuilder
559       varPojoFormatter.acceptLoad(mv);
560       varBuilder.acceptLoad(mv);
561       visitLineNumber(41, propertyElement);
562       loadPropertyElementField(propertyElement);
563       visitLineNumber(42, propertyElement);
564       invokeInterface(
565         EnhancedPojoFormatter.class,  "appendPropertySuffix", void.class, StringBuilder.class, PropertyElement.class);
566     }
567 
568     // Have any toString suffix appended
569     varPojoFormatter.acceptLoad(mv);
570     varBuilder.acceptLoad(mv);
571     loadPojoClass(varThis);
572     visitLineNumber(43, null);
573     invokeInterface(EnhancedPojoFormatter.class,  "appendToStringSuffix", void.class, StringBuilder.class, Class.class);
574 
575     // invoke toString and return the result
576     varBuilder.acceptLoad(mv);
577     visitLineNumber(44, null);
578     invokeVirtual(StringBuilder.class, "toString", String.class);
579     mv.visitInsn(ARETURN);
580 
581     Label end = visitNewLabel();
582     varThis.withScope(start, end).acceptLocalVariable(mv);
583     varPojo.withScope(start, end).acceptLocalVariable(mv);
584     varPojoFormatter.withScope(start, end).acceptLocalVariable(mv);
585     varBuilder.withScope(start, end).acceptLocalVariable(mv);
586     mv.visitMaxs(3 + longOrDoubleStackAdjustment, 4);
587     mv.visitEnd();
588   }
589 
590   private static Class<?> appendFormattedType(Class<?> propertyType) {
591     if (propertyType.isPrimitive()) {
592       return propertyType;
593     }
594     else if (propertyType.isArray()) {
595       return propertyType.getComponentType().isPrimitive() ? propertyType : Object[].class;
596     }
597     else {
598       return Object.class;
599     }
600   }
601 
602   private void loadPropertyElementField(PropertyElement propertyElement) {
603     mv.visitFieldInsn(
604       GETSTATIC,
605       pojomatorInternalClassName,
606       propertyElementName(propertyElement),
607       classDesc(PropertyElement.class));
608   }
609 
610   /**
611    * Construct the pojoFormatter to use. This method will contribute 2 or 4 to the max stack depth,
612    * depending on whether the pojoFormatter implements {@link EnhancedPojoFormatter} or not.
613    */
614   @SuppressWarnings("deprecation")
615   private void constructEnhancedPojoFormatter() {
616     PojoFormat format = pojoClass.getAnnotation(PojoFormat.class);
617     if (format == null) {
618       mv.visitTypeInsn(NEW, internalName(DefaultEnhancedPojoFormatter.class));
619       mv.visitInsn(DUP);
620       visitLineNumber(45, null);
621       construct(DefaultEnhancedPojoFormatter.class);
622     }
623     else {
624       Class<? extends org.pojomatic.formatter.PojoFormatter> pojoFormatterClass = format.value();
625       // if it isn't an enhanced formatter, we'll need to wrap it a EnhancedPojoFormatter. If we do this, we'll first
626       // invoke new on the wrapper, then construct the underlying formatter, then call the constructor on the wrapper.
627       boolean isEnhancedFormatter = EnhancedPojoFormatter.class.isAssignableFrom(pojoFormatterClass);
628       if (! isEnhancedFormatter) {
629         visitLineNumber(46, null);
630         mv.visitTypeInsn(NEW, ENHANCED_POJO_FORMATTER_WRAPPER_INTERNAL_NAME);
631         mv.visitInsn(DUP);
632       }
633       mv.visitTypeInsn(NEW, internalName(pojoFormatterClass));
634       mv.visitInsn(DUP);
635       visitLineNumber(47, null);
636       construct(pojoFormatterClass);
637       if (! isEnhancedFormatter) {
638         visitLineNumber(48, null);
639         construct(
640           org.pojomatic.internal.EnhancedPojoFormatterWrapper.class, org.pojomatic.formatter.PojoFormatter.class);
641       }
642     }
643   }
644 
645   /**
646    * Load a reference to the pojo class. We cannot refer to this directly, since the class may not be visible to us,
647    * so instead, we refer to the instance variable in {@link BasePojomator}.
648    * @param varThis the "this" local variable.
649    */
650   private void loadPojoClass(LocalVariable varThis) {
651     visitLineNumber(49, null);
652     varThis.acceptLoad(mv);
653     mv.visitFieldInsn(GETFIELD, BASE_POJOMATOR_INTERNAL_NAME, POJO_CLASS_FIELD_NAME, classDesc(Class.class));
654     //mv.visitFieldInsn(GETSTATIC, pojomatorInternalClassName, POJO_CLASS_FIELD_NAME, classDesc(Class.class));
655   }
656 
657   /**
658    * Generate {@link Pojomator#doDiff(Object, Object)}
659    * @param cw
660    */
661   private void makeDoDiff(ClassVisitor cw) {
662     LocalVariable varThis = new LocalVariable("this", pojomatorInternalClassDesc, null, 0);
663     LocalVariable varPojo1 = new LocalVariable("instance", pojoClass, pojoDescriptor, 1);
664     LocalVariable varPojo2 = new LocalVariable("other", pojoClass, pojoDescriptor, 2);
665     LocalVariable varDifferencesList = new LocalVariable(
666       "differences", List.class, "Ljava/util/List<Lorg/pojomatic/diff/Difference;>;", 3);
667 
668     StackAdjustments stackAdjustments = new StackAdjustments();
669     Object[] localVarTypes = new Object[] {
670       pojomatorInternalClassName, OBJECT_INTERNAL_NAME, OBJECT_INTERNAL_NAME, internalName(List.class), null, null };
671 
672     mv = cw.visitMethod(ACC_PUBLIC, "doDiff", methodDesc(Differences.class, Object.class, Object.class), null, null);
673     mv.visitCode();
674     Label start = visitNewLabel();
675     varPojo1.acceptLoad(mv);
676     visitLineNumber(50, null);
677     checkNotNull("instance is null");
678     varPojo2.acceptLoad(mv);
679     checkNotNull("other is null");
680 
681     // If instance and other are the same object, then return NoDifferences.getInstance();
682     Label notSameInstance = new Label();
683     mv.visitJumpInsn(IF_ACMPNE, notSameInstance);
684     invokeStatic(NoDifferences.class, "getInstance", NoDifferences.class);
685     mv.visitInsn(ARETURN);
686 
687     // not the same instance, some work to do
688     mv.visitLabel(notSameInstance);
689     mv.visitFrame(F_FULL, 3, localVarTypes, 0, NO_STACK);
690     visitLineNumber(51, null);
691     checkCompatibleForEquality(varThis, varPojo1, "instance");
692     visitLineNumber(52, null);
693     checkCompatibleForEquality(varThis, varPojo2, "other");
694 
695     Label makeDiferences = notSameInstance;
696     mv.visitTypeInsn(NEW, "java/util/ArrayList");
697     mv.visitInsn(DUP);
698     construct(ArrayList.class);
699     varDifferencesList.acceptStore(mv);
700 
701     List<LocalVariable> propertyVariables = new ArrayList<>(); // these will occur in a block scope
702     // compare properties
703     for(PropertyElement propertyElement: classProperties.getHashCodeProperties()) {
704       int width = isWide(propertyElement) ? 2 : 1;
705       Class<?> propertyType = propertyElement.getPropertyType();
706       LocalVariable varProp1 = new LocalVariable(
707         "property_" + propertyElement.getName() + "_1", propertyType, null, 4);
708       //If the type is long or double, we need to store the next var at 6, not 5.
709       LocalVariable varProp2 = new LocalVariable(
710         "property_" + propertyElement.getName() + "_2", propertyType, null, 4 + width);
711       propertyVariables.add(varProp1);
712       propertyVariables.add(varProp2);
713 
714       Label blockStart = visitNewLabel();
715 
716       visitLineNumber(53, propertyElement);
717       visitAccessor(varPojo1, propertyElement);
718       varProp1.acceptStore(mv);
719       visitLineNumber(54, propertyElement);
720       visitAccessor(varPojo2, propertyElement);
721       varProp2.acceptStore(mv);
722 
723       visitLineNumber(55, propertyElement);
724       visitAccessorAndConvert(varPojo1, propertyElement);
725       visitLineNumber(56, propertyElement);
726       visitAccessorAndConvert(varPojo2, propertyElement);
727 
728       Label propertiesNotEqual = new Label();
729       Label next = new Label();
730       visitLineNumber(57, propertyElement);
731       compareProperties(mv, propertiesNotEqual, propertyElement, stackAdjustments);
732       mv.visitJumpInsn(GOTO, next); // there were no differences.
733 
734       mv.visitLabel(propertiesNotEqual);
735 
736       localVarTypes[5] = localVarTypes[4] = propertyType.isPrimitive()
737         ? Primitives.getOpcode(propertyType)
738         : internalName(effectiveType(propertyType));
739       mv.visitFrame(F_FULL, 6, localVarTypes, 0, NO_STACK);
740 
741       // Create a ValueDifference instance, initialized with the property name and the two values, and add it to our list
742       varDifferencesList.acceptLoad(mv); // we'll need this to add to the list
743       mv.visitTypeInsn(NEW, "org/pojomatic/diff/ValueDifference");
744       mv.visitInsn(DUP);
745       mv.visitLdcInsn(propertyElement.getName());
746       varProp1.acceptLoad(mv);
747       visitLineNumber(58, propertyElement);
748       convertToObject(propertyType);
749       varProp2.acceptLoad(mv);
750       visitLineNumber(59, propertyElement);
751       convertToObject(propertyType);
752       visitLineNumber(60, propertyElement);
753       construct(ValueDifference.class, String.class, Object.class, Object.class);
754 
755       // add the ValueDifference instance to our list
756       visitLineNumber(61, propertyElement);
757       invokeInterface(List.class, "add", boolean.class, Object.class);
758       mv.visitInsn(POP); // ignore the return value of List#add
759       mv.visitLabel(next);
760       mv.visitFrame(F_FULL, 4, localVarTypes, 0, NO_STACK);
761 
762       varProp1.withScope(blockStart, next);
763       varProp2.withScope(blockStart, next);
764     }
765 
766     // if our list is empty, return the NoDifferences instance
767     varDifferencesList.acceptLoad(mv);
768     visitLineNumber(62, null);
769     invokeInterface(List.class, "isEmpty", boolean.class);
770     Label hasDifferences = new Label();
771     mv.visitJumpInsn(IFEQ, hasDifferences);
772     visitLineNumber(63, null);
773     invokeStatic(NoDifferences.class, "getInstance", NoDifferences.class);
774     mv.visitInsn(ARETURN);
775 
776     // our list is not empty, so wrap it in a PropertyDiferences instance
777     mv.visitLabel(hasDifferences);
778     mv.visitFrame(F_FULL, 4, localVarTypes, 0, NO_STACK);
779 
780     visitLineNumber(64, null);
781     mv.visitTypeInsn(NEW, internalName(PropertyDifferences.class));
782     mv.visitInsn(DUP);
783     varDifferencesList.acceptLoad(mv);
784     visitLineNumber(65, null);
785     construct(PropertyDifferences.class, List.class);
786     mv.visitInsn(ARETURN);
787 
788     Label end = visitNewLabel();
789 
790     varThis.withScope(start, end).acceptLocalVariable(mv);
791     varPojo1.withScope(start, end).acceptLocalVariable(mv);
792     varPojo2.withScope(start, end).acceptLocalVariable(mv);
793     varDifferencesList.withScope(makeDiferences, end).acceptLocalVariable(mv);
794     for (LocalVariable var: propertyVariables) {
795       var.acceptLocalVariable(mv);
796     }
797     mv.visitMaxs(6 + stackAdjustments.adjustments(2), 6 + stackAdjustments.adjustments(2));
798     mv.visitEnd();
799   }
800 
801   /**
802    * Invoke {@link BasePojomator#checkCompatibleForEquality(Object, String)} on the specified variable
803    * @param message the message to include in the {@link IllegalArgumentException} if the variable fails the
804    * class test
805    * @param varNumber the variable number to check
806    */
807   private void checkCompatibleForEquality(LocalVariable varThis, LocalVariable var, String message) {
808     varThis.acceptLoad(mv);
809     var.acceptLoad(mv);
810     mv.visitLdcInsn(message);
811     visitLineNumber(66, null);
812     invokeVirtual(BasePojomator.class, "checkCompatibleForEquality", void.class, Object.class, String.class);
813   }
814 
815   /**
816    * Determine if the given propertyElement of array type should be treated as possibly containing a multi-level array.
817    * This will be the case if it is:
818    * <ul>
819    *   <li>of type Object and is not annotated with @{@link SkipArrayCheck}</li>
820    *   <li>of type Object[]</li>
821    *   <li>of array type with a component type of array type</li>
822    * </ul>
823    * @param propertyElement
824    * @return {@code true} if the given propertyElement should be treated as possibly containing a multi-level array,
825    * or {@code false} otherwise.
826    */
827   private boolean isDeepArray(Class<?> propertyType) {
828     return propertyType.equals(Object[].class) || propertyType.getComponentType().isArray();
829   }
830 
831   /**
832    * Determine if the given propertyElement should be treated as one that could be an array.
833    * @param propertyElement
834    * @return {@code true} if the given propertyElement is either of array type, or is of type Object and not annotated
835    * with @{@link SkipArrayCheck}
836    */
837   private boolean isObjectPossiblyHoldingArray(PropertyElement propertyElement) {
838     return Object.class.equals(propertyElement.getPropertyType())
839           && ! propertyElement.getElement().isAnnotationPresent(SkipArrayCheck.class);
840   }
841 
842   /**
843    * If the parameter on the stack (of type propertyType) is primitive, convert it to the appropriate wrapper object.
844    * Otherwise, leave it alone
845    * @param propertyType the type of the parameter on the stack
846    */
847   private void convertToObject(Class<?> propertyType) {
848     if (propertyType.isPrimitive()) {
849       Class<?> wrapperClass = Primitives.getWrapperClass(propertyType);
850       invokeStatic(wrapperClass, "valueOf", wrapperClass, propertyType);
851     }
852   }
853 
854   /**
855    * Visit an accessor, converting floats or doubles to int bits or long bits respectively.
856    * @param propertyElement the property to access
857    * @param variableNumber the index of the local variable holding a the pojo instance to access
858    */
859   private void visitAccessorAndConvert(LocalVariable var, PropertyElement propertyElement) {
860     visitAccessor(var, propertyElement);
861     if (propertyElement.getPropertyType().equals(float.class)) {
862       invokeStatic(Float.class, "floatToIntBits", int.class, float.class);
863     }
864     else if (propertyElement.getPropertyType().equals(double.class)) {
865       invokeStatic(Double.class, "doubleToLongBits", long.class, double.class);
866     }
867   }
868 
869   /**
870    * Visit an accessor
871    * @param var the index of the local variable holding a the pojo instance to access
872    * @param propertyElement the property to access
873    */
874   private void visitAccessor(LocalVariable var, PropertyElement propertyElement) {
875     var.acceptLoad(mv);
876     mv.visitMethodInsn(
877       INVOKESTATIC, pojomatorInternalClassName, propertyAccessorName(propertyElement),
878       accessorMethodDescription(propertyElement), false);
879   }
880 
881   private String accessorMethodDescription(PropertyElement propertyElement) {
882     return methodDesc(effectiveType(propertyElement.getPropertyType()), Object.class);
883   }
884 
885   /**
886    * Determine what, for our purposes, is the effective type of a property. Since a property type may be a class which
887    * is not visible to us, we'll treat any type which is neither primitive nor an array to be of type Object.
888    * A primitive type or array of primitives will be returned as is. All other array types will be returned as Object[].
889    * @param propertyClass the class to determine the effective type for.
890    * @return the effective type of {@code propertyClass}
891    */
892   private Class<?> effectiveType(Class<?> propertyClass) {
893     if (propertyClass.isArray()) {
894       return propertyClass.getComponentType().isPrimitive() ? propertyClass : Object[].class;
895     }
896     else {
897       return propertyClass.isPrimitive() ? propertyClass : Object.class;
898     }
899   }
900 
901   /**
902    * Create a new label and visit it.
903    * @return the new label
904    */
905   private Label visitNewLabel() {
906     Label label = new Label();
907     mv.visitLabel(label);
908     return label;
909   }
910 
911   /**
912    * Visit a line number, based on a provided number, and a propertyElement (possibly null). The propertyElement will
913    * be used to distinguish line numbers generated from the same place in the code of this class, but for different
914    * properties.
915    * </p>
916    * To ensure unique line numbers, run the following:
917    * <code>
918    *   perl -pi -e 'if (/visitLineNumber\(/) { $i++; s/visitLineNumber\(mv, \d+/visitLineNumber\(mv, $i/; }' \
919    *     src/main/java/org/pojomatic/internal/PojomatorByteCodeGenerator.java
920    * </code>
921    * @param lineNumberBase
922    * @param propertyElement
923    */
924   private void visitLineNumber(int lineNumberBase, PropertyElement propertyElement) {
925     Integer offset = propertyNumbers.get(propertyElement);
926     mv.visitLineNumber(lineNumberBase + 100 * (offset == null ? 0 : offset), visitNewLabel());
927   }
928 
929   /**
930    * Determine if the type of a property is "wide" - i.e. is a long or double.
931    * @param propertyElement
932    * @return
933    */
934   private static boolean isWide(PropertyElement propertyElement) {
935     Class<?> type = propertyElement.getPropertyType();
936     return type == long.class || type == double.class;
937   }
938 
939   /**
940    * Pop the top element off of the stack and invoke {@link BasePojomator#checkNotNull(Object)} on it.
941    */
942   private void checkNotNullPop() {
943     invokeStatic(BasePojomator.class, "checkNotNullPop", void.class, Object.class);
944   }
945 
946   /**
947    * Invoke {@link BasePojomator#checkNotNull(Object)} on the top element of the stack, leaving that element there.
948    */
949   private void checkNotNull() {
950     invokeStatic(BasePojomator.class, "checkNotNull", Object.class, Object.class);
951   }
952 
953   /**
954    * Invoke {@link BasePojomator#checkNotNull(Object, String)} on the top element of the stack, leaving that element there.
955    * @param message the message to include in the {@link NullPointerException} if the top element is null
956    */
957   private void checkNotNull(String message) {
958     mv.visitLdcInsn(message);
959     invokeStatic(BasePojomator.class, "checkNotNull", Object.class, Object.class, String.class);
960   }
961 
962   private void invokeStatic(Class<?> ownerClass, String methodName, Class<?> returnType, Class<?>... parameterTypes) {
963     mv.visitMethodInsn(
964       INVOKESTATIC, internalName(ownerClass), methodName, methodDesc(returnType, parameterTypes), false);
965   }
966 
967   private void invokeInterface(
968     Class<?> ownerClass, String methodName, Class<?> returnType, Class<?>... parameterTypes) {
969     mv.visitMethodInsn(
970       INVOKEINTERFACE, internalName(ownerClass), methodName, methodDesc(returnType, parameterTypes), true);
971   }
972 
973   private void invokeVirtual(
974     Class<?> ownerClass, String methodName, Class<?> returnType, Class<?>... parameterTypes) {
975     mv.visitMethodInsn(
976       INVOKEVIRTUAL, internalName(ownerClass), methodName, methodDesc(returnType, parameterTypes), false);
977   }
978 
979   private void construct(
980     Class<?> ownerClass, Class<?>... parameterTypes) {
981     mv.visitMethodInsn(
982       INVOKESPECIAL, internalName(ownerClass), "<init>", methodDesc(void.class, parameterTypes), false);
983   }
984 
985   private static String internalName(Class<?> clazz) {
986     return internalName(clazz.getName());
987   }
988 
989   private static String internalName(String className) {
990     return className.replace('.', '/');
991   }
992 
993   private static String classDesc(Class<?> clazz) {
994     return Type.getDescriptor(clazz);
995   }
996 
997   private static String methodDesc(Class<?> returnType, Class<?>... parameterTypes) {
998     return MethodType.methodType(returnType, parameterTypes).toMethodDescriptorString();
999   }
1000 
1001   private static String propertyAccessorName(PropertyElement property) {
1002     return "get_" + qualifiedPropertyName(property);
1003   }
1004 
1005   static String propertyElementName(PropertyElement property) {
1006     return "element_" + qualifiedPropertyName(property);
1007   }
1008 
1009   static String propertyFormatterName(PropertyElement property) {
1010     return "formatter_" + qualifiedPropertyName(property);
1011   }
1012 
1013   private static String qualifiedPropertyName(PropertyElement property) {
1014     return property.getType()
1015       + "_" + property.getDeclaringClass().getName().replace('.', '$')
1016       + "_" + property.getElementName();
1017   }
1018 }