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