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