001package io.ebean.enhance.entity;
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.asm.Opcodes;
008import io.ebean.enhance.common.ClassMeta;
009import io.ebean.enhance.common.EnhanceConstants;
010import io.ebean.enhance.common.EnhanceContext;
011import io.ebean.enhance.common.NoEnhancementRequiredException;
012
013import static io.ebean.enhance.Transformer.EBEAN_ASM_VERSION;
014
015/**
016 * ClassAdapter for enhancing entities.
017 * <p>
018 * Used for javaagent or ant etc to modify the class with field interception.
019 * </p>
020 * <p>
021 * This is NOT used for subclass generation.
022 * </p>
023 */
024public final class ClassAdapterEntity extends ClassVisitor implements EnhanceConstants {
025
026  private final EnhanceContext enhanceContext;
027  private final ClassLoader classLoader;
028  private final ClassMeta classMeta;
029  private boolean firstMethod = true;
030
031  public ClassAdapterEntity(ClassVisitor cv, ClassLoader classLoader, EnhanceContext context) {
032    super(EBEAN_ASM_VERSION, cv);
033    this.classLoader = classLoader;
034    this.enhanceContext = context;
035    this.classMeta = context.createClassMeta();
036  }
037
038  /**
039   * Log that the class has been enhanced.
040   */
041  public void logEnhanced() {
042    classMeta.logEnhanced();
043  }
044
045  public boolean isLog(int level) {
046    return classMeta.isLog(level);
047  }
048
049  public void log(String msg) {
050    classMeta.log(msg);
051  }
052
053  /**
054   * Create the class definition replacing the className and super class.
055   */
056  @Override
057  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
058    skipMockitoMock(name);
059    classMeta.setClassName(name, superName);
060
061    String[] c = new String[interfaces.length + 1];
062    for (int i = 0; i < interfaces.length; i++) {
063      c[i] = interfaces[i];
064      if (c[i].equals(C_ENTITYBEAN)) {
065        throw new NoEnhancementRequiredException();
066      }
067      if (c[i].equals(C_SCALAOBJECT)) {
068        classMeta.setScalaInterface(true);
069      }
070      if (c[i].equals(C_GROOVYOBJECT)) {
071        classMeta.setGroovyInterface(true);
072      }
073    }
074    // add the EntityBean interface
075    c[c.length - 1] = C_ENTITYBEAN;
076    if (classMeta.isLog(8)) {
077      classMeta.log("... add EntityBean interface");
078    }
079    if (!superName.equals("java/lang/Object")) {
080      // read information about superClasses...
081      if (classMeta.isLog(7)) {
082        classMeta.log("read information about superClasses " + superName + " to see if it is entity/embedded/mappedSuperclass");
083      }
084      ClassMeta superMeta = enhanceContext.superMeta(superName, classLoader);
085      if (superMeta != null && superMeta.isEntity()) {
086        // the superClass is an entity/embedded/mappedSuperclass...
087        classMeta.setSuperMeta(superMeta);
088      }
089    }
090    super.visit(version, access, name, signature, superName, c);
091  }
092
093  /**
094   * Do not enhance a Mockito mock or spy.
095   */
096  private void skipMockitoMock(String name) {
097    if (name.contains(MOCKITO_MOCK)) {
098      throw new NoEnhancementRequiredException();
099    }
100  }
101
102  @Override
103  public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
104    classMeta.addClassAnnotation(desc);
105    return super.visitAnnotation(desc, visible);
106  }
107
108  private boolean isPropertyChangeListenerField(String desc) {
109    return desc.equals("Ljava/beans/PropertyChangeSupport;");
110  }
111
112  /**
113   * The ebeanIntercept field is added once but thats all. Note the other
114   * fields are defined in the superclass.
115   */
116  @Override
117  public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
118    if ((access & Opcodes.ACC_STATIC) != 0) {
119      // no interception of static fields
120      return super.visitField(access, name, desc, signature, value);
121    }
122    if (isPropertyChangeListenerField(desc)) {
123      if (isLog(4)) {
124        classMeta.log("Found existing PropertyChangeSupport field " + name);
125      }
126      // no interception on PropertyChangeSupport field
127      return super.visitField(access, name, desc, signature, value);
128    }
129    if ((access & Opcodes.ACC_TRANSIENT) != 0) {
130      // no interception of transient fields
131      return super.visitField(access, name, desc, signature, value);
132    }
133    if ((access & Opcodes.ACC_FINAL) != 0) {
134      // remove final modifier from fields (for lazy loading partials in Java9+)
135      access = (access ^ Opcodes.ACC_FINAL);
136    }
137    FieldVisitor fv = super.visitField(access, name, desc, signature, value);
138    return classMeta.createLocalFieldVisitor(fv, name, desc);
139  }
140
141  /**
142   * Replace the method code with field interception.
143   */
144  @Override
145  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
146    if (firstMethod) {
147      if (classMeta.isAlreadyEnhanced()) {
148        throw new NoEnhancementRequiredException();
149      }
150      if (classMeta.hasEntityBeanInterface()) {
151        log("Enhancing when EntityBean interface already exists!");
152      }
153      IndexFieldWeaver.addPropertiesField(cv, classMeta);
154      if (isLog(4)) {
155        log("... add _ebean_props field");
156      }
157      if (!classMeta.isSuperClassEntity()) {
158        // only add the intercept and identity fields if
159        // the superClass is not also enhanced
160        if (isLog(4)) {
161          log("... add intercept and identity fields");
162        }
163        InterceptField.addField(cv, classMeta, enhanceContext.isTransientInternalFields());
164        MethodEquals.addIdentityField(cv, classMeta);
165
166      }
167      firstMethod = false;
168    }
169
170    if (isLog(4)) {
171      log("--- #### method name[" + name + "] desc[" + desc + "] sig[" + signature + "]");
172    }
173
174    if (isConstructor(name, desc)) {
175      if (desc.equals(NOARG_VOID)) {
176        // ensure public access on the default constructor
177        access = Opcodes.ACC_PUBLIC;
178      }
179      MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
180      return new ConstructorAdapter(mv, classMeta, desc);
181    }
182
183    if (isStaticInit(name, desc)) {
184      if (isLog(4)) {
185        log("... --- #### enhance existing static init method");
186      }
187      MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, name, desc, signature, exceptions);
188      return new MethodStaticInitAdapter(mv, classMeta);
189    }
190
191    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
192    if (interceptEntityMethod(access, name, desc)) {
193      // change the method replacing the relevant GETFIELD PUTFIELD with
194      // our special field methods with interception...
195      return new MethodFieldAdapter(mv, classMeta, name + " " + desc);
196    }
197    // just leave as is, no interception etc
198    return mv;
199  }
200
201  /**
202   * Add methods to get and set the entityBeanIntercept. Also add the
203   * writeReplace method to control serialisation.
204   */
205  @Override
206  public void visitEnd() {
207    if (!classMeta.isEntityEnhancementRequired()) {
208      throw new NoEnhancementRequiredException();
209    }
210    if (!classMeta.hasStaticInit()) {
211      IndexFieldWeaver.addPropertiesInit(cv, classMeta);
212    }
213    if (!classMeta.hasDefaultConstructor()) {
214      DefaultConstructor.add(cv, classMeta);
215    }
216    if (isLog(4)) {
217      log("... add _ebean_getPropertyNames() and _ebean_getPropertyName()");
218    }
219    IndexFieldWeaver.addGetPropertyNames(cv, classMeta);
220    IndexFieldWeaver.addGetPropertyName(cv, classMeta);
221
222    if (!classMeta.isSuperClassEntity()) {
223      if (isLog(8)) {
224        log("... add _ebean_getIntercept() and _ebean_setIntercept()");
225      }
226      InterceptField.addGetterSetter(cv, classMeta);
227    }
228
229    // Add the field set/get methods which are used in place
230    // of GETFIELD PUTFIELD instructions
231    classMeta.addFieldGetSetMethods(cv);
232
233    //Add the getField(index) and setField(index) methods
234    IndexFieldWeaver.addMethods(cv, classMeta);
235
236    MethodSetEmbeddedLoaded.addMethod(cv, classMeta);
237    MethodIsEmbeddedNewOrDirty.addMethod(cv, classMeta);
238    MethodNewInstance.addMethod(cv, classMeta);
239    MethodNewInstanceReadOnly.interceptAddReadOnly(cv, classMeta);
240    MethodToString.addMethod(cv, classMeta);
241
242    // register with the agentContext
243    enhanceContext.addClassMeta(classMeta);
244    enhanceContext.summaryEntity(classMeta.className());
245    super.visitEnd();
246  }
247
248  private boolean isConstructor(String name, String desc) {
249    if (name.equals(INIT)) {
250      if (desc.equals(NOARG_VOID)) {
251        classMeta.setHasDefaultConstructor(true);
252      }
253      return true;
254    }
255    return false;
256  }
257
258  private boolean isStaticInit(String name, String desc) {
259    if (name.equals(CLINIT) && desc.equals(NOARG_VOID)) {
260      classMeta.setHasStaticInit(true);
261      return true;
262    }
263    return false;
264  }
265
266  private boolean interceptEntityMethod(int access, String name, String desc) {
267    if ((access & Opcodes.ACC_STATIC) != 0) {
268      // no interception of static methods?
269      if (isLog(4)) {
270        log("Skip intercepting static method " + name);
271      }
272      return false;
273    }
274    if (name.equals("hashCode") && desc.equals("()I")) {
275      classMeta.setHasEqualsOrHashcode(true);
276      return true;
277    }
278    if (name.equals("equals") && desc.equals("(Ljava/lang/Object;)Z")) {
279      classMeta.setHasEqualsOrHashcode(true);
280      return true;
281    }
282    if (name.equals("toString") && desc.equals("()Ljava/lang/String;")) {
283      // don't intercept toString as its is used during debugging etc
284      classMeta.setHasToString();
285      return false;
286    }
287    return true;
288  }
289}