001package io.ebean.enhance.common;
002
003import io.ebean.enhance.querybean.DetectQueryBean;
004
005import java.io.IOException;
006import java.io.InputStream;
007import java.net.URL;
008import java.util.*;
009import java.util.jar.Attributes;
010import java.util.jar.Manifest;
011
012import static io.ebean.enhance.asm.Opcodes.*;
013
014/**
015 * Reads all the META-INF/ebean.mf and META-INF/ebean-generated-info.mf resources with the locations
016 * of all the entity beans (and hence locations of query beans).
017 */
018public final class AgentManifest {
019
020  private final Set<Integer> classLoaderIdentities = new HashSet<>();
021  private final List<String> loadedResources = new ArrayList<>();
022  private final Set<String> entityPackages = new HashSet<>();
023  private final Set<String> transactionalPackages = new HashSet<>();
024  private final Set<String> querybeanPackages = new HashSet<>();
025  private final DetectQueryBean detectQueryBean;
026  private int debugLevel = -1;
027  private boolean allowNullableDbArray;
028  private boolean transientInternalFields;
029  private boolean transientInit;
030  private boolean transientInitThrowError;
031  private boolean unsupportedInitThrowError = true;
032  private boolean checkNullManyFields = true;
033  private boolean enableProfileLocation = true;
034  private boolean enableEntityFieldAccess;
035  /**
036   * Default the mode to NONE for consistency with existing behavior. Long term preference is AUTO.
037   */
038  private EnhanceContext.ProfileLineNumberMode profileLineNumberMode = EnhanceContext.ProfileLineNumberMode.NONE;
039  private boolean synthetic = true;
040  private int enhancementVersion;
041
042  public AgentManifest(ClassLoader classLoader) {
043    this.detectQueryBean = new DetectQueryBean();
044    readManifest(classLoader);
045  }
046
047  public AgentManifest() {
048    this.detectQueryBean = new DetectQueryBean();
049  }
050
051  /**
052   * Return true if more entity packages were loaded.
053   */
054  public boolean readManifest(ClassLoader classLoader) {
055    if (classLoader == null) {
056      return false;
057    }
058    final int loaderIdentity = System.identityHashCode(classLoader);
059    if (classLoaderIdentities.add(loaderIdentity)) {
060      try {
061        int beforeSize = entityPackages.size();
062        readEbeanVersion(classLoader, "META-INF/ebean-version.mf");
063        readManifests(classLoader, "META-INF/ebean-generated-info.mf");
064        readManifests(classLoader, "META-INF/ebean.mf");
065        readManifests(classLoader, "ebean.mf");
066        int afterSize = entityPackages.size();
067        if (afterSize > beforeSize) {
068          detectQueryBean.addAll(entityPackages);
069          return true;
070        }
071      } catch (IOException e) {
072        // log to standard error and return empty
073        System.err.println("Agent: error reading ebean manifest resources");
074        e.printStackTrace();
075      }
076    }
077    return false;
078  }
079
080  @Override
081  public String toString() {
082    return "entityPackages:" + entityPackages + " querybeanPackages:" + querybeanPackages
083      + " transactionalPackages:" + transactionalPackages;
084  }
085
086  public boolean isDetectEntityBean(String owner) {
087    return detectQueryBean.isEntityBean(owner);
088  }
089
090  public boolean isDetectQueryBean(String owner) {
091    return detectQueryBean.isQueryBean(owner);
092  }
093
094  public int enhancementVersion() {
095    return enhancementVersion;
096  }
097
098  /**
099   * Return true if enhancement of profileLocations should be added.
100   */
101  public boolean isEnableProfileLocation() {
102    return enableProfileLocation;
103  }
104
105  public boolean isEnableEntityFieldAccess() {
106    return enableEntityFieldAccess;
107  }
108
109  public EnhanceContext.ProfileLineNumberMode profileLineMode() {
110    return profileLineNumberMode;
111  }
112
113  /**
114   * Return the debug level read from ebean.mf
115   */
116  public int debugLevel() {
117    return debugLevel;
118  }
119
120  /**
121   * Return the paths that manifests were loaded from.
122   */
123  public List<String> loadedResources() {
124    return loadedResources;
125  }
126
127  /**
128   * Return the parsed set of packages that type query beans are in.
129   */
130  public Set<String> entityPackages() {
131    return entityPackages;
132  }
133
134  /**
135   * Return true if transactional enhancement is turned off.
136   */
137  public boolean isTransactionalNone() {
138    return transactionalPackages.contains("none") && transactionalPackages.size() == 1;
139  }
140
141  /**
142   * Return true if enhancement should add initialisation of transient fields when adding a default constructor.
143   */
144  public boolean isTransientInit() {
145    return transientInit;
146  }
147
148  /**
149   * Return true if an error should be thrown when adding a default constructor and transient fields can't be initialised.
150   */
151  public boolean isTransientInitThrowError() {
152    return transientInitThrowError;
153  }
154
155  public boolean isUnsupportedInitThrowError() {
156    return unsupportedInitThrowError;
157  }
158
159  /**
160   * Return true if we should use transient internal fields.
161   */
162  public boolean isTransientInternalFields() {
163    return transientInternalFields;
164  }
165
166  /**
167   * Return false if enhancement should skip checking for null many fields.
168   */
169  public boolean isCheckNullManyFields() {
170    return checkNullManyFields;
171  }
172
173  public boolean isAllowNullableDbArray() {
174    return allowNullableDbArray;
175  }
176
177  public int accPublic() {
178    return synthetic ? (ACC_PUBLIC + ACC_SYNTHETIC) : ACC_PUBLIC;
179  }
180
181  public int accProtected() {
182    return synthetic ? (ACC_PROTECTED + ACC_SYNTHETIC) : ACC_PROTECTED;
183  }
184
185  public int accPrivate() {
186    return synthetic ? (ACC_PRIVATE + ACC_SYNTHETIC) : ACC_PRIVATE;
187  }
188
189  /**
190   * Return true if query bean enhancement is turned off.
191   */
192  public boolean isQueryBeanNone() {
193    return querybeanPackages.contains("none") && querybeanPackages.size() == 1;
194  }
195
196  /**
197   * Return the packages that should be enhanced for transactional.
198   * An empty set means all packages are scanned for transaction classes and methods.
199   */
200  public Set<String> transactionalPackages() {
201    return transactionalPackages;
202  }
203
204  /**
205   * Return the packages that should be enhanced for query bean use.
206   * An empty set means all packages are scanned for transaction classes and methods.
207   */
208  public Set<String> querybeanPackages() {
209    return querybeanPackages;
210  }
211
212  protected void readEbeanVersion(ClassLoader classLoader, String path) throws IOException {
213    Enumeration<URL> resources = classLoader.getResources(path);
214    while (resources.hasMoreElements()) {
215      URL url = resources.nextElement();
216      try {
217        final Manifest manifest = manifest(UrlHelper.openNoCache(url));
218        final String value = manifest.getMainAttributes().getValue("ebean-version");
219        if (value != null) {
220          enhancementVersion = Integer.parseInt(value.trim());
221          if (enhancementVersion > 141) {
222            // default these to true for ebean version 13.12.0 or higher
223            allowNullableDbArray = true;
224            transientInit = true;
225            transientInitThrowError = true;
226          }
227        }
228        loadedResources.add(path);
229      } catch (Exception e) {
230        System.err.println("Error reading manifest resources " + url);
231        e.printStackTrace();
232      }
233    }
234  }
235
236  /**
237   * Read all the specific manifest files and return the set of packages containing type query beans.
238   */
239  void readManifests(ClassLoader classLoader, String path) throws IOException {
240    Enumeration<URL> resources = classLoader.getResources(path);
241    while (resources.hasMoreElements()) {
242      URL url = resources.nextElement();
243      try {
244        addResource(UrlHelper.openNoCache(url));
245        loadedResources.add(path);
246      } catch (IOException e) {
247        System.err.println("Error reading manifest resources " + url);
248        e.printStackTrace();
249      }
250    }
251  }
252
253  /**
254   * Add given the manifest InputStream.
255   */
256  private void addResource(InputStream is) throws IOException {
257    addManifest(manifest(is));
258  }
259
260  private Manifest manifest(InputStream is) throws IOException {
261    try {
262      return new Manifest(is);
263    } finally {
264      try {
265        is.close();
266      } catch (IOException e) {
267        System.err.println("Error closing manifest resource");
268        e.printStackTrace();
269      }
270    }
271  }
272
273  private void readProfilingMode(Attributes attributes) {
274    String debug = attributes.getValue("debug");
275    if (debug != null) {
276      debugLevel = Integer.parseInt(debug);
277    }
278    String locationMode = attributes.getValue("profile-location");
279    if (locationMode != null) {
280      enableProfileLocation = Boolean.parseBoolean(locationMode);
281    }
282    String profileWithLine = attributes.getValue("profile-line-number-mode");
283    if (profileWithLine != null) {
284      try {
285        profileLineNumberMode = EnhanceContext.ProfileLineNumberMode.valueOf(profileWithLine.toUpperCase().trim());
286      } catch (Exception e) {
287        e.printStackTrace();
288      }
289    }
290    String fieldAccessMode = attributes.getValue("entity-field-access");
291    if (fieldAccessMode != null) {
292      enableEntityFieldAccess = Boolean.parseBoolean(fieldAccessMode);
293    }
294    String syntheticOption = attributes.getValue("synthetic");
295    if (syntheticOption != null) {
296      synthetic = Boolean.parseBoolean(syntheticOption);
297    }
298  }
299
300  private void addManifest(Manifest manifest) {
301    Attributes attributes = manifest.getMainAttributes();
302    readProfilingMode(attributes);
303    readOptions(attributes);
304
305    add(entityPackages, attributes.getValue("packages"));
306    add(entityPackages, attributes.getValue("entity-packages"));
307    add(transactionalPackages, attributes.getValue("transactional-packages"));
308    add(querybeanPackages, attributes.getValue("querybean-packages"));
309
310    final String topPackages = attributes.getValue("top-packages");
311    if (topPackages != null) {
312      add(transactionalPackages, topPackages);
313      add(querybeanPackages, topPackages);
314    }
315  }
316
317  private void readOptions(Attributes attributes) {
318    transientInit = bool("transient-init", transientInit, attributes);
319    transientInitThrowError = bool("transient-init-error", transientInit, attributes);
320    transientInternalFields = bool("transient-internal-fields", transientInternalFields, attributes);
321    checkNullManyFields = bool("check-null-many-fields", checkNullManyFields, attributes);
322    allowNullableDbArray = bool("allow-nullable-dbarray", allowNullableDbArray, attributes);
323    unsupportedInitThrowError = bool("unsupported-init-error", unsupportedInitThrowError, attributes);
324  }
325
326  private boolean bool(String key, boolean defaultValue, Attributes attributes) {
327    String val = attributes.getValue(key);
328    return val != null ? Boolean.parseBoolean(val) : defaultValue;
329  }
330
331  /**
332   * Collect each individual package splitting by delimiters.
333   */
334  private void add(Set<String> addTo, String packages) {
335    if (packages != null) {
336      String[] split = packages.split("[,; ]");
337      for (String aSplit : split) {
338        String pkg = aSplit.trim();
339        if (!pkg.isEmpty()) {
340          addTo.add(pkg);
341        }
342      }
343    }
344  }
345}