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 (IllegalArgumentException | IllegalStateException e) {
195      log(2, className, "No enhancement on class due to " + e);
196      return null;
197    } catch (Exception e) {
198      if (enhanceContext.isThrowOnError()) {
199        throw new IllegalStateException(e);
200      }
201      enhanceContext.log(className, "Error during transform " + e);
202      enhanceContext.log(e);
203      return null;
204    } finally {
205      logUnresolvedCommonSuper(className);
206    }
207  }
208
209  private boolean isQueryBeanCompanion(String className, ClassLoader classLoader) {
210    return className.endsWith("$Companion") && enhanceContext.isQueryBean(className, classLoader);
211  }
212
213  /**
214   * Perform entity and transactional enhancement.
215   */
216  private void enhanceEntityAndTransactional(ClassLoader loader, TransformRequest request) {
217    try {
218      DetectEnhancement detect = detect(loader, request.getBytes());
219      if (detect.isEntity()) {
220        if (detect.isEnhancedEntity()) {
221          detect.log(3, "already enhanced entity");
222        } else {
223          entityEnhancement(loader, request);
224        }
225      }
226      if (enhanceContext.isEnableProfileLocation() || detect.isTransactional()) {
227        if (detect.isEnhancedTransactional()) {
228          detect.log(3, "already enhanced transactional");
229        } else {
230          transactionalEnhancement(loader, request);
231        }
232      }
233    } catch (NoEnhancementRequiredException e) {
234      log(8, request.getClassName(), "No entity or transactional enhancement required " + e.getMessage());
235    }
236  }
237
238  /**
239   * Log and common superclass classpath issues that defaulted to Object.
240   */
241  private void logUnresolvedCommonSuper(String className) {
242    if (!keepUnresolved && !unresolved.isEmpty()) {
243      for (CommonSuperUnresolved commonUnresolved : unresolved) {
244        log(0, className, commonUnresolved.getMessage());
245      }
246      unresolved.clear();
247    }
248  }
249
250  /**
251   * Return the list of unresolved common superclass issues. This should be cleared
252   * after each use and can only be used with {@link #setKeepUnresolved()}.
253   */
254  public List<CommonSuperUnresolved> getUnresolved() {
255    return unresolved;
256  }
257
258  /**
259   * Perform entity bean enhancement.
260   */
261  private void entityEnhancement(ClassLoader loader, TransformRequest request) {
262    ClassReader cr = new ClassReader(request.getBytes());
263    ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader);
264    ClassAdapterEntity ca = new ClassAdapterEntity(cw, loader, enhanceContext);
265    try {
266      cr.accept(ca, ClassReader.EXPAND_FRAMES);
267      if (ca.isLog(2)) {
268        ca.logEnhanced();
269        unresolved.addAll(cw.getUnresolved());
270      }
271
272      request.enhancedEntity(cw.toByteArray());
273
274    } catch (AlreadyEnhancedException e) {
275      if (ca.isLog(3)) {
276        ca.log("already enhanced entity");
277      }
278      request.enhancedEntity(null);
279    } catch (NoEnhancementRequiredException e) {
280      if (ca.isLog(4)) {
281        ca.log("skipped entity enhancement");
282      }
283    }
284  }
285
286  /**
287   * Perform transactional enhancement and Finder profileLocation enhancement.
288   */
289  private void transactionalEnhancement(ClassLoader loader, TransformRequest request) {
290    ClassReader cr = new ClassReader(request.getBytes());
291    ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader);
292    ClassAdapterTransactional ca = new ClassAdapterTransactional(cw, loader, enhanceContext);
293    try {
294      cr.accept(ca, ClassReader.EXPAND_FRAMES);
295      if (ca.isLog(2)) {
296        ca.logEnhanced();
297      }
298
299      request.enhancedTransactional(cw.toByteArray());
300
301    } catch (AlreadyEnhancedException e) {
302      if (ca.isLog(3)) {
303        ca.log("already transactional enhanced");
304      }
305    } catch (NoEnhancementRequiredException e) {
306      if (ca.isLog(4)) {
307        ca.log("skipped transactional enhancement");
308      }
309    } finally {
310      unresolved.addAll(cw.getUnresolved());
311    }
312  }
313
314  /**
315   * Perform enhancement.
316   */
317  private void enhanceQueryBean(ClassLoader loader, TransformRequest request) {
318    ClassReader cr = new ClassReader(request.getBytes());
319    ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader);
320    TypeQueryClassAdapter ca = new TypeQueryClassAdapter(cw, enhanceContext, loader);
321    try {
322      cr.accept(ca, ClassReader.EXPAND_FRAMES);
323      request.enhancedQueryBean(cw.toByteArray());
324    } catch (AlreadyEnhancedException e) {
325      if (ca.isLog(3)) {
326        ca.log("already query bean enhanced");
327      }
328    } catch (NoEnhancementRequiredException e) {
329      if (ca.isLog(4)) {
330        ca.log("skipped query bean enhancement");
331      }
332    } finally {
333      unresolved.addAll(cw.getUnresolved());
334    }
335  }
336
337  /**
338   * Helper method to split semi-colon separated class paths into a URL array.
339   */
340  public static URL[] parseClassPaths(String extraClassPath) {
341    if (extraClassPath == null) {
342      return new URL[0];
343    }
344    return UrlPathHelper.convertToUrl(extraClassPath.split(";"));
345  }
346
347  /**
348   * Read the bytes quickly trying to detect if it needs entity or transactional
349   * enhancement.
350   */
351  private DetectEnhancement detect(ClassLoader classLoader, byte[] classfileBuffer) {
352    DetectEnhancement detect = new DetectEnhancement(classLoader, enhanceContext);
353    ClassReader cr = new ClassReader(classfileBuffer);
354    cr.accept(detect, ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES);
355    return detect;
356  }
357}