001package io.ebean.enhance.common;
002
003import io.ebean.enhance.Transformer;
004import io.ebean.enhance.asm.MethodVisitor;
005import io.ebean.enhance.entity.MessageOutput;
006
007import java.io.ByteArrayOutputStream;
008import java.io.PrintStream;
009import java.util.HashMap;
010import java.util.Map;
011import java.util.Set;
012import java.util.logging.Level;
013import java.util.logging.Logger;
014
015import static io.ebean.enhance.asm.Opcodes.INVOKEINTERFACE;
016import static io.ebean.enhance.asm.Opcodes.INVOKEVIRTUAL;
017import static io.ebean.enhance.common.EnhanceConstants.C_INTERCEPT_I;
018import static io.ebean.enhance.common.EnhanceConstants.C_INTERCEPT_RW;
019
020/**
021 * Used to hold metadata, arguments and log levels for the enhancement.
022 */
023public final class EnhanceContext {
024
025  private static final Logger logger = Logger.getLogger(EnhanceContext.class.getName());
026
027  public enum ProfileLineNumberMode {
028    /**
029     * Line numbering on profile location when method + queryBean type is not unique
030     */
031    AUTO,
032    /**
033     * No line numbering on profile locations
034     */
035    NONE,
036    /**
037     * Line numbering on all profile locations
038     */
039    ALL
040  }
041
042  private final AgentManifest manifest;
043  private final IgnoreClassHelper ignoreClassHelper;
044  private final Map<String, String> agentArgsMap;
045  private final ClassMetaReader reader;
046  private final ClassBytesReader classBytesReader;
047  private MessageOutput logout;
048  private int logLevel;
049  private final HashMap<String, ClassMeta> map = new HashMap<>();
050  private final FilterEntityTransactional filterEntityTransactional;
051  private final FilterQueryBean filterQueryBean;
052  private final PackageFilter packageFilter;
053  private boolean throwOnError;
054  private final boolean enableProfileLocation;
055  private final boolean enableEntityFieldAccess;
056  private final ProfileLineNumberMode profileLineNumberMode;
057  private final int accPublic;
058  private final int accProtected;
059  private final int accPrivate;
060  private final int enhancementVersion;
061  private SummaryInfo summaryInfo;
062
063  public EnhanceContext(ClassBytesReader classBytesReader, String agentArgs, AgentManifest manifest) {
064    this(classBytesReader, agentArgs, manifest, new ClassMetaCache());
065  }
066
067  /**
068   * Construct a context for enhancement.
069   */
070  public EnhanceContext(ClassBytesReader classBytesReader, String agentArgs, AgentManifest manifest, ClassMetaCache metaCache) {
071    this.manifest = manifest;
072    this.enableProfileLocation = manifest.isEnableProfileLocation();
073    this.enableEntityFieldAccess = manifest.isEnableEntityFieldAccess();
074    this.profileLineNumberMode = manifest.profileLineMode();
075    this.accPublic = manifest.accPublic();
076    this.accProtected = manifest.accProtected();
077    this.accPrivate = manifest.accPrivate();
078    this.agentArgsMap = ArgParser.parse(agentArgs);
079    this.enhancementVersion = versionOf(manifest);
080    this.filterEntityTransactional = new FilterEntityTransactional(manifest);
081    this.filterQueryBean = new FilterQueryBean(manifest);
082    this.ignoreClassHelper = new IgnoreClassHelper();
083    this.logout = new SysoutMessageOutput(System.out);
084    this.classBytesReader = classBytesReader;
085    this.reader = new ClassMetaReader(this, metaCache);
086    this.packageFilter = initPackageFilter(agentArgsMap.get("packages"));
087
088    if (manifest.debugLevel() > -1) {
089      logLevel = manifest.debugLevel();
090    }
091    String debugValue = agentArgsMap.get("debug");
092    if (debugValue != null) {
093      try {
094        logLevel = Integer.parseInt(debugValue);
095      } catch (NumberFormatException e) {
096        logger.log(Level.WARNING, "Agent debug argument [" + debugValue + "] is not an int?");
097      }
098    }
099    if (logLevel > 0 || propertyBoolean("printversion", false)) {
100      System.out.println("ebean-agent version:" + Transformer.getVersion() + " enhancement:" + enhancementVersion + " resources:" + manifest.loadedResources());
101    }
102  }
103
104  private PackageFilter initPackageFilter(String packages) {
105    return packages == null ? null : new PackageFilter(packages);
106  }
107
108  private int versionOf(AgentManifest manifest) {
109    String ver = agentArgsMap.get("version");
110    if (ver != null) {
111      return Integer.parseInt(ver);
112    }
113    return manifest.enhancementVersion();
114  }
115
116  public void withClassLoader(ClassLoader loader) {
117    if (manifest.readManifest(loader)) {
118      if (logLevel > 1) {
119        log(null, "loaded entity packages: " + manifest.entityPackages());
120      }
121    }
122  }
123
124  public void setLogLevel(int logLevel) {
125    this.logLevel = logLevel;
126  }
127
128  @Deprecated
129  public String getPackagesSummary() {
130    return packagesSummary();
131  }
132
133  /**
134   * Return the summary of the packages controlling enhancement.
135   */
136  public String packagesSummary() {
137    return "packages entity:" + entityPackages()
138      + "  transactional:" + transactionalPackages()
139      + "  querybean:" + querybeanPackages()
140      + "  profileLocation:" + enableProfileLocation
141      + "  version:" + enhancementVersion;
142  }
143
144  public Set<String> entityPackages() {
145    return manifest.entityPackages();
146  }
147
148  public Set<String> transactionalPackages() {
149    return manifest.transactionalPackages();
150  }
151
152  public Set<String> querybeanPackages() {
153    return manifest.querybeanPackages();
154  }
155
156  public byte[] classBytes(String className, ClassLoader classLoader) {
157    return classBytesReader.getClassBytes(className, classLoader);
158  }
159
160  public boolean isEntityBean(String owner) {
161    return manifest.isDetectEntityBean(owner);
162  }
163
164  /**
165   * Return true if the owner class is a type query bean.
166   * <p>
167   * If true typically means the caller needs to change GETFIELD calls to instead invoke the generated
168   * 'property access' methods.
169   */
170  public boolean isQueryBean(String owner, ClassLoader classLoader) {
171    if (manifest.isDetectQueryBean(owner)) {
172      try {
173        final ClassMeta classMeta = reader.get(true, owner, classLoader);
174        if (classMeta == null) {
175          // For Gradle Kotlin KAPT the generate query bean bytecode
176          // isn't available to the classLoader. Just returning true.
177          return true;
178        }
179        return classMeta.isQueryBean();
180      } catch (ClassNotFoundException e) {
181        throw new RuntimeException(e);
182      }
183    }
184    return false;
185  }
186
187  /**
188   * Return a value from the entity arguments using its key.
189   */
190  private String property(String key) {
191    return agentArgsMap.get(key.toLowerCase());
192  }
193
194  private boolean propertyBoolean(String key, boolean defaultValue) {
195    String s = property(key);
196    if (s == null) {
197      return defaultValue;
198    } else {
199      return s.trim().equalsIgnoreCase("true");
200    }
201  }
202
203  public boolean isEnableEntityFieldAccess() {
204    return enableEntityFieldAccess;
205  }
206
207  /**
208   * Return true if profile location enhancement is on.
209   */
210  public boolean isEnableProfileLocation() {
211    return enableProfileLocation;
212  }
213
214  /**
215   * Return true if this class should be scanned for transactional enhancement.
216   */
217  public boolean detectEntityTransactionalEnhancement(String className) {
218    return filterEntityTransactional.detectEnhancement(className);
219  }
220
221  /**
222   * Return true if this class should be scanned for query bean enhancement.
223   */
224  public boolean detectQueryBeanEnhancement(String className) {
225    return filterQueryBean.detectEnhancement(className);
226  }
227
228  /**
229   * Return true if this class should be ignored. That is JDK classes and
230   * known libraries JDBC drivers etc can be skipped.
231   */
232  public boolean isIgnoreClass(String className) {
233    if (packageFilter != null && packageFilter.ignore(className)) {
234      return true;
235    }
236    return ignoreClassHelper.isIgnoreClass(className);
237  }
238
239  /**
240   * Change the logout to something other than system out.
241   */
242  public void setLogout(MessageOutput logout) {
243    this.logout = logout;
244  }
245
246  /**
247   * Create a new meta object for enhancing a class.
248   */
249  public ClassMeta createClassMeta() {
250    return new ClassMeta(this, logLevel, logout);
251  }
252
253  /**
254   * Read the class metadata for a super class.
255   * <p>
256   * Typically used to read meta data for inheritance hierarchy.
257   */
258  public ClassMeta superMeta(String superClassName, ClassLoader classLoader) {
259    try {
260      if (isIgnoreClass(superClassName)) {
261        return null;
262      }
263      return reader.get(false, superClassName, classLoader);
264    } catch (ClassNotFoundException e) {
265      throw new RuntimeException(e);
266    }
267  }
268
269  /**
270   * Read the class metadata for an interface.
271   * <p>
272   * Typically used to check the interface to see if it is transactional.
273   */
274  public ClassMeta interfaceMeta(String interfaceClassName, ClassLoader classLoader) {
275    try {
276      if (isIgnoreClass(interfaceClassName)) {
277        return null;
278      }
279      return reader.get(true, interfaceClassName, classLoader);
280    } catch (ClassNotFoundException e) {
281      throw new RuntimeException(e);
282    }
283  }
284
285  public void addClassMeta(ClassMeta meta) {
286    map.put(meta.className(), meta);
287  }
288
289  public ClassMeta get(String className) {
290    return map.get(className);
291  }
292
293  /**
294   * Log some debug output.
295   */
296  public void log(int level, String className, String msg) {
297    if (logLevel >= level) {
298      log(className, msg);
299    }
300  }
301
302  public void log(String className, String msg) {
303    if (className != null) {
304      msg = "cls: " + className + "  msg: " + msg;
305    }
306    logout.println("ebean-enhance> " + msg);
307  }
308
309  public boolean isLog(int level) {
310    return logLevel >= level;
311  }
312
313  /**
314   * Log an error.
315   */
316  public void log(Throwable e) {
317    e.printStackTrace(
318      new PrintStream(new ByteArrayOutputStream()) {
319        @Override
320        public void print(String message) {
321          logout.println(message);
322        }
323
324        @Override
325        public void println(String message) {
326          logout.println(message);
327        }
328      });
329  }
330
331
332  /**
333   * Return the log level.
334   */
335  public int logLevel() {
336    return logLevel;
337  }
338
339  public boolean isTransientInit() {
340    return manifest.isTransientInit();
341  }
342
343  public boolean isTransientInitThrowError() {
344    return manifest.isTransientInitThrowError();
345  }
346
347  /**
348   * Return true if internal ebean fields in entity classes should be transient.
349   */
350  public boolean isTransientInternalFields() {
351    return manifest.isTransientInternalFields();
352  }
353
354  /**
355   * Return true if we should add null checking on *ToMany fields.
356   * <p>
357   * On getting a many that is null Ebean will create an empty List, Set or Map. If it is a
358   * ManyToMany it will turn on Modify listening.
359   */
360  public boolean isCheckNullManyFields() {
361    return manifest.isCheckNullManyFields();
362  }
363
364  public boolean isAllowNullableDbArray() {
365    return manifest.isAllowNullableDbArray();
366  }
367
368  /**
369   * Return true if transform should throw exception rather than log and return null.
370   */
371  public boolean isThrowOnError() {
372    return throwOnError;
373  }
374
375  /**
376   * Set to true if you want transform to throw exceptions rather than return null.
377   */
378  public void setThrowOnError(boolean throwOnError) {
379    this.throwOnError = throwOnError;
380  }
381
382  /**
383   * Turn on the summary collection of the enhancement.
384   */
385  public void collectSummary() {
386    this.summaryInfo = new SummaryInfo(manifest.loadedResources());
387  }
388
389  /**
390   * Add the transactional enhanced class to summary information.
391   */
392  public void summaryTransactional(String className) {
393    if (summaryInfo != null) {
394      summaryInfo.addTransactional(className);
395    }
396  }
397
398  /**
399   * Add the entity enhanced class to summary information.
400   */
401  public void summaryEntity(String className) {
402    if (summaryInfo != null) {
403      summaryInfo.addEntity(className);
404    }
405  }
406
407  /**
408   * Add the query bean enhanced class to summary information.
409   */
410  public void summaryQueryBean(String className) {
411    if (summaryInfo != null) {
412      summaryInfo.addQueryBean(className);
413    }
414  }
415
416  /**
417   * Add the query bean caller enhanced class to summary information.
418   */
419  public void summaryQueryBeanCaller(String className) {
420    if (summaryInfo != null) {
421      summaryInfo.addQueryBeanCaller(className);
422    }
423  }
424
425  /**
426   * Add the enhanced class with field access replacement to summary information.
427   */
428  public void summaryFieldAccessUser(String className) {
429    if (summaryInfo != null) {
430      summaryInfo.addFieldAccessUser(className);
431    }
432  }
433
434  @Deprecated
435  public SummaryInfo getSummaryInfo() {
436    return summaryInfo();
437  }
438
439  /**
440   * Return the summary of the enhancement.
441   * <p>
442   * Note that <code>collectSummary()</code> must be called in order for summary
443   * information to be collected and returned here.
444   */
445  public SummaryInfo summaryInfo() {
446    return summaryInfo.prepare();
447  }
448
449  public int accPublic() {
450    return accPublic;
451  }
452
453  public int accProtected() {
454    return accProtected;
455  }
456
457  public int accPrivate() {
458    return accPrivate;
459  }
460
461  public boolean isToManyGetField() {
462    return enhancementVersion > 128;
463  }
464
465  public boolean isEnhancedToString() {
466    return enhancementVersion > 132;
467  }
468
469  public String interceptNew() {
470    return enhancementVersion >= 140 ? C_INTERCEPT_RW : C_INTERCEPT_I;
471  }
472
473  public void visitMethodInsnIntercept(MethodVisitor mv, String name, String desc) {
474    mv.visitMethodInsn(interceptInvoke(), C_INTERCEPT_I, name, desc, interceptIface());
475  }
476
477  private int interceptInvoke() {
478    return enhancementVersion >= 140 ? INVOKEINTERFACE : INVOKEVIRTUAL;
479  }
480
481  private boolean interceptIface() {
482    return enhancementVersion >= 140;
483  }
484
485  public boolean interceptAddReadOnly() {
486    return enhancementVersion >= 141;
487  }
488
489  public boolean supportsProfileWithLine() {
490    return enhancementVersion >= 143; // Ebean 13.13.1 onwards
491  }
492
493  public boolean improvedQueryBeans() {
494    return enhancementVersion >= 145;
495  }
496
497  public boolean fluidQueryBuilders() {
498    return enhancementVersion >= 148;
499  }
500
501  public ProfileLineNumberMode profileLineMode() {
502    return profileLineNumberMode;
503  }
504}