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