001package io.ebean.enhance.common;
002
003import io.ebean.enhance.asm.AnnotationVisitor;
004import io.ebean.enhance.asm.ClassVisitor;
005import io.ebean.enhance.asm.FieldVisitor;
006import io.ebean.enhance.asm.MethodVisitor;
007import io.ebean.enhance.entity.*;
008
009import java.util.*;
010import java.util.logging.Level;
011import java.util.logging.Logger;
012
013import static io.ebean.enhance.Transformer.EBEAN_ASM_VERSION;
014import static io.ebean.enhance.common.EnhanceConstants.*;
015
016/**
017 * Holds the meta-data for an entity bean class that is being enhanced.
018 */
019public class ClassMeta {
020
021  private static final Logger logger = Logger.getLogger(ClassMeta.class.getName());
022
023  private final MessageOutput logout;
024  private final int logLevel;
025  private String className;
026  private String superClassName;
027  private ClassMeta superMeta;
028  /**
029   * Set to true if the class implements th GroovyObject interface.
030   */
031  private boolean hasGroovyInterface;
032  /**
033   * Set to true if the class implements the ScalaObject interface.
034   */
035  private boolean hasScalaInterface;
036  /**
037   * Set to true if the class already implements the EntityBean interface.
038   */
039  private boolean hasEntityBeanInterface;
040  private boolean alreadyEnhanced;
041  private boolean hasEqualsOrHashcode;
042  private boolean hasToString;
043  private boolean hasDefaultConstructor;
044  private boolean hasStaticInit;
045
046  /**
047   * If enhancement is adding a default constructor - only single type is supported initialising transient fields.
048   */
049  private final Set<String> unsupportedTransientMultipleTypes = new LinkedHashSet<>();
050  /**
051   * If enhancement is adding a default constructor - only default constructors are supported initialising transient fields.
052   */
053  private final Set<String> unsupportedTransientInitialisation = new LinkedHashSet<>();
054  private final Map<String, CapturedInitCode> transientInitCode = new LinkedHashMap<>();
055  private final LinkedHashMap<String, FieldMeta> fields = new LinkedHashMap<>();
056  private final HashSet<String> classAnnotation = new HashSet<>();
057  private final AnnotationInfo annotationInfo = new AnnotationInfo(null);
058  private final ArrayList<MethodMeta> methodMetaList = new ArrayList<>();
059  private final EnhanceContext enhanceContext;
060  private List<FieldMeta> allFields;
061  private boolean recordType;
062
063  public ClassMeta(EnhanceContext enhanceContext, int logLevel, MessageOutput logout) {
064    this.enhanceContext = enhanceContext;
065    this.logLevel = logLevel;
066    this.logout = logout;
067  }
068
069  /**
070   * Return the enhance context which has options for enhancement.
071   */
072  public EnhanceContext context() {
073    return enhanceContext;
074  }
075
076  /**
077   * Return the AnnotationInfo collected on methods.
078   * Used to determine Transactional method enhancement.
079   */
080  public AnnotationInfo annotationInfo() {
081    return annotationInfo;
082  }
083
084  public boolean isAllowNullableDbArray() {
085    return enhanceContext.isAllowNullableDbArray();
086  }
087
088  /**
089   * Return the transactional annotation information for a matching interface method.
090   */
091  public AnnotationInfo interfaceTransactionalInfo(String methodName, String methodDesc) {
092    AnnotationInfo annotationInfo = null;
093    for (int i = 0; i < methodMetaList.size(); i++) {
094      MethodMeta meta = methodMetaList.get(i);
095      if (meta.isMatch(methodName, methodDesc)) {
096        if (annotationInfo != null) {
097          String msg = "Error in [" + className + "] searching the transactional methods[" + methodMetaList
098            + "] found more than one match for the transactional method:" + methodName + " "
099            + methodDesc;
100
101          logger.log(Level.SEVERE, msg);
102          log(msg);
103        } else {
104          annotationInfo = meta.getAnnotationInfo();
105          if (isLog(9)) {
106            log("... found transactional info from interface " + className + " " + methodName + " " + methodDesc);
107          }
108        }
109      }
110    }
111    return annotationInfo;
112  }
113
114  public boolean isCheckSuperClassForEntity() {
115    return !superClassName.equals(C_OBJECT) && isCheckEntity();
116  }
117
118  @Override
119  public String toString() {
120    return className;
121  }
122
123  public boolean isTransactional() {
124    return classAnnotation.contains(TRANSACTIONAL_ANNOTATION);
125  }
126
127  public void setClassName(String className, String superClassName) {
128    this.className = className;
129    this.superClassName = superClassName;
130    if (superClassName.equals(C_RECORDTYPE)) {
131      recordType = true;
132    }
133  }
134
135  public String superClassName() {
136    return superClassName;
137  }
138
139  public boolean isLog(int level) {
140    return level <= logLevel;
141  }
142
143  public void log(String msg) {
144    if (className != null) {
145      msg = "cls: " + className + "  msg: " + msg;
146    }
147    logout.println("ebean-enhance> " + msg);
148  }
149
150  public void logEnhanced() {
151    String m = "enhanced ";
152    if (hasScalaInterface()) {
153      m += " (scala)";
154    }
155    if (hasGroovyInterface()) {
156      m += " (groovy)";
157    }
158    log(m);
159  }
160
161  public void setSuperMeta(ClassMeta superMeta) {
162    this.superMeta = superMeta;
163  }
164
165  /**
166   * Set to true if the class has an existing equals() or hashcode() method.
167   */
168  public void setHasEqualsOrHashcode(boolean hasEqualsOrHashcode) {
169    this.hasEqualsOrHashcode = hasEqualsOrHashcode;
170  }
171
172  public void setHasToString() {
173    this.hasToString = true;
174  }
175
176  /**
177   * Return true if Equals/hashCode is implemented on this class or a super class.
178   */
179  public boolean hasEqualsOrHashCode() {
180    if (hasEqualsOrHashcode) {
181      return true;
182    } else {
183      return (superMeta != null && superMeta.hasEqualsOrHashCode());
184    }
185  }
186
187  public boolean hasToString() {
188    if (hasToString) {
189      return true;
190    } else {
191      return (superMeta != null && superMeta.hasToString());
192    }
193  }
194
195  /**
196   * Return true if the field is a persistent field.
197   */
198  public boolean isFieldPersistent(String fieldName) {
199    FieldMeta f = field(fieldName);
200    return (f != null) && f.isPersistent();
201  }
202
203  public boolean isTransient(String fieldName) {
204    FieldMeta f = field(fieldName);
205    return (f != null && f.isTransient());
206  }
207
208  public boolean isInitTransient(String fieldName) {
209    if (!enhanceContext.isTransientInit()) {
210      return false;
211    }
212    return isTransient(fieldName);
213  }
214
215  /**
216   * Return true if the field is a persistent many field that we want to consume the init on.
217   */
218  public boolean isConsumeInitMany(String fieldName) {
219    FieldMeta f = field(fieldName);
220    return (f != null && f.isPersistent() && f.isInitMany());
221  }
222
223  /**
224   * Return the field - null when not found.
225   */
226  public FieldMeta field(String fieldName) {
227    FieldMeta f = fields.get(fieldName);
228    if (f != null) {
229      return f;
230    }
231    return (superMeta == null) ? null : superMeta.field(fieldName);
232  }
233
234  /**
235   * Return the list of fields local to this type (not inherited).
236   */
237  private List<FieldMeta> localFields() {
238    List<FieldMeta> list = new ArrayList<>();
239    for (FieldMeta fm : fields.values()) {
240      if (!fm.isObjectArray()) {
241        // add field local to this entity type
242        list.add(fm);
243      }
244    }
245    return list;
246  }
247
248  /**
249   * Return the list of fields inherited from super types that are entities.
250   */
251  private void addInheritedFields(List<FieldMeta> list) {
252    if (superMeta != null) {
253      superMeta.addFieldsForInheritance(list);
254    }
255  }
256
257  /**
258   * Add all fields to the list.
259   */
260  private void addFieldsForInheritance(List<FieldMeta> list) {
261    if (isEntity()) {
262      list.addAll(0, fields.values());
263      if (superMeta != null) {
264        superMeta.addFieldsForInheritance(list);
265      }
266    }
267  }
268
269  /**
270   * Return true if the class contains persistent fields.
271   */
272  public boolean hasPersistentFields() {
273    for (FieldMeta fieldMeta : fields.values()) {
274      if (fieldMeta.isPersistent() || fieldMeta.isTransient()) {
275        return true;
276      }
277    }
278    return superMeta != null && superMeta.hasPersistentFields();
279  }
280
281  /**
282   * Return the list of all fields including ones inherited from entity super
283   * types and mappedSuperclasses.
284   */
285  public List<FieldMeta> allFields() {
286    if (allFields != null) {
287      return allFields;
288    }
289    List<FieldMeta> list = localFields();
290    addInheritedFields(list);
291
292    this.allFields = list;
293    for (int i = 0; i < allFields.size(); i++) {
294      allFields.get(i).setIndexPosition(i);
295    }
296    return list;
297  }
298
299  /**
300   * Add field level get set methods for each field.
301   */
302  public void addFieldGetSetMethods(ClassVisitor cv) {
303    if (isEntityEnhancementRequired()) {
304      for (FieldMeta fm : fields.values()) {
305        fm.addGetSetMethods(cv, this);
306      }
307    }
308  }
309
310  /**
311   * Return true if this is a mapped superclass.
312   */
313  boolean isMappedSuper() {
314    return classAnnotation.contains(Javax.MappedSuperclass) || classAnnotation.contains(Jakarta.MappedSuperclass);
315  }
316
317  /**
318   * Return true if this is a query bean.
319   */
320  public boolean isQueryBean() {
321    return classAnnotation.contains(TYPEQUERYBEAN_ANNOTATION);
322  }
323
324  /**
325   * Return true if the class has an Entity, Embeddable or MappedSuperclass.
326   */
327  public boolean isEntity() {
328    return EntityCheck.hasEntityAnnotation(classAnnotation);
329  }
330
331  /**
332   * Return true if the class has an Entity, Embeddable, or MappedSuperclass.
333   */
334  private boolean isCheckEntity() {
335    return EntityCheck.hasEntityAnnotation(classAnnotation);
336  }
337
338  /**
339   * Return true for classes not already enhanced and yet annotated with entity, embeddable or mappedSuperclass.
340   */
341  public boolean isEntityEnhancementRequired() {
342    return !alreadyEnhanced && isEntity();
343  }
344
345  /**
346   * Return true if the bean is already enhanced.
347   */
348  public boolean isAlreadyEnhanced() {
349    return alreadyEnhanced;
350  }
351
352  /**
353   * Return the className of this entity class.
354   */
355  public String className() {
356    return className;
357  }
358
359  /**
360   * Return true if this entity bean has a super class that is an entity.
361   */
362  public boolean isSuperClassEntity() {
363    return superMeta != null && superMeta.isEntity();
364  }
365
366  /**
367   * Add a class annotation.
368   */
369  public void addClassAnnotation(String desc) {
370    classAnnotation.add(desc);
371  }
372
373  MethodVisitor createMethodVisitor(MethodVisitor mv, String name, String desc) {
374    MethodMeta methodMeta = new MethodMeta(annotationInfo, name, desc);
375    methodMetaList.add(methodMeta);
376    return new MethodReader(mv, methodMeta);
377  }
378
379  /**
380   * ACC_PUBLIC with maybe ACC_SYNTHETIC.
381   */
382  public int accPublic() {
383    return enhanceContext.accPublic();
384  }
385
386  /**
387   * ACC_PROTECTED with maybe ACC_SYNTHETIC.
388   */
389  public int accProtected() {
390    return enhanceContext.accProtected();
391  }
392
393  /**
394   * If field access use public rather than protected plus usually with synthetic.
395   */
396  public int accAccessor() {
397    return enhanceContext.isEnableEntityFieldAccess() ? accPublic() : accProtected();
398  }
399
400  /**
401   * ACC_PRIVATE with maybe ACC_SYNTHETIC.
402   */
403  public int accPrivate() {
404    return enhanceContext.accPrivate();
405  }
406
407  public boolean isToManyGetField() {
408    return enhanceContext.isToManyGetField();
409  }
410
411  /**
412   * Return the EntityBeanIntercept type that will be new'ed up for the EntityBean.
413   * For version 140+ EntityBeanIntercept is an interface and instead we new up InterceptReadWrite.
414   */
415  public String interceptNew() {
416    return enhanceContext.interceptNew();
417  }
418
419  /**
420   * Invoke a method on EntityBeanIntercept.
421   * For version 140+ EntityBeanIntercept is an interface and this uses INVOKEINTERFACE.
422   */
423  public void visitMethodInsnIntercept(MethodVisitor mv, String name, String desc) {
424    enhanceContext.visitMethodInsnIntercept(mv, name, desc);
425  }
426
427  /**
428   * If 141+ Add InterceptReadOnly support.
429   */
430  public boolean interceptAddReadOnly() {
431    return enhanceContext.interceptAddReadOnly();
432  }
433
434  public boolean isRecordType() {
435    return recordType;
436  }
437
438  public void addTransientInit(CapturedInitCode deferredInitCode) {
439    CapturedInitCode old = transientInitCode.put(deferredInitCode.name(), deferredInitCode);
440    if (old != null && !old.type().equals(deferredInitCode.type())) {
441      transientInitCode.put(deferredInitCode.name(), old);
442      unsupportedTransientMultipleTypes.add("field: " + old.name() + " types: " + old.type() + " " + deferredInitCode.type());
443    }
444  }
445
446  public Collection<CapturedInitCode> transientInit() {
447    return transientInitCode.values();
448  }
449
450  public void addUnsupportedTransientInit(String name) {
451    unsupportedTransientInitialisation.add(name);
452  }
453
454  public boolean hasTransientFieldErrors() {
455    return !unsupportedTransientMultipleTypes.isEmpty() || !unsupportedTransientInitialisation.isEmpty();
456  }
457
458  public String transientFieldErrorMessage() {
459    String msg = "ERROR: Entity class without default constructor has unsupported initialisation of transient fields. Entity class: " + className;
460    if (!unsupportedTransientMultipleTypes.isEmpty()) {
461      msg += " - fields initialised in constructor with 2 different types - " + unsupportedTransientMultipleTypes;
462    }
463    if (!unsupportedTransientInitialisation.isEmpty()) {
464      msg += " - Unsupported initialisation of transient fields - " + unsupportedTransientInitialisation;
465    }
466    msg += " Refer: https://ebean.io/docs/trouble-shooting#transient-initialisation";
467    return msg;
468  }
469
470  private static final class MethodReader extends MethodVisitor {
471
472    final MethodMeta methodMeta;
473
474    MethodReader(MethodVisitor mv, MethodMeta methodMeta) {
475      super(EBEAN_ASM_VERSION, mv);
476      this.methodMeta = methodMeta;
477    }
478
479    @Override
480    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
481      AnnotationVisitor av = null;
482      if (mv != null) {
483        av = mv.visitAnnotation(desc, visible);
484      }
485      if (!isInterestingAnnotation(desc)) {
486        return av;
487      }
488      return new AnnotationInfoVisitor(null, methodMeta.getAnnotationInfo(), av);
489    }
490
491    private boolean isInterestingAnnotation(String desc) {
492      return TRANSACTIONAL_ANNOTATION.equals(desc)
493        || TYPEQUERYBEAN_ANNOTATION.equals(desc);
494    }
495  }
496
497  /**
498   * Create and return a read only fieldVisitor for subclassing option.
499   */
500  FieldVisitor createLocalFieldVisitor(String name, String desc) {
501    return createLocalFieldVisitor(null, name, desc);
502  }
503
504  /**
505   * Create and return a new fieldVisitor for use when enhancing a class.
506   */
507  public FieldVisitor createLocalFieldVisitor(FieldVisitor fv, String name, String desc) {
508    FieldMeta fieldMeta = new FieldMeta(this, name, desc, className);
509    LocalFieldVisitor localField = new LocalFieldVisitor(fv, fieldMeta);
510    if (name.startsWith("_ebean")) {
511      // can occur when reading inheritance information on
512      // a entity that has already been enhanced
513      if (isLog(5)) {
514        log("... ignore field " + name);
515      }
516    } else {
517      fields.put(localField.name(), fieldMeta);
518    }
519    return localField;
520  }
521
522  public void setAlreadyEnhanced(boolean alreadyEnhanced) {
523    this.alreadyEnhanced = alreadyEnhanced;
524  }
525
526  public boolean hasDefaultConstructor() {
527    return hasDefaultConstructor;
528  }
529
530  public void setHasDefaultConstructor(boolean hasDefaultConstructor) {
531    this.hasDefaultConstructor = hasDefaultConstructor;
532  }
533
534  public void setHasStaticInit(boolean hasStaticInit) {
535    this.hasStaticInit = hasStaticInit;
536  }
537
538  public boolean hasStaticInit() {
539    return hasStaticInit;
540  }
541
542  public String description() {
543    StringBuilder sb = new StringBuilder();
544    appendDescription(sb);
545    return sb.toString();
546  }
547
548  private void appendDescription(StringBuilder sb) {
549    sb.append(className);
550    if (superMeta != null) {
551      sb.append(" : ");
552      superMeta.appendDescription(sb);
553    }
554  }
555
556  private boolean hasScalaInterface() {
557    return hasScalaInterface;
558  }
559
560  public void setScalaInterface(boolean hasScalaInterface) {
561    this.hasScalaInterface = hasScalaInterface;
562  }
563
564  public boolean hasEntityBeanInterface() {
565    return hasEntityBeanInterface;
566  }
567
568  public void setEntityBeanInterface(boolean hasEntityBeanInterface) {
569    this.hasEntityBeanInterface = hasEntityBeanInterface;
570  }
571
572  private boolean hasGroovyInterface() {
573    return hasGroovyInterface;
574  }
575
576  public void setGroovyInterface(boolean hasGroovyInterface) {
577    this.hasGroovyInterface = hasGroovyInterface;
578  }
579
580}