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