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