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