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