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