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