001package io.ebean.enhance.transactional;
002
003import io.ebean.enhance.asm.AnnotationVisitor;
004import io.ebean.enhance.asm.ClassVisitor;
005import io.ebean.enhance.asm.FieldVisitor;
006import io.ebean.enhance.asm.Label;
007import io.ebean.enhance.asm.MethodVisitor;
008import io.ebean.enhance.common.AlreadyEnhancedException;
009import io.ebean.enhance.common.AnnotationInfo;
010import io.ebean.enhance.common.AnnotationInfoVisitor;
011import io.ebean.enhance.common.ClassMeta;
012import io.ebean.enhance.common.EnhanceConstants;
013import io.ebean.enhance.common.EnhanceContext;
014import io.ebean.enhance.common.NoEnhancementRequiredException;
015
016import java.util.*;
017import java.util.logging.Level;
018import java.util.logging.Logger;
019
020import static io.ebean.enhance.Transformer.EBEAN_ASM_VERSION;
021import static io.ebean.enhance.asm.Opcodes.ACC_STATIC;
022import static io.ebean.enhance.asm.Opcodes.BIPUSH;
023import static io.ebean.enhance.asm.Opcodes.INVOKESTATIC;
024import static io.ebean.enhance.asm.Opcodes.PUTSTATIC;
025import static io.ebean.enhance.asm.Opcodes.RETURN;
026import static io.ebean.enhance.common.EnhanceConstants.*;
027
028/**
029 * ClassAdapter used to add transactional support.
030 */
031public class ClassAdapterTransactional extends ClassVisitor {
032
033  private static final Logger logger = Logger.getLogger(ClassAdapterTransactional.class.getName());
034
035  static final String QP_FIELD_PREFIX = "_$ebpq";
036  static final String TX_FIELD_PREFIX = "_$ebpt";
037  private static final String IO_EBEAN_FINDER = "io/ebean/Finder";
038  private static final String $_COMPANION = "$Companion";
039  private static final String INIT_PROFILE_LOCATIONS = "_$initProfileLocations";
040  private static final String LKOTLIN_METADATA = "Lkotlin/Metadata;";
041  private static final String _$EBP = "_$ebp";
042  private static final String LIO_EBEAN_PROFILE_LOCATION = "Lio/ebean/ProfileLocation;";
043
044  private final EnhanceContext enhanceContext;
045  private final ClassLoader classLoader;
046  private final ArrayList<ClassMeta> transactionalInterfaces = new ArrayList<>();
047  private final EnhanceContext.ProfileLineNumberMode profileLineNumberMode;
048  /**
049   * Class level annotation information.
050   */
051  private AnnotationInfo classAnnotationInfo;
052  private String className;
053  private boolean markAsKotlin;
054  private boolean existingStaticInitialiser;
055  private boolean finder;
056  private int queryProfileCount;
057  private int transactionProfileCount;
058  private final Map<Integer, String> txLabels = new LinkedHashMap<>();
059  /**
060   * ProfileLocation index to (method name + queryBean type)
061   */
062  private final Map<Integer,String> locationToMethodName = new HashMap<>();
063  private final Set<String> methodNames = new HashSet<>();
064  private final Set<String> overloadedMethodNames = new HashSet<>();
065
066  public ClassAdapterTransactional(ClassVisitor cv, ClassLoader classLoader, EnhanceContext context) {
067    super(EBEAN_ASM_VERSION, cv);
068    this.classLoader = classLoader;
069    this.enhanceContext = context;
070    this.profileLineNumberMode = context.profileLineMode();
071  }
072
073  public String className() {
074    return className;
075  }
076
077  public boolean isLog(int level) {
078    return enhanceContext.isLog(level);
079  }
080
081  public void log(String msg) {
082    enhanceContext.log(className, msg);
083  }
084
085  boolean isQueryBean(String owner) {
086    return enhanceContext.isQueryBean(owner, classLoader);
087  }
088
089  AnnotationInfo getClassAnnotationInfo() {
090    return classAnnotationInfo;
091  }
092
093  /**
094   * Returns Transactional information from a matching interface method.
095   * <p>
096   * Returns null if no matching (transactional) interface method was found.
097   * </p>
098   */
099  AnnotationInfo getInterfaceTransactionalInfo(String methodName, String methodDesc) {
100    AnnotationInfo interfaceAnnotationInfo = null;
101    for (int i = 0; i < transactionalInterfaces.size(); i++) {
102      ClassMeta interfaceMeta = transactionalInterfaces.get(i);
103      AnnotationInfo ai = interfaceMeta.interfaceTransactionalInfo(methodName, methodDesc);
104      if (ai != null) {
105        if (interfaceAnnotationInfo != null) {
106          String msg = "Error in [" + className + "] searching the transactional interfaces ["
107            + transactionalInterfaces + "] found more than one match for the transactional method:"
108            + methodName + " " + methodDesc;
109
110          logger.log(Level.SEVERE, msg);
111
112        } else {
113          interfaceAnnotationInfo = ai;
114          if (isLog(4)) {
115            log("inherit transactional from interface [" + interfaceMeta + "] method[" + methodName + " " + methodDesc + "]");
116          }
117        }
118      }
119    }
120    return interfaceAnnotationInfo;
121  }
122
123  /**
124   * Visit the class with interfaces.
125   */
126  @Override
127  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
128    className = name;
129    if (C_QUERYBEAN.equals(superName)) {
130      // query beans do not need profile locations
131      throw new NoEnhancementRequiredException();
132    }
133    finder = superName.equals(IO_EBEAN_FINDER);
134    // Note: interfaces can be an empty array but not null
135    int n = 1 + interfaces.length;
136    String[] newInterfaces = new String[n];
137    for (int i = 0; i < interfaces.length; i++) {
138      newInterfaces[i] = interfaces[i];
139      if (newInterfaces[i].equals(EnhanceConstants.C_ENHANCEDTRANSACTIONAL)) {
140        throw new AlreadyEnhancedException(name);
141      }
142      ClassMeta interfaceMeta = enhanceContext.interfaceMeta(newInterfaces[i], classLoader);
143      if (interfaceMeta != null && interfaceMeta.isTransactional()) {
144        // the interface was transactional. We gather its information
145        // because our methods inherit that transactional configuration
146        transactionalInterfaces.add(interfaceMeta);
147
148        if (isLog(6)) {
149          log(" implements transactional interface " + interfaceMeta.description());
150        }
151      }
152    }
153
154    // Add the EnhancedTransactional interface
155    newInterfaces[newInterfaces.length - 1] = EnhanceConstants.C_ENHANCEDTRANSACTIONAL;
156    super.visit(version, access, name, signature, superName, newInterfaces);
157  }
158
159  /**
160   * Visit class level annotations.
161   */
162  @Override
163  public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
164    if (LKOTLIN_METADATA.equals(desc)) {
165      markAsKotlin = true;
166    }
167    AnnotationVisitor av = super.visitAnnotation(desc, visible);
168    if (desc.equals(TRANSACTIONAL_ANNOTATION)) {
169      // we have class level Transactional annotation
170      // which will act as default for all methods in this class
171      classAnnotationInfo = new AnnotationInfo(null);
172      return new AnnotationInfoVisitor(null, classAnnotationInfo, av);
173    } else {
174      return av;
175    }
176  }
177
178  @Override
179  public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
180    if (name.startsWith(_$EBP) && desc.equals(LIO_EBEAN_PROFILE_LOCATION)) {
181      throw new AlreadyEnhancedException(className);
182    }
183    return super.visitField(access, name, desc, signature, value);
184  }
185
186  /**
187   * Visit the methods specifically looking for method level transactional
188   * annotations.
189   */
190  @Override
191  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
192    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
193    if (name.equals(INIT_PROFILE_LOCATIONS)) {
194      throw new AlreadyEnhancedException(className);
195    }
196    if (name.equals(INIT)) {
197      if (checkConstructorForProfileLocation(desc)) {
198        // check constructor, it might contain query bean queries needing profile location
199        if (isLog(7)) {
200          log("checking constructor, maybe add profile location for queries in className:" + className + " " + name + " [" + desc + "]");
201        }
202        return new ConstructorMethodAdapter(this, mv);
203      }
204      return mv;
205    }
206    if (name.equals(CLINIT)) {
207      if (!enhanceContext.isEnableProfileLocation()) {
208        // not enhancing class static initialiser
209        return mv;
210      } else {
211        if (isLog(4)) {
212          log("... <clinit> exists - adding call to _$initProfileLocations()");
213        }
214        existingStaticInitialiser = true;
215        return new StaticInitAdapter(mv, access, name, desc, className);
216      }
217    }
218    return new MethodAdapter(this, mv, access, name, desc);
219  }
220
221  private boolean checkConstructorForProfileLocation(String desc) {
222    return enhanceContext.isEnableProfileLocation()
223      && !desc.startsWith("(Lio/ebean/Query;")
224      && !kotlinCompanion();
225  }
226
227  private boolean kotlinCompanion() {
228    return markAsKotlin && className.endsWith($_COMPANION);
229  }
230
231  @Override
232  public void visitEnd() {
233    if (queryProfileCount == 0 && transactionProfileCount == 0) {
234      throw new NoEnhancementRequiredException(className);
235    }
236    if (isLog(4)) {
237      log("queryCount:" + queryProfileCount + " txnCount:" + transactionProfileCount + " profileLocation:" + isEnableProfileLocation());
238    }
239    if (enhanceContext.isEnableProfileLocation()) {
240      addStaticFieldDefinitions();
241      addStaticFieldInitialisers();
242      if (!existingStaticInitialiser) {
243        if (isLog(5)) {
244          log("... add <clinit> to call _$initProfileLocations()");
245        }
246        addStaticInitialiser();
247      }
248    }
249    if (transactionProfileCount > 0) {
250      enhanceContext.summaryTransactional(className);
251    } else {
252      enhanceContext.summaryQueryBeanCaller(className);
253    }
254    super.visitEnd();
255  }
256
257  public void logEnhanced() {
258    if (transactionProfileCount > 0) {
259      log("enhanced transactional");
260    } else {
261      log("enhanced query bean caller");
262    }
263  }
264
265  private void addStaticFieldDefinitions() {
266    for (int i = 0; i < queryProfileCount; i++) {
267      FieldVisitor fv = cv.visitField(enhanceContext.accPrivate() + ACC_STATIC, QP_FIELD_PREFIX + i, "Lio/ebean/ProfileLocation;", null, null);
268      fv.visitEnd();
269    }
270    for (int i = 0; i < transactionProfileCount; i++) {
271      FieldVisitor fv = cv.visitField(enhanceContext.accPrivate() + ACC_STATIC, TX_FIELD_PREFIX + i, "Lio/ebean/ProfileLocation;", null, null);
272      fv.visitEnd();
273    }
274  }
275
276  private void addStaticFieldInitialisers() {
277    MethodVisitor mv = cv.visitMethod(enhanceContext.accPrivate() + ACC_STATIC, "_$initProfileLocations", NOARG_VOID, null, null);
278    mv.visitCode();
279
280    final boolean supportsProfileWithLine = enhanceContext.supportsProfileWithLine();
281    for (int i = 0; i < queryProfileCount; i++) {
282      Label l0 = new Label();
283      mv.visitLabel(l0);
284      mv.visitLineNumber(1, l0);
285      String createMethod = supportsProfileWithLine && includeLineNumber(i) ? "createWithLine" : "create";
286      mv.visitMethodInsn(INVOKESTATIC, "io/ebean/ProfileLocation", createMethod, "()Lio/ebean/ProfileLocation;", true);
287      mv.visitFieldInsn(PUTSTATIC, className, QP_FIELD_PREFIX + i, "Lio/ebean/ProfileLocation;");
288    }
289
290    for (int i = 0; i < transactionProfileCount; i++) {
291      Label l0 = new Label();
292      mv.visitLabel(l0);
293      mv.visitLineNumber(2, l0);
294      String label = getTxnLabel(i);
295      if (supportsProfileWithLine) {
296        mv.visitLdcInsn(label);
297        mv.visitMethodInsn(INVOKESTATIC, "io/ebean/ProfileLocation", "create", "(Ljava/lang/String;)Lio/ebean/ProfileLocation;", true);
298      } else {
299        mv.visitIntInsn(BIPUSH, 0); // always 0 for historic reasons
300        mv.visitLdcInsn(label);
301        mv.visitMethodInsn(INVOKESTATIC, "io/ebean/ProfileLocation", "create", "(ILjava/lang/String;)Lio/ebean/ProfileLocation;", true);
302      }
303      mv.visitFieldInsn(PUTSTATIC, className, TX_FIELD_PREFIX + i, "Lio/ebean/ProfileLocation;");
304    }
305
306    Label l1 = new Label();
307    mv.visitLabel(l1);
308    mv.visitLineNumber(3, l1);
309    mv.visitInsn(RETURN);
310    mv.visitMaxs(1, 0);
311    mv.visitEnd();
312  }
313
314  private String getTxnLabel(int i) {
315    String label = txLabels.get(i);
316    return (label != null) ? label : "";
317  }
318
319  /**
320   * Add a static initialization block when there was not one on the class.
321   */
322  private void addStaticInitialiser() {
323    MethodVisitor mv = cv.visitMethod(ACC_STATIC, CLINIT, NOARG_VOID, null, null);
324    mv.visitCode();
325    Label l0 = new Label();
326    mv.visitLabel(l0);
327    mv.visitLineNumber(4, l0);
328    mv.visitMethodInsn(INVOKESTATIC, className, INIT_PROFILE_LOCATIONS, NOARG_VOID, false);
329    Label l1 = new Label();
330    mv.visitLabel(l1);
331    mv.visitLineNumber(5, l1);
332    mv.visitInsn(RETURN);
333    mv.visitMaxs(0, 0);
334    mv.visitEnd();
335  }
336
337  /**
338   * Return true if profile location enhancement is on.
339   */
340  boolean isEnableProfileLocation() {
341    return enhanceContext.isEnableProfileLocation();
342  }
343
344  int nextQueryProfileLocation() {
345    return queryProfileCount++;
346  }
347
348  /**
349   * Return the next index for query profile location.
350   */
351  int nextQueryProfileLocation(String methodName, String queryBeanType) {
352    if (profileLineNumberMode == EnhanceContext.ProfileLineNumberMode.AUTO) {
353      // looking to determine if the methodName + queryBean is unique (desire no line numbers) or not unique
354      // implying there is method overloading (desire line numbers to identify the code executing the query)
355      final String key = methodName + queryBeanType;
356      locationToMethodName.put(queryProfileCount, key);
357      if (!methodNames.add(key)) {
358        overloadedMethodNames.add(key);
359      }
360    }
361    return queryProfileCount++;
362  }
363
364  /**
365   * Include the line number when method overloading means we can't identify the query by method name only.
366   */
367  private boolean includeLineNumber(int pos) {
368    if (profileLineNumberMode == EnhanceContext.ProfileLineNumberMode.ALL) {
369      return true;
370    }
371    final String name = locationToMethodName.get(pos);
372    return name != null && overloadedMethodNames.contains(name);
373  }
374
375  /**
376   * Return the next index for transaction profile location.
377   */
378  int nextTransactionLocation() {
379    return transactionProfileCount++;
380  }
381
382  /**
383   * Return true if this enhancing class extends Ebean Finder.
384   */
385  boolean isFinder() {
386    return finder;
387  }
388
389  /**
390   * Set the transaction label for a given index.
391   */
392  void putTxnLabel(int locationField, String txLabel) {
393    txLabels.put(locationField, txLabel);
394  }
395}