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