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