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}