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