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