001package io.ebean.enhance.entity;
002
003import io.ebean.enhance.asm.ClassVisitor;
004import io.ebean.enhance.asm.Label;
005import io.ebean.enhance.asm.MethodVisitor;
006import io.ebean.enhance.asm.Opcodes;
007import io.ebean.enhance.asm.Type;
008import io.ebean.enhance.common.ClassMeta;
009import io.ebean.enhance.common.EnhanceConstants;
010import io.ebean.enhance.common.VisitUtil;
011
012import java.util.HashSet;
013
014/**
015 * Holds meta data for a field.
016 * <p>
017 * This can then generate the appropriate byte code for this field.
018 * </p>
019 */
020public class FieldMeta implements Opcodes, EnhanceConstants {
021
022  private final ClassMeta classMeta;
023  private final String fieldClass;
024  private final String fieldName;
025  private final String fieldDesc;
026
027  private final HashSet<String> annotations = new HashSet<>();
028
029  private final Type asmType;
030
031  private final boolean primitiveType;
032  private final boolean objectType;
033
034  private final String getMethodName;
035  private final String getMethodDesc;
036  private final String setMethodName;
037  private final String setMethodDesc;
038  private final String getNoInterceptMethodName;
039  private final String setNoInterceptMethodName;
040
041  private int indexPosition;
042
043  /**
044   * Construct based on field name and desc from reading byte code.
045   * <p>
046   * Used for reading local fields (not inherited) via visiting the class bytes.
047   * </p>
048   */
049  public FieldMeta(ClassMeta classMeta, String name, String desc, String fieldClass) {
050    
051    this.classMeta = classMeta;
052    this.fieldName = name;
053    this.fieldDesc = desc;
054    this.fieldClass = fieldClass;
055    this.asmType = Type.getType(desc);
056
057    int sort = asmType.getSort();
058    this.primitiveType = sort > Type.VOID && sort <= Type.DOUBLE;
059    this.objectType = sort == Type.OBJECT;
060
061    this.getMethodDesc = "()" + desc;
062    this.setMethodDesc = "(" + desc + ")V";
063    
064    this.getMethodName = "_ebean_get_" + name;
065    this.setMethodName = "_ebean_set_" + name;
066
067    this.getNoInterceptMethodName = "_ebean_getni_" + name;
068    this.setNoInterceptMethodName = "_ebean_setni_" + name;
069  }
070
071  public void setIndexPosition(int indexPosition) {
072    this.indexPosition = indexPosition;
073  }
074
075  public String toString() {
076    return fieldName;
077  }
078
079  /**
080   * Return the field name.
081   */
082  public String getFieldName() {
083    return fieldName;
084  }
085
086  /**
087   * Return true if this is a primitiveType.
088   */
089  public boolean isPrimitiveType() {
090    return primitiveType;
091  }
092
093  /**
094   * Add a field annotation.
095   */
096  protected void addAnnotationDesc(String desc) {
097    annotations.add(desc);
098  }
099
100  /**
101   * Return the field name.
102   */
103  public String getName() {
104    return fieldName;
105  }
106
107  /**
108   * Return the field bytecode type description.
109   */
110  public String getDesc() {
111    return fieldDesc;
112  }
113
114  private boolean isInterceptGet() {
115    if (isId()) {
116      return false;
117    }
118    if (isTransient()) {
119      return false;
120    }
121    if (isMany()) {
122      return true;
123    }
124    return true;
125  }
126
127  private boolean isInterceptSet() {
128    return !isId() && !isTransient() && !isMany();
129  }
130
131  /**
132   * Return true if this field type is an Array of Objects.
133   * <p>
134   * We can not support Object Arrays for field types.
135   * </p>
136   */
137  public boolean isObjectArray() {
138    if (fieldDesc.charAt(0) == '[') {
139      if (fieldDesc.length() > 2) {
140        if (!isTransient()) {
141          System.err.println("ERROR: We can not support Object Arrays... for field: " + fieldName);
142        }
143        return true;
144      }
145    }
146    return false;
147  }
148
149  /**
150   * Return true is this is a persistent field.
151   */
152  public boolean isPersistent() {
153    return !isTransient();
154  }
155
156  /**
157   * Return true if this is a transient field.
158   */
159  public boolean isTransient() {
160    return annotations.contains("Ljavax/persistence/Transient;")
161        || annotations.contains(L_DRAFT);
162  }
163
164  /**
165   * Return true if this is an ID field.
166   * <p>
167   * ID fields are used in generating equals() logic based on identity.
168   * </p>
169   */
170  public boolean isId() {
171    return (annotations.contains("Ljavax/persistence/Id;")
172            || annotations.contains("Ljavax/persistence/EmbeddedId;"));
173  }
174
175  /**
176   * Return true if this is a OneToMany or ManyToMany field.
177   */
178  public boolean isMany() {
179    return annotations.contains("Ljavax/persistence/OneToMany;")
180        || annotations.contains("Ljavax/persistence/ManyToMany;");
181  }
182
183  public boolean isManyToMany() {
184    return annotations.contains("Ljavax/persistence/ManyToMany;");
185  }
186
187  /**
188   * Return true if this is an Embedded field.
189   */
190  public boolean isEmbedded() {
191    return annotations.contains("Ljavax/persistence/Embedded;");
192  }
193
194  /**
195   * Return true if the field is local to this class. Returns false if the field
196   * is actually on a super class.
197   */
198  public boolean isLocalField(ClassMeta classMeta) {
199    return fieldClass.equals(classMeta.getClassName());
200  }
201
202  /**
203   * Append byte code to return the Id value (for primitives).
204   */
205  public void appendGetPrimitiveIdValue(MethodVisitor mv, ClassMeta classMeta) {
206    mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getMethodName, getMethodDesc, false);
207  }
208
209  /**
210   * Append compare instructions if its a long, float or double.
211   */
212  public void appendCompare(MethodVisitor mv, ClassMeta classMeta) {
213    if (primitiveType) {
214      if (classMeta.isLog(4)) {
215        classMeta.log(" ... getIdentity compare primitive field[" + fieldName + "] type[" + fieldDesc + "]");
216      }
217      if (fieldDesc.equals("J")) {
218        // long compare to 0
219        mv.visitInsn(LCONST_0);
220        mv.visitInsn(LCMP);
221
222      } else if (fieldDesc.equals("D")) {
223        // double compare to 0
224        mv.visitInsn(DCONST_0);
225        mv.visitInsn(DCMPL);
226
227      } else if (fieldDesc.equals("F")) {
228        // float compare to 0
229        mv.visitInsn(FCONST_0);
230        mv.visitInsn(FCMPL);
231
232      }
233      // no extra instructions required for
234      // int, short, byte, char
235    }
236  }
237
238  /**
239   * Append code to get the Object value of a primitive.
240   * <p>
241   * This becomes a Integer.valueOf(someInt); or similar.
242   * </p>
243   */
244  public void appendValueOf(MethodVisitor mv) {
245    if (primitiveType) {
246      // use valueOf methods to return primitives as objects
247      Type objectWrapperType = PrimitiveHelper.getObjectWrapper(asmType);
248
249      String objDesc = objectWrapperType.getInternalName();
250      String primDesc = asmType.getDescriptor();
251
252      mv.visitMethodInsn(Opcodes.INVOKESTATIC, objDesc, "valueOf", "(" + primDesc + ")L" + objDesc + ";", false);
253    }
254  }
255
256  /**
257   * As part of the switch statement to read the fields generate the get code.
258   */
259  public void appendSwitchGet(MethodVisitor mv, ClassMeta classMeta, boolean intercept) {
260
261    if (intercept) {
262      // use the special get method with interception...
263      mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getMethodName, getMethodDesc, false);
264    } else {
265      if (isLocalField(classMeta)) {
266        mv.visitFieldInsn(GETFIELD, classMeta.getClassName(), fieldName, fieldDesc);
267      } else {
268        // field is on a superclass... so use virtual getNoInterceptMethodName
269        mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getNoInterceptMethodName, getMethodDesc, false);
270      }
271    }
272
273    if (primitiveType) {
274      appendValueOf(mv);
275    }
276  }
277
278  public void appendSwitchSet(MethodVisitor mv, ClassMeta classMeta, boolean intercept) {
279
280    if (primitiveType) {
281      // convert Object to primitive first...
282      Type objectWrapperType = PrimitiveHelper.getObjectWrapper(asmType);
283
284      String primDesc = asmType.getDescriptor();
285      String primType = asmType.getClassName();
286      String objInt = objectWrapperType.getInternalName();
287      mv.visitTypeInsn(CHECKCAST, objInt);
288
289      mv.visitMethodInsn(INVOKEVIRTUAL, objInt, primType + "Value", "()" + primDesc, false);
290    } else {
291      // check correct object type
292      mv.visitTypeInsn(CHECKCAST, asmType.getInternalName());
293    }
294
295    if (intercept) {
296      // go through the set method to check for interception...
297      mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), setMethodName, setMethodDesc, false);
298
299    } else {
300      mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), setNoInterceptMethodName, setMethodDesc, false);
301    }
302  }
303
304
305  /**
306   * Add get and set methods for field access/interception.
307   */
308  public void addGetSetMethods(ClassVisitor cv, ClassMeta classMeta) {
309
310    if (!isLocalField(classMeta)) {
311      String msg = "ERROR: " + fieldClass + " != " + classMeta.getClassName() + " for field "
312          + fieldName + " " + fieldDesc;
313      throw new RuntimeException(msg);
314    }
315    // add intercepting methods that are used to replace the
316    // standard GETFIELD PUTFIELD byte codes for field access
317    addGet(cv, classMeta);
318    addSet(cv, classMeta);
319
320    // add non-interception methods... so that we can get access
321    // to private fields on super classes
322    addGetNoIntercept(cv, classMeta);
323    addSetNoIntercept(cv, classMeta);
324  }
325
326  private String getEbeanCollectionClass() {
327    if (fieldDesc.equals("Ljava/util/List;")) {
328      return BEANLIST;
329    }
330    if (fieldDesc.equals("Ljava/util/Set;")) {
331      return BEANSET;
332    }
333    if (fieldDesc.equals("Ljava/util/Map;")) {
334      return BEANMAP;
335    }
336    return null;
337  }
338
339  /**
340   * Return true if null check should be added to this many field.
341   */
342  private boolean isInterceptMany() {
343
344    if (isMany() && !isTransient()) {
345
346      String ebCollection = getEbeanCollectionClass();
347      if (ebCollection != null) {
348        return true;
349      } else {
350        classMeta.log("Error unexpected many type " + fieldDesc);
351      }
352    }
353    return false;
354  }
355
356  /**
357   * Add a get field method with interception.
358   */
359  private void addGet(ClassVisitor cw, ClassMeta classMeta) {
360
361    if (classMeta.isLog(3)) {
362      classMeta.log(getMethodName + " " + getMethodDesc + " intercept:" + isInterceptGet() + " " + annotations);
363    }
364
365    MethodVisitor mv = cw.visitMethod(ACC_PROTECTED, getMethodName, getMethodDesc, null, null);
366    mv.visitCode();
367
368    if (isInterceptMany()) {
369      addGetForMany(mv);
370      return;
371    }
372
373    // ARETURN or IRETURN
374    int iReturnOpcode = asmType.getOpcode(Opcodes.IRETURN);
375
376    String className = classMeta.getClassName();
377
378    Label labelEnd = new Label();
379    Label labelStart = null;
380
381    int maxVars = 1;
382    if (isId()) {
383      labelStart = new Label();
384      mv.visitLabel(labelStart);
385      mv.visitLineNumber(5, labelStart);
386      mv.visitVarInsn(ALOAD, 0);
387      mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT);
388      mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetId", "()V", false);
389
390    } else if (isInterceptGet()) {
391      maxVars = 2;
392      labelStart = new Label();
393      mv.visitLabel(labelStart);
394      mv.visitLineNumber(6, labelStart);
395      mv.visitVarInsn(ALOAD, 0);
396      mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT);
397      VisitUtil.visitIntInsn(mv, indexPosition);
398      mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetter", "(I)V", false);
399    }
400    if (labelStart == null) {
401      labelStart = labelEnd;
402    }
403    mv.visitLabel(labelEnd);
404    mv.visitLineNumber(7, labelEnd);
405    mv.visitVarInsn(ALOAD, 0);
406    mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc);
407    mv.visitInsn(iReturnOpcode);// ARETURN or IRETURN
408    Label labelEnd1 = new Label();
409    mv.visitLabel(labelEnd1);
410    mv.visitLocalVariable("this", "L" + className + ";", null, labelStart, labelEnd1, 0);
411    mv.visitMaxs(maxVars, 1);
412    mv.visitEnd();
413  }
414
415  private void addGetForMany(MethodVisitor mv) {
416
417    String className = classMeta.getClassName();
418    String ebCollection = getEbeanCollectionClass();
419
420    Label l0 = new Label();
421    mv.visitLabel(l0);
422    mv.visitLineNumber(1, l0);
423    mv.visitVarInsn(ALOAD, 0);
424    mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT);
425    VisitUtil.visitIntInsn(mv, indexPosition);
426    mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetter", "(I)V", false);
427
428    Label l4 = new Label();
429    if (classMeta.getEnhanceContext().isCheckNullManyFields()) {
430
431      if (classMeta.isLog(3)) {
432        classMeta.log("... add Many null check on " + fieldName + " ebtype:" + ebCollection);
433      }
434
435      Label l3 = new Label();
436      mv.visitLabel(l3);
437      mv.visitLineNumber(2, l3);
438      mv.visitVarInsn(ALOAD, 0);
439      mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc);
440
441      mv.visitJumpInsn(IFNONNULL, l4);
442      Label l5 = new Label();
443      mv.visitLabel(l5);
444      mv.visitLineNumber(3, l5);
445      mv.visitVarInsn(ALOAD, 0);
446      mv.visitTypeInsn(NEW, ebCollection);
447      mv.visitInsn(DUP);
448      mv.visitMethodInsn(INVOKESPECIAL, ebCollection, "<init>", "()V", false);
449      mv.visitFieldInsn(PUTFIELD, className, fieldName, fieldDesc);
450
451      mv.visitVarInsn(ALOAD, 0);
452      mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT);
453      VisitUtil.visitIntInsn(mv, indexPosition);
454      mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "initialisedMany", "(I)V", false);
455
456      if (isManyToMany()) {
457        // turn on modify listening for ManyToMany
458        if (classMeta.isLog(3)) {
459          classMeta.log("... add ManyToMany modify listening to " + fieldName);
460        }
461
462        Label l6 = new Label();
463        mv.visitLabel(l6);
464        mv.visitLineNumber(4, l6);
465        mv.visitVarInsn(ALOAD, 0);
466        mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc);
467        mv.visitTypeInsn(CHECKCAST, C_BEANCOLLECTION);
468        mv.visitFieldInsn(GETSTATIC, C_BEANCOLLECTION + "$ModifyListenMode", "ALL", "L" + C_BEANCOLLECTION + "$ModifyListenMode;");
469        mv.visitMethodInsn(INVOKEINTERFACE, C_BEANCOLLECTION, "setModifyListening", "(L" + C_BEANCOLLECTION + "$ModifyListenMode;)V", true);
470      }
471    }
472
473
474    mv.visitLabel(l4);
475    mv.visitLineNumber(5, l4);
476    mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
477    mv.visitVarInsn(ALOAD, 0);
478    mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc);
479    mv.visitInsn(ARETURN);
480    Label l7 = new Label();
481    mv.visitLabel(l7);
482    mv.visitLocalVariable("this", "L" + className + ";", null, l0, l7, 0);
483    mv.visitMaxs(3, 1);
484    mv.visitEnd();
485  }
486
487  /**
488   * This is a get method with no interception.
489   * <p>
490   * It exists to be able to read private fields that are on super classes.
491   * </p>
492   */
493  private void addGetNoIntercept(ClassVisitor cw, ClassMeta classMeta) {
494
495    // ARETURN or IRETURN
496    int iReturnOpcode = asmType.getOpcode(Opcodes.IRETURN);
497
498    if (classMeta.isLog(3)) {
499      classMeta.log(getNoInterceptMethodName + " " + getMethodDesc);
500    }
501
502    MethodVisitor mv = cw.visitMethod(ACC_PROTECTED, getNoInterceptMethodName, getMethodDesc, null, null);
503    mv.visitCode();
504
505    Label l0 = new Label();
506    mv.visitLabel(l0);
507    mv.visitLineNumber(1, l0);
508    mv.visitVarInsn(ALOAD, 0);
509    mv.visitFieldInsn(GETFIELD, fieldClass, fieldName, fieldDesc);
510    mv.visitInsn(iReturnOpcode);// ARETURN or IRETURN
511    Label l2 = new Label();
512    mv.visitLabel(l2);
513    mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l2, 0);
514    mv.visitMaxs(2, 1);
515    mv.visitEnd();
516  }
517
518  /**
519   * Setter method with interception.
520   * 
521   * <pre>
522   * public void _ebean_set_propname(String newValue) {
523   *   ebi.preSetter(true, propertyIndex, _ebean_get_propname(), newValue);
524   *   this.propname = newValue;
525   * }
526   * </pre>
527   */
528  private void addSet(ClassVisitor cw, ClassMeta classMeta) {
529
530    String preSetterArgTypes = "Ljava/lang/Object;Ljava/lang/Object;";
531    if (!objectType) {
532      // preSetter method overloaded for primitive type comparison
533      preSetterArgTypes = fieldDesc + fieldDesc;
534    }
535
536    // ALOAD or ILOAD etc
537    int iLoadOpcode = asmType.getOpcode(Opcodes.ILOAD);
538
539    // double and long have a size of 2
540    int iPosition = asmType.getSize();
541
542    if (classMeta.isLog(3)) {
543      classMeta.log(setMethodName + " " + setMethodDesc + " intercept:" + isInterceptSet()
544          + " opCode:" + iLoadOpcode + "," + iPosition + " preSetterArgTypes" + preSetterArgTypes);
545    }
546
547    MethodVisitor mv = cw.visitMethod(ACC_PROTECTED, setMethodName, setMethodDesc, null, null);
548    mv.visitCode();
549
550    Label l0 = new Label();
551    mv.visitLabel(l0);
552    mv.visitLineNumber(1, l0);
553    mv.visitVarInsn(ALOAD, 0);
554    mv.visitFieldInsn(GETFIELD, fieldClass, INTERCEPT_FIELD, L_INTERCEPT);
555    if (isInterceptSet()) {
556      mv.visitInsn(ICONST_1);
557    } else {
558      // id or OneToMany field etc
559      mv.visitInsn(ICONST_0);
560    }
561    VisitUtil.visitIntInsn(mv, indexPosition);
562    mv.visitVarInsn(ALOAD, 0);
563    if (isId()) {
564      // skip getter on Id as we now intercept that via preGetId() for automatic jdbc batch flushing
565      mv.visitFieldInsn(GETFIELD, fieldClass, fieldName, fieldDesc);
566    } else {
567      mv.visitMethodInsn(INVOKEVIRTUAL, fieldClass, getMethodName, getMethodDesc, false);
568    }
569    mv.visitVarInsn(iLoadOpcode, 1);
570    String preSetterMethod = "preSetter";
571    if (isMany()) {
572      preSetterMethod = "preSetterMany";
573    }
574    mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, preSetterMethod, "(ZI"+ preSetterArgTypes + ")V", false);
575    Label l1 = new Label();
576    mv.visitLabel(l1);
577    mv.visitLineNumber(2, l1);
578    mv.visitVarInsn(ALOAD, 0);
579    mv.visitVarInsn(iLoadOpcode, 1);// ALOAD or ILOAD
580    mv.visitFieldInsn(PUTFIELD, fieldClass, fieldName, fieldDesc);
581
582    Label l3 = new Label();
583    mv.visitLabel(l3);
584    mv.visitLineNumber(4, l3);
585    mv.visitInsn(RETURN);
586    Label l4 = new Label();
587    mv.visitLabel(l4);
588    mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l4, 0);
589    mv.visitLocalVariable("newValue", fieldDesc, null, l0, l4, 1);
590    mv.visitMaxs(5, 2);
591    mv.visitEnd();
592  }
593
594  /**
595   * Add a non-intercepting field set method.
596   * <p>
597   * So we can set private fields on super classes.
598   * </p>
599   */
600  private void addSetNoIntercept(ClassVisitor cw, ClassMeta classMeta) {
601
602    // ALOAD or ILOAD etc
603    int iLoadOpcode = asmType.getOpcode(Opcodes.ILOAD);
604
605    // double and long have a size of 2
606    int iPosition = asmType.getSize();
607
608    if (classMeta.isLog(3)) {
609      classMeta.log(setNoInterceptMethodName + " " + setMethodDesc + " opCode:" + iLoadOpcode + "," + iPosition);
610    }
611
612    MethodVisitor mv = cw.visitMethod(ACC_PROTECTED, setNoInterceptMethodName, setMethodDesc, null, null);
613    mv.visitCode();
614    Label l0 = new Label();
615
616    mv.visitLabel(l0);
617    mv.visitLineNumber(1, l0);
618    mv.visitVarInsn(ALOAD, 0);
619    mv.visitVarInsn(iLoadOpcode, 1);// ALOAD or ILOAD
620    mv.visitFieldInsn(PUTFIELD, fieldClass, fieldName, fieldDesc);
621
622    Label l1 = new Label();
623    mv.visitLabel(l1);
624    mv.visitLineNumber(2, l1);
625    mv.visitVarInsn(ALOAD, 0);
626    mv.visitFieldInsn(GETFIELD, fieldClass, INTERCEPT_FIELD, L_INTERCEPT);
627    VisitUtil.visitIntInsn(mv, indexPosition);
628    mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "setLoadedProperty", "(I)V", false);
629    
630    Label l2 = new Label();
631    mv.visitLabel(l2);
632    mv.visitLineNumber(1, l2);
633    mv.visitInsn(RETURN);
634    Label l3 = new Label();
635    mv.visitLabel(l3);
636    mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l3, 0);
637    mv.visitLocalVariable("_newValue", fieldDesc, null, l0, l3, 1);
638    mv.visitMaxs(4, 2);
639    mv.visitEnd();
640  }
641
642}