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