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