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