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