001package io.ebean.enhance;
002
003import io.ebean.enhance.asm.ClassReader;
004import io.ebean.enhance.asm.ClassWriter;
005import io.ebean.enhance.asm.ClassWriterWithoutClassLoading;
006import io.ebean.enhance.common.AgentManifest;
007import io.ebean.enhance.common.AlreadyEnhancedException;
008import io.ebean.enhance.common.ClassBytesReader;
009import io.ebean.enhance.common.CommonSuperUnresolved;
010import io.ebean.enhance.common.DetectEnhancement;
011import io.ebean.enhance.common.EnhanceContext;
012import io.ebean.enhance.common.NoEnhancementRequiredException;
013import io.ebean.enhance.common.TransformRequest;
014import io.ebean.enhance.common.UrlPathHelper;
015import io.ebean.enhance.entity.ClassAdapterEntity;
016import io.ebean.enhance.entity.ClassPathClassBytesReader;
017import io.ebean.enhance.entity.MessageOutput;
018import io.ebean.enhance.querybean.TypeQueryClassAdapter;
019import io.ebean.enhance.transactional.ClassAdapterTransactional;
020import io.ebean.enhance.transactional.TransactionalMethodKey;
021import org.avaje.agentloader.AgentLoader;
022
023import java.lang.instrument.ClassFileTransformer;
024import java.lang.instrument.IllegalClassFormatException;
025import java.lang.instrument.Instrumentation;
026import java.net.URL;
027import java.security.ProtectionDomain;
028import java.util.ArrayList;
029import java.util.List;
030
031/**
032 * A Class file Transformer that performs Ebean enhancement of entity beans,
033 * transactional methods and query bean enhancement.
034 * <p>
035 * This is used as both a java agent or via Maven and Gradle plugins etc.
036 * </p>
037 */
038public class Transformer implements ClassFileTransformer {
039
040  public static void agentmain(String agentArgs, Instrumentation inst) {
041    premain(agentArgs, inst);
042  }
043
044  public static void premain(String agentArgs, Instrumentation inst) {
045
046    instrumentation = inst;
047    transformer = new Transformer(null, agentArgs);
048    inst.addTransformer(transformer);
049  }
050
051  private static Instrumentation instrumentation;
052
053  private static Transformer transformer;
054
055  private final EnhanceContext enhanceContext;
056
057  private final List<CommonSuperUnresolved> unresolved = new ArrayList<>();
058
059  private boolean keepUnresolved;
060
061  public Transformer(ClassLoader classLoader, String agentArgs) {
062    if (classLoader == null) {
063      classLoader = getClass().getClassLoader();
064    }
065    ClassBytesReader reader = new ClassPathClassBytesReader(null);
066    AgentManifest manifest = AgentManifest.read(classLoader, null);
067    this.enhanceContext = new EnhanceContext(reader, agentArgs, manifest);
068  }
069
070  /**
071  * Create with an EnhancementContext (for IDE Plugins mainly)
072  */
073  public Transformer(EnhanceContext enhanceContext) {
074    this.enhanceContext = enhanceContext;
075  }
076
077  /**
078  * Create a transformer for entity bean enhancement and transactional method enhancement.
079  *
080  * @param bytesReader reads resources from class path for related inheritance and interfaces
081  * @param agentArgs command line arguments for debug level etc
082  */
083  public Transformer(ClassBytesReader bytesReader, String agentArgs, AgentManifest manifest) {
084    this.enhanceContext = new EnhanceContext(bytesReader, agentArgs, manifest);
085  }
086
087  /**
088  * Return the Instrumentation instance.
089  */
090  public static Instrumentation instrumentation()  {
091    verifyInitialization();
092    return instrumentation;
093  }
094
095  /**
096  * Return the Transformer instance.
097  */
098  public static Transformer get()  {
099    verifyInitialization();
100    return transformer;
101  }
102
103  /**
104  * Use agent loader if necessary to initialise the transformer.
105  */
106  public static void verifyInitialization() {
107    if (instrumentation == null) {
108      if (!AgentLoader.loadAgentFromClasspath("ebean-agent", "debug=1")) {
109        throw new IllegalStateException("ebean-agent not found in classpath - not dynamically loaded");
110      }
111    }
112  }
113
114  /**
115  * Set this to keep and report unresolved explicitly.
116  */
117  public void setKeepUnresolved() {
118    this.keepUnresolved = true;
119  }
120
121  /**
122  * Change the logout to something other than system out.
123  */
124  public void setLogout(MessageOutput logout) {
125    this.enhanceContext.setLogout(logout);
126  }
127
128  public void log(int level, String msg) {
129    log(level, null, msg);
130  }
131
132  private void log(int level, String className, String msg) {
133    enhanceContext.log(level, className, msg);
134  }
135
136  public int getLogLevel() {
137    return enhanceContext.getLogLevel();
138  }
139
140  @Override
141  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
142
143    try {
144      // ignore JDK and JDBC classes etc
145      if (enhanceContext.isIgnoreClass(className)) {
146        log(9, className, "ignore class");
147        return null;
148      }
149      TransformRequest request = new TransformRequest(className, classfileBuffer);
150
151      if (enhanceContext.detectEntityTransactionalEnhancement(className)) {
152        enhanceEntityAndTransactional(loader, request);
153      }
154
155      if (enhanceContext.detectQueryBeanEnhancement(className)) {
156        enhanceQueryBean(loader, request);
157      }
158
159      if (request.isEnhanced()) {
160        return request.getBytes();
161      }
162
163      log(9, className, "no enhancement on class");
164      return null;
165
166    } catch (NoEnhancementRequiredException e) {
167      // the class is an interface
168      log(8, className, "No Enhancement required " + e.getMessage());
169      return null;
170
171    } catch (Exception e) {
172      if (enhanceContext.isThrowOnError()) {
173        throw new IllegalStateException(e);
174      }
175      enhanceContext.log(e);
176      return null;
177    } finally {
178      logUnresolvedCommonSuper(className);
179    }
180  }
181
182  /**
183  * Perform entity and transactional enhancement.
184  */
185  private void enhanceEntityAndTransactional(ClassLoader loader, TransformRequest request) {
186    try {
187      DetectEnhancement detect = detect(loader, request.getBytes());
188      if (detect.isEntity()) {
189        if (detect.isEnhancedEntity()) {
190          detect.log(3, "already enhanced entity");
191        } else {
192          entityEnhancement(loader, request);
193        }
194      }
195      if (enhanceContext.isEnableProfileLocation() || detect.isTransactional()) {
196        if (detect.isEnhancedTransactional()) {
197          detect.log(3, "already enhanced transactional");
198        } else {
199          transactionalEnhancement(loader, request);
200        }
201      }
202    } catch (NoEnhancementRequiredException e) {
203      log(8, request.getClassName(), "No entity or transactional enhancement required " + e.getMessage());
204    }
205  }
206
207  /**
208  * Return the transaction profiling keys.
209  *
210  * We use these to decode a the transaction profile.
211  */
212  public List<TransactionalMethodKey> getTransactionProfilingKeys() {
213    return enhanceContext.getTransactionProfilingKeys();
214  }
215
216  /**
217  * Log and common superclass classpath issues that defaulted to Object.
218  */
219  private void logUnresolvedCommonSuper(String className) {
220    if (!keepUnresolved && !unresolved.isEmpty()) {
221      for (CommonSuperUnresolved commonUnresolved : unresolved) {
222        log(0, className, commonUnresolved.getMessage());
223      }
224      unresolved.clear();
225    }
226  }
227
228  /**
229  * Return the list of unresolved common superclass issues. This should be cleared
230  * after each use and can only be used with {@link #setKeepUnresolved()}.
231  */
232  public List<CommonSuperUnresolved> getUnresolved() {
233    return unresolved;
234  }
235
236  /**
237  * Perform entity bean enhancement.
238  */
239  private void entityEnhancement(ClassLoader loader, TransformRequest request) {
240
241    ClassReader cr = new ClassReader(request.getBytes());
242    ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader);
243    ClassAdapterEntity ca = new ClassAdapterEntity(cw, loader, enhanceContext);
244    try {
245
246      cr.accept(ca, ClassReader.EXPAND_FRAMES);
247
248      if (ca.isLog(1)) {
249        ca.logEnhanced();
250      }
251
252      request.enhancedEntity(cw.toByteArray());
253
254    } catch (AlreadyEnhancedException e) {
255      if (ca.isLog(2)) {
256        ca.log("already enhanced entity");
257      }
258      request.enhancedEntity(null);
259
260    } catch (NoEnhancementRequiredException e) {
261      if (ca.isLog(3)) {
262        ca.log("skipping... no enhancement required");
263      }
264    } finally {
265      unresolved.addAll(cw.getUnresolved());
266    }
267  }
268
269  /**
270  * Perform transactional enhancement and Finder profileLocation enhancement.
271  */
272  private void transactionalEnhancement(ClassLoader loader, TransformRequest request) {
273
274    ClassReader cr = new ClassReader(request.getBytes());
275    ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader);
276    ClassAdapterTransactional ca = new ClassAdapterTransactional(cw, loader, enhanceContext);
277
278    try {
279      cr.accept(ca, ClassReader.EXPAND_FRAMES);
280
281      if (ca.isLog(1)) {
282        ca.log("enhanced transactional");
283      }
284
285      request.enhancedTransactional(cw.toByteArray());
286
287    } catch (AlreadyEnhancedException e) {
288      if (ca.isLog(3)) {
289        ca.log("already enhanced");
290      }
291
292    } catch (NoEnhancementRequiredException e) {
293      if (ca.isLog(3)) {
294        ca.log("skipping... no enhancement required");
295      }
296    } finally {
297      unresolved.addAll(cw.getUnresolved());
298    }
299  }
300
301
302  /**
303  * Perform enhancement.
304  */
305  private void enhanceQueryBean(ClassLoader loader, TransformRequest request) {
306
307    ClassReader cr = new ClassReader(request.getBytes());
308    ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader);
309    TypeQueryClassAdapter ca = new TypeQueryClassAdapter(cw, enhanceContext);
310
311    try {
312      cr.accept(ca, ClassReader.EXPAND_FRAMES);
313      if (ca.isLog(9)) {
314        ca.log("... completed");
315      }
316      request.enhancedQueryBean(cw.toByteArray());
317
318    } catch (AlreadyEnhancedException e) {
319      if (ca.isLog(1)) {
320        ca.log("already enhanced");
321      }
322
323    } catch (NoEnhancementRequiredException e) {
324      if (ca.isLog(9)) {
325        ca.log("... skipping, no enhancement required");
326      }
327    } finally {
328      unresolved.addAll(cw.getUnresolved());
329    }
330  }
331
332  /**
333  * Helper method to split semi-colon separated class paths into a URL array.
334  */
335  public static URL[] parseClassPaths(String extraClassPath) {
336
337    if (extraClassPath == null) {
338      return new URL[0];
339    }
340
341    return UrlPathHelper.convertToUrl(extraClassPath.split(";"));
342  }
343
344  /**
345  * Read the bytes quickly trying to detect if it needs entity or transactional
346  * enhancement.
347  */
348  private DetectEnhancement detect(ClassLoader classLoader, byte[] classfileBuffer) {
349
350    DetectEnhancement detect = new DetectEnhancement(classLoader, enhanceContext);
351
352    ClassReader cr = new ClassReader(classfileBuffer);
353    cr.accept(detect, ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES);
354    return detect;
355  }
356}