View Javadoc

1   package com.atlassian.plugin.osgi.container.felix;
2   
3   import com.atlassian.plugin.osgi.container.PackageScannerConfiguration;
4   import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
5   import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
6   import com.atlassian.plugin.util.PluginFrameworkUtils;
7   import com.google.common.annotations.VisibleForTesting;
8   import com.google.common.base.Predicate;
9   import com.google.common.collect.Sets;
10  import org.dom4j.Document;
11  import org.dom4j.DocumentException;
12  import org.dom4j.Element;
13  import org.dom4j.io.SAXReader;
14  import org.slf4j.Logger;
15  import org.slf4j.LoggerFactory;
16  import org.twdata.pkgscanner.DefaultOsgiVersionConverter;
17  import org.twdata.pkgscanner.ExportPackage;
18  import org.twdata.pkgscanner.PackageScanner;
19  
20  import static com.atlassian.plugin.osgi.container.felix.ExportBuilderUtils.parseExportFile;
21  import static com.atlassian.plugin.osgi.container.felix.ExportBuilderUtils.copyUnlessExist;
22  import static com.google.common.collect.Iterables.any;
23  import static com.google.common.collect.Lists.newArrayList;
24  import static org.twdata.pkgscanner.PackageScanner.exclude;
25  import static org.twdata.pkgscanner.PackageScanner.include;
26  import static org.twdata.pkgscanner.PackageScanner.jars;
27  import static org.twdata.pkgscanner.PackageScanner.packages;
28  
29  import javax.annotation.Nullable;
30  import javax.servlet.ServletContext;
31  import java.io.File;
32  import java.io.IOException;
33  import java.net.MalformedURLException;
34  import java.net.URL;
35  import java.util.*;
36  
37  /**
38   * Builds the OSGi package exports string.  Uses a file to cache the scanned results, keyed by the application version.
39   */
40  class ExportsBuilder
41  {
42      static final String JDK_6 = "1.6";
43      static final String JDK_7 = "1.7";
44  
45      static final String OSGI_PACKAGES_PATH = "osgi-packages.txt";
46      static final String JDK_PACKAGES_PATH = "jdk-packages.txt";
47  
48      private static Logger log = LoggerFactory.getLogger(ExportsBuilder.class);
49      private static String exportStringCache;
50  
51      @VisibleForTesting
52      static final Predicate<String> UNDER_PLUGIN_FRAMEWORK = new Predicate<String>()
53      {
54          private Iterable<String> packagesNotInPlugins = newArrayList(
55                  "com.atlassian.plugin.remotable",
56                  // webfragments and webresources moved out in PLUG-942 and PLUG-943
57                  "com.atlassian.plugin.cache.filecache",
58                  "com.atlassian.plugin.webresource",
59                  "com.atlassian.plugin.web"
60          );
61  
62          public boolean apply(final String pkg)
63          {
64              Predicate<String> underPackage = new Predicate<String>() {
65                  @Override
66                  public boolean apply(@Nullable String input) {
67                      return pkg.equals(input) || pkg.startsWith(input + ".");
68                  }
69              };
70              return pkg.startsWith("com.atlassian.plugin.") && !any(packagesNotInPlugins, underPackage);
71          }
72      };
73  
74      public static interface CachedExportPackageLoader
75      {
76          Collection<ExportPackage> load();
77      }
78  
79      private final CachedExportPackageLoader cachedExportPackageLoader;
80  
81      public ExportsBuilder()
82      {
83          this(new PackageScannerExportsFileLoader("package-scanner-exports.xml"));
84      }
85  
86      public ExportsBuilder(CachedExportPackageLoader loader)
87      {
88          this.cachedExportPackageLoader = loader;
89      }
90      /**
91       * Gets the framework exports taking into account host components and package scanner configuration.
92       * <p>
93       * Often, this information will not change without a system restart, so we determine this once and then cache the value.
94       * The cache is only useful if the plugin system is thrown away and re-initialised. This is done thousands of times
95       * during JIRA functional testing, and the cache was added to speed this up.
96       *
97       * If needed, call {@link #clearExportCache()} to clear the cache.
98       *
99       * @param regs The list of host component registrations
100      * @param packageScannerConfig The configuration for the package scanning
101      * @return A list of exports, in a format compatible with OSGi headers
102      */
103     public String getExports(List<HostComponentRegistration> regs, PackageScannerConfiguration packageScannerConfig)
104     {
105         if (exportStringCache == null)
106         {
107             exportStringCache = determineExports(regs, packageScannerConfig);
108         }
109         return exportStringCache;
110     }
111 
112     /**
113      * Clears the export string cache. This results in {@link #getExports(java.util.List, com.atlassian.plugin.osgi.container.PackageScannerConfiguration)}
114      * having to recalculate the export string next time which can significantly slow down the start up time of plugin framework.
115      * @since 2.9.0
116      */
117     public void clearExportCache()
118     {
119         exportStringCache = null;
120     }
121 
122     /**
123      * Determines framework exports taking into account host components and package scanner configuration.
124      *
125      * @param regs The list of host component registrations
126      * @param packageScannerConfig The configuration for the package scanning
127      * @param cacheDir No longer used. (method deprecated).
128      * @return A list of exports, in a format compatible with OSGi headers
129      * @deprecated Please use {@link #getExports}. Deprecated since 2.3.6
130      */
131     @SuppressWarnings ({ "UnusedDeclaration" })
132     public String determineExports(List<HostComponentRegistration> regs, PackageScannerConfiguration packageScannerConfig, File cacheDir)
133     {
134         return determineExports(regs, packageScannerConfig);
135     }
136 
137     /**
138      * Determines framework exports taking into account host components and package scanner configuration.
139      *
140      * @param regs The list of host component registrations
141      * @param packageScannerConfig The configuration for the package scanning
142      * @return A list of exports, in a format compatible with OSGi headers
143      */
144     String determineExports(List<HostComponentRegistration> regs, PackageScannerConfiguration packageScannerConfig)
145     {
146         Map<String, String> exportPackages = new HashMap<String, String>();
147 
148         // The first part is osgi related packages.
149         copyUnlessExist(exportPackages, parseExportFile(OSGI_PACKAGES_PATH));
150 
151         // The second part is JDK packages.
152         copyUnlessExist(exportPackages, parseExportFile(JDK_PACKAGES_PATH));
153 
154         // Third part by scanning packages available via classloader. The versions are determined by jar names.
155         Collection<ExportPackage> scannedPackages = generateExports(packageScannerConfig);
156         copyUnlessExist(exportPackages, ExportBuilderUtils.toMap(scannedPackages));
157 
158         // Fourth part by scanning host components since all the classes referred to by them must be available to consumers.
159         try
160         {
161             Map<String,String> referredPackages = OsgiHeaderUtil.findReferredPackageVersions(regs, packageScannerConfig.getPackageVersions());
162             copyUnlessExist(exportPackages, referredPackages);
163         }
164         catch (IOException ex)
165         {
166             log.error("Unable to calculate necessary exports based on host components", ex);
167         }
168 
169         // All the packages under plugin framework namespace must be exported as the plugin framework's version.
170         enforceFrameworkVersion(exportPackages);
171 
172         // Generate the actual export string in OSGi spec.
173         final String exports = OsgiHeaderUtil.generatePackageVersionString(exportPackages);
174 
175         if (log.isDebugEnabled())
176         {
177             log.debug("Exports:\n"+exports.replaceAll(",", "\r\n"));
178         }
179 
180         return exports;
181     }
182 
183     private void enforceFrameworkVersion(Map<String, String> exportPackages)
184     {
185         final String frameworkVersion = PluginFrameworkUtils.getPluginFrameworkVersion();
186 
187         // convert the version to OSGi format.
188         DefaultOsgiVersionConverter converter = new DefaultOsgiVersionConverter();
189         final String frameworkVersionOsgi = converter.getVersion(frameworkVersion);
190 
191         for(String pkg: Sets.filter(exportPackages.keySet(), UNDER_PLUGIN_FRAMEWORK))
192         {
193             exportPackages.put(pkg, frameworkVersionOsgi);
194         }
195     }
196 
197     Collection<ExportPackage> generateExports(PackageScannerConfiguration packageScannerConfig)
198     {
199         String[] arrType = new String[0];
200 
201         Map<String,String> pkgVersions = new HashMap<String,String>(packageScannerConfig.getPackageVersions());
202         if (packageScannerConfig.getServletContext() != null)
203         {
204             String ver = packageScannerConfig.getServletContext().getMajorVersion() + "." + packageScannerConfig.getServletContext().getMinorVersion();
205             pkgVersions.put("javax.servlet*", ver);
206         }
207 
208         PackageScanner scanner = new PackageScanner()
209            .select(
210                jars(
211                        include(packageScannerConfig.getJarIncludes().toArray(arrType)),
212                        exclude(packageScannerConfig.getJarExcludes().toArray(arrType))),
213                packages(
214                        include(packageScannerConfig.getPackageIncludes().toArray(arrType)),
215                        exclude(packageScannerConfig.getPackageExcludes().toArray(arrType)))
216            )
217            .withMappings(pkgVersions);
218 
219         if (log.isDebugEnabled())
220         {
221             scanner.enableDebug();
222         }
223 
224         Collection<ExportPackage> exports = cachedExportPackageLoader.load();
225         if (exports == null)
226         {
227             exports = scanner.scan();
228         }
229         log.info("Package scan completed. Found " + exports.size() + " packages to export.");
230 
231         if (!isPackageScanSuccessful(exports) && packageScannerConfig.getServletContext() != null)
232         {
233             log.warn("Unable to find expected packages via classloader scanning.  Trying ServletContext scanning...");
234             ServletContext ctx = packageScannerConfig.getServletContext();
235             try
236             {
237                 exports = scanner.scan(ctx.getResource("/WEB-INF/lib"), ctx.getResource("/WEB-INF/classes"));
238             }
239             catch (MalformedURLException e)
240             {
241                 log.warn("Unable to scan webapp for packages", e);
242             }
243         }
244 
245         if (!isPackageScanSuccessful(exports))
246         {
247             throw new IllegalStateException("Unable to find required packages via classloader or servlet context"
248                     + " scanning, most likely due to an application server bug.");
249         }
250         return exports;
251     }
252 
253     /**
254      * Tests to see if a scan of packages to export was successful, using the presence of slf4j as the criteria.
255      *
256      * @param exports The exports found so far
257      * @return True if slf4j is present, false otherwise
258      */
259     private static boolean isPackageScanSuccessful(Collection<ExportPackage> exports)
260     {
261         boolean slf4jFound = false;
262         for (ExportPackage export : exports)
263         {
264             if (export.getPackageName().equals("org.slf4j"))
265             {
266                 slf4jFound = true;
267                 break;
268             }
269         }
270         return slf4jFound;
271     }
272 
273     static class PackageScannerExportsFileLoader implements CachedExportPackageLoader
274     {
275         private final String path;
276 
277         public PackageScannerExportsFileLoader(String path)
278         {
279             this.path = path;
280         }
281 
282         @Override
283         public Collection<ExportPackage> load()
284         {
285             URL exportsUrl = getClass().getClassLoader().getResource(path);
286             if (exportsUrl != null)
287             {
288                 log.debug("Precalculated exports found, loading...");
289                 List<ExportPackage> result = newArrayList();
290                 try
291                 {
292                     Document doc = new SAXReader().read(exportsUrl);
293                     for (Element export : ((List<Element>)doc.getRootElement().elements()))
294                     {
295                         String packageName = export.attributeValue("package");
296                         String version = export.attributeValue("version");
297                         String location = export.attributeValue("location");
298 
299                         if (packageName == null || location == null)
300                         {
301                             log.warn("Invalid configuration: package({}) and location({}) are required, " +
302                                     "aborting precalculated exports and reverting to normal scanning",
303                                     packageName, location);
304                             return Collections.emptyList();
305                         }
306                         result.add(new ExportPackage(packageName, version, new File(location)));
307                     }
308                     log.debug("Loaded {} precalculated exports", result.size());
309 
310                     return result;
311                 }
312                 catch (DocumentException e)
313                 {
314                     log.warn("Unable to load exports from " + path + " due to malformed XML", e);
315                 }
316             }
317             log.debug("No precalculated exports found");
318             return null;
319         }
320     }
321 }