View Javadoc

1   package com.atlassian.plugin.osgi.container.felix;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.net.MalformedURLException;
6   import java.net.URL;
7   import java.net.URLClassLoader;
8   import java.util.ArrayDeque;
9   import java.util.ArrayList;
10  import java.util.Collection;
11  import java.util.Collections;
12  import java.util.Deque;
13  import java.util.HashMap;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.StringTokenizer;
17  import java.util.jar.Attributes;
18  import java.util.jar.JarFile;
19  import java.util.jar.Manifest;
20  
21  import javax.annotation.Nullable;
22  import javax.servlet.ServletContext;
23  
24  import com.atlassian.plugin.osgi.container.PackageScannerConfiguration;
25  import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
26  import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
27  import com.atlassian.plugin.util.PluginFrameworkUtils;
28  
29  import com.google.common.annotations.VisibleForTesting;
30  import com.google.common.base.Predicate;
31  import com.google.common.collect.ImmutableList;
32  import com.google.common.collect.Sets;
33  
34  import org.apache.commons.io.FileUtils;
35  import org.dom4j.Document;
36  import org.dom4j.DocumentException;
37  import org.dom4j.Element;
38  import org.dom4j.io.SAXReader;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  import org.twdata.pkgscanner.DefaultOsgiVersionConverter;
42  import org.twdata.pkgscanner.ExportPackage;
43  import org.twdata.pkgscanner.PackageScanner;
44  
45  import static com.atlassian.plugin.osgi.container.felix.ExportBuilderUtils.copyUnlessExist;
46  import static com.atlassian.plugin.osgi.container.felix.ExportBuilderUtils.parseExportFile;
47  import static com.google.common.collect.Iterables.any;
48  import static com.google.common.collect.Lists.newArrayList;
49  import static org.twdata.pkgscanner.PackageScanner.exclude;
50  import static org.twdata.pkgscanner.PackageScanner.include;
51  import static org.twdata.pkgscanner.PackageScanner.jars;
52  import static org.twdata.pkgscanner.PackageScanner.packages;
53  
54  /**
55   * Builds the OSGi package exports string.  Uses a file to cache the scanned results, keyed by the application version.
56   */
57  class ExportsBuilder
58  {
59      static final String JDK_6 = "1.6";
60      static final String JDK_7 = "1.7";
61  
62      static final String OSGI_PACKAGES_PATH = "osgi-packages.txt";
63      static final String JDK_PACKAGES_PATH = "jdk-packages.txt";
64  
65      public static String getLegacyScanModeProperty()
66      {
67          return "com.atlassian.plugin.export.legacy.scan.mode";
68      }
69  
70      private static Logger log = LoggerFactory.getLogger(ExportsBuilder.class);
71      private static String exportStringCache;
72  
73      @VisibleForTesting
74      static final Predicate<String> UNDER_PLUGIN_FRAMEWORK = new Predicate<String>()
75      {
76          private Iterable<String> packagesNotInPlugins = newArrayList(
77                  "com.atlassian.plugin.remotable",
78                  // webfragments and webresources moved out in PLUG-942 and PLUG-943
79                  "com.atlassian.plugin.cache.filecache",
80                  "com.atlassian.plugin.webresource",
81                  "com.atlassian.plugin.web"
82          );
83  
84          public boolean apply(final String pkg)
85          {
86              final Predicate<String> underPackage = new Predicate<String>() {
87                  @Override
88                  public boolean apply(@Nullable final String input) {
89                      return pkg.equals(input) || pkg.startsWith(input + ".");
90                  }
91              };
92              return pkg.startsWith("com.atlassian.plugin.") && !any(packagesNotInPlugins, underPackage);
93          }
94      };
95  
96      public static interface CachedExportPackageLoader
97      {
98          Collection<ExportPackage> load();
99      }
100 
101     private final CachedExportPackageLoader cachedExportPackageLoader;
102 
103     public ExportsBuilder()
104     {
105         this(new PackageScannerExportsFileLoader("package-scanner-exports.xml"));
106     }
107 
108     public ExportsBuilder(final CachedExportPackageLoader loader)
109     {
110         this.cachedExportPackageLoader = loader;
111     }
112     /**
113      * Gets the framework exports taking into account host components and package scanner configuration.
114      * <p>
115      * Often, this information will not change without a system restart, so we determine this once and then cache the value.
116      * The cache is only useful if the plugin system is thrown away and re-initialised. This is done thousands of times
117      * during JIRA functional testing, and the cache was added to speed this up.
118      *
119      * If needed, call {@link #clearExportCache()} to clear the cache.
120      *
121      * @param regs The list of host component registrations
122      * @param packageScannerConfig The configuration for the package scanning
123      * @return A list of exports, in a format compatible with OSGi headers
124      */
125     public String getExports(final List<HostComponentRegistration> regs, final PackageScannerConfiguration packageScannerConfig)
126     {
127         if (exportStringCache == null)
128         {
129             exportStringCache = determineExports(regs, packageScannerConfig);
130         }
131         return exportStringCache;
132     }
133 
134     /**
135      * Clears the export string cache. This results in {@link #getExports(java.util.List, com.atlassian.plugin.osgi.container.PackageScannerConfiguration)}
136      * having to recalculate the export string next time which can significantly slow down the start up time of plugin framework.
137      * @since 2.9.0
138      */
139     public void clearExportCache()
140     {
141         exportStringCache = null;
142     }
143 
144     /**
145      * Determines framework exports taking into account host components and package scanner configuration.
146      *
147      * @param regs The list of host component registrations
148      * @param packageScannerConfig The configuration for the package scanning
149      * @param cacheDir No longer used. (method deprecated).
150      * @return A list of exports, in a format compatible with OSGi headers
151      * @deprecated Please use {@link #getExports}. Deprecated since 2.3.6
152      */
153     @SuppressWarnings ({ "UnusedDeclaration" })
154     public String determineExports(final List<HostComponentRegistration> regs,
155             final PackageScannerConfiguration packageScannerConfig,
156             final File cacheDir)
157     {
158         return determineExports(regs, packageScannerConfig);
159     }
160 
161     /**
162      * Determines framework exports taking into account host components and package scanner configuration.
163      *
164      * @param regs The list of host component registrations
165      * @param packageScannerConfig The configuration for the package scanning
166      * @return A list of exports, in a format compatible with OSGi headers
167      */
168     String determineExports(final List<HostComponentRegistration> regs, final PackageScannerConfiguration packageScannerConfig)
169     {
170         final Map<String, String> exportPackages = new HashMap<String, String>();
171 
172         // The first part is osgi related packages.
173         copyUnlessExist(exportPackages, parseExportFile(OSGI_PACKAGES_PATH));
174 
175         // The second part is JDK packages.
176         copyUnlessExist(exportPackages, parseExportFile(JDK_PACKAGES_PATH));
177 
178         // Third part by scanning packages available via classloader. The versions are determined by jar names.
179         final Collection<ExportPackage> scannedPackages = generateExports(packageScannerConfig);
180         copyUnlessExist(exportPackages, ExportBuilderUtils.toMap(scannedPackages));
181 
182         // Fourth part by scanning host components since all the classes referred to by them must be available to consumers.
183         try
184         {
185             final Map<String,String> referredPackages = OsgiHeaderUtil.findReferredPackageVersions(
186                     regs, packageScannerConfig.getPackageVersions());
187             copyUnlessExist(exportPackages, referredPackages);
188         }
189         catch (final IOException ex)
190         {
191             log.error("Unable to calculate necessary exports based on host components", ex);
192         }
193 
194         // All the packages under plugin framework namespace must be exported as the plugin framework's version.
195         enforceFrameworkVersion(exportPackages);
196 
197         // Generate the actual export string in OSGi spec.
198         final String exports = OsgiHeaderUtil.generatePackageVersionString(exportPackages);
199 
200         if (log.isDebugEnabled())
201         {
202             log.debug("Exports:\n"+exports.replaceAll(",", "\r\n"));
203         }
204 
205         return exports;
206     }
207 
208     private void enforceFrameworkVersion(final Map<String, String> exportPackages)
209     {
210         final String frameworkVersion = PluginFrameworkUtils.getPluginFrameworkVersion();
211 
212         // convert the version to OSGi format.
213         final DefaultOsgiVersionConverter converter = new DefaultOsgiVersionConverter();
214         final String frameworkVersionOsgi = converter.getVersion(frameworkVersion);
215 
216         for(final String pkg: Sets.filter(exportPackages.keySet(), UNDER_PLUGIN_FRAMEWORK))
217         {
218             exportPackages.put(pkg, frameworkVersionOsgi);
219         }
220     }
221 
222     Collection<ExportPackage> generateExports(final PackageScannerConfiguration packageScannerConfig)
223     {
224         final String[] arrType = new String[0];
225 
226         final Map<String,String> pkgVersions = new HashMap<String,String>(packageScannerConfig.getPackageVersions());
227         // If the product isn't trying to override servlet, set the version to that reported by the ServletContext
228         final String javaxServletPattern = "javax.servlet*";
229         final ServletContext servletContext = packageScannerConfig.getServletContext();
230         if ((null == pkgVersions.get(javaxServletPattern)) && (null != servletContext))
231         {
232             final String servletVersion = servletContext.getMajorVersion() + "." + servletContext.getMinorVersion();
233             pkgVersions.put(javaxServletPattern, servletVersion);
234         }
235 
236         final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
237         final PackageScanner scanner = new PackageScanner()
238                 .useClassLoader(contextClassLoader)
239                 .select(
240                         jars(
241                                 include(packageScannerConfig.getJarIncludes().toArray(arrType)),
242                                 exclude(packageScannerConfig.getJarExcludes().toArray(arrType))),
243                         packages(
244                                 include(packageScannerConfig.getPackageIncludes().toArray(arrType)),
245                                 exclude(packageScannerConfig.getPackageExcludes().toArray(arrType)))
246                 )
247                 .withMappings(pkgVersions);
248 
249         if (log.isDebugEnabled())
250         {
251             scanner.enableDebug();
252         }
253 
254         Collection<ExportPackage> exports = cachedExportPackageLoader.load();
255         if (exports == null)
256         {
257             final boolean legacyMode = Boolean.getBoolean(getLegacyScanModeProperty());
258             if (legacyMode)
259             {
260                 // Legacy mode is still in used on windows bamboo agents - see https://extranet.atlassian.com/jira/browse/BDEV-8619
261                 // The issue is that the path walking code org.twdata.pkgscanner.InternalScanner#loadImplementationsInDirectory
262                 // assumes that (File.isDirectory() == true) => (File.listFiles[] != null), which is not true for windows Junctions.
263                 exports = scanner.scan();
264             }
265             else
266             {
267                 final List<URLClassLoader> urlClassLoaders = getUrlClassLoaders(contextClassLoader);
268                 final URL[] urls = getUrlClassPath(urlClassLoaders);
269                 exports = scanner.scan(urls);
270             }
271         }
272         log.info("Package scan completed. Found " + exports.size() + " packages to export.");
273 
274         if (!isPackageScanSuccessful(exports) && servletContext != null)
275         {
276             log.warn("Unable to find expected packages via classloader scanning.  Trying ServletContext scanning...");
277             try
278             {
279                 exports = scanner.scan(servletContext.getResource("/WEB-INF/lib"), servletContext.getResource("/WEB-INF/classes"));
280             }
281             catch (final MalformedURLException e)
282             {
283                 log.warn("Unable to scan webapp for packages", e);
284             }
285         }
286 
287         if (!isPackageScanSuccessful(exports))
288         {
289             throw new IllegalStateException("Unable to find required packages via classloader or servlet context"
290                     + " scanning, most likely due to an application server bug.");
291         }
292         return exports;
293     }
294 
295     /**
296      * Walk the {@link ClassLoader#getParent()} chain, returning the {@link URLClassLoader} instances found in the order
297      * they would be scanned during class loading.
298      *
299      * @param leafClassLoader the class loader to commence walking from.
300      * @return the URLClassLoader parents of this class loader
301      */
302     private static List<URLClassLoader> getUrlClassLoaders(final ClassLoader leafClassLoader)
303     {
304         final ImmutableList.Builder<URLClassLoader> urlClassLoaders = ImmutableList.builder();
305         for (ClassLoader classLoader = leafClassLoader; classLoader != null; classLoader = classLoader.getParent())
306         {
307             if (classLoader instanceof URLClassLoader)
308             {
309                 final URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
310                 urlClassLoaders.add(urlClassLoader);
311             }
312             else
313             {
314                 // This technique is probably broken if we hit non-URLClassLoaders, so warn about it
315                 log.warn("ignoring non-URLClassLoader {}", classLoader.getClass());
316             }
317         }
318         // We've collected the class loaders in leaf to root order, but we want to return them in load order, which is the
319         // reverse given class loader delegation.
320         return urlClassLoaders.build().reverse();
321     }
322 
323     /**
324      * Expand a list of URLClassLoaders to obtain all jar and directory entries accessible from them.
325      * <p/>
326      * A best effort is made to return the results in the order they are loaded by class loading. We deal only in file (jar
327      * and directory) entries, because PackageScanner only works on these anyway. For reasons inexplicable, intermittently
328      * in some environments urls such as "http://felix.extensions:9/" appear in the classpath.
329      *
330      * @param urlClassLoaders the list of class loaders to expand.
331      * @return all jars and directories which could be loaded by the given class loaders.
332      */
333     private URL[] getUrlClassPath(final List<URLClassLoader> urlClassLoaders)
334     {
335         final List<URL> allUrls = new ArrayList<URL>();
336         for (final URLClassLoader urlClassLoader : urlClassLoaders)
337         {
338             // The stack of urls we have yet to expand. We initialize the deque with the URLs in the order the class loader
339             // searches them, since the ArrayDeque implementation pops from the front.
340             final Deque<URL> loaderUrls = new ArrayDeque<URL>(ImmutableList.copyOf(urlClassLoader.getURLs()));
341             while (!loaderUrls.isEmpty())
342             {
343                 final URL url = loaderUrls.pop();
344                 try
345                 {
346                     final File file = FileUtils.toFile(url);
347                     if (null == file)
348                     {
349                         log.warn("Cannot deep scan non file '{}'", url);
350                     }
351                     else if (!file.exists())
352                     {
353                         // This is only debug worthy - jars sometimes speculatively reference optional components
354                         log.debug("Cannot deep scan missing file '{}'", url);
355                     }
356                     else if (file.isDirectory())
357                     {
358                         // A directory class path entry is fine, so collect it, but we can't scan any deeper into it
359                         allUrls.add(url);
360                     }
361                     else if (file.isFile() && file.getName().endsWith(".jar"))
362                     {
363                         // It's a jar file, so collect it ...
364                         allUrls.add(url);
365                         // ... and look for a Class-Path to expand
366                         final JarFile jar = new JarFile(file);
367                         collectClassPath(loaderUrls, url, jar);
368                     }
369                     else
370                     {
371                         // This is reasonable, for example there's JNI stuff in the class path we can't deep scan,
372                         // but a log message seems reasonable just so everything can be tracked when debugging.
373                         log.debug("Skipping deep scan of non jar-file ");
374                     }
375                 }
376                 catch (final Exception exception)
377                 {
378                     // The likely reasons for hitting this, based on inspection at the time of writing, are
379                     // 1. An unparseable URL in the Class-Path in a manifest, and
380                     // 2. An IOExcetion opening a JAR
381                     // Neither of these are expected during normal usage, nor have happened during testing, so the current
382                     // plan is to ignore them and push on. Things not defitely critically broken at this point, the likely
383                     // outcome is just that some jars didn't get scanned. It may be that we need to fallback to the mode
384                     // used when getLegacyScanModeProperty is set, but it's not yet clear what failure modes are extreme
385                     // enough to warrant this, since both the above cases have the same expected outcome in legacy mode.
386                     log.warn("Failed to deep scan '{}'", url, exception);
387                 }
388             }
389         }
390         return allUrls.toArray(new URL[allUrls.size()]);
391     }
392 
393     /**
394      * Collect URLs obtained by parsing the Manifest "Class-Path:" of the given jar.
395      * <p/>
396      * We collect the results using a Deque so we can push jars found in Manifest Class-Path: entries back on the front and
397      * search them before following jars.
398      * <p/>
399      * The expansion of the Manifest Class-Path: entries adds code complexity and may impact performance, but since surefire
400      * uses the Manifest Class-Path: to run tests in the correct class loaders, we need to handle it. See PLUGDEV-67 for
401      * more details.
402      *
403      * @param loaderUrls the stack of urls to push discovered entries on to.
404      * @param url the url of the jar used to resolve relative entries.
405      * @param jar the actual jar file whose manifest is parsed.
406      */
407     private void collectClassPath(final Deque<URL> loaderUrls, final URL url, final JarFile jar) throws IOException
408     {
409         final Manifest manifest = jar.getManifest();
410         if (null == manifest)
411         {
412             log.debug("Missing manifest prevents deep scan of '{}'", url);
413             return;
414         }
415 
416         final String classPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
417         if (null != classPath)
418         {
419             final StringTokenizer tokenizer = new StringTokenizer(classPath);
420             while (tokenizer.hasMoreTokens())
421             {
422                 final String classPathEntry = tokenizer.nextToken();
423                 try
424                 {
425                     loaderUrls.push(new URL(url, classPathEntry));
426                     log.debug("Deep scan found url '{}'", loaderUrls.peekFirst());
427                 }
428                 catch (final MalformedURLException emu)
429                 {
430                     // Classloaders silently ignore bad URLs in Class-Path, so we nonsilently ignore them
431                     log.warn("Cannot deep scan unparseable Class-Path entry '{}' in '{}'", url, classPath);
432                 }
433             }
434         }
435         // else no Class-Path: entry which is fine, there's no need to scan anything else
436     }
437 
438     /**
439      * Tests to see if a scan of packages to export was successful, using the presence of slf4j as the criteria.
440      *
441      * @param exports The exports found so far
442      * @return True if slf4j is present, false otherwise
443      */
444     private static boolean isPackageScanSuccessful(final Collection<ExportPackage> exports)
445     {
446         boolean slf4jFound = false;
447         for (final ExportPackage export : exports)
448         {
449             if (export.getPackageName().equals("org.slf4j"))
450             {
451                 slf4jFound = true;
452                 break;
453             }
454         }
455         return slf4jFound;
456     }
457 
458     static class PackageScannerExportsFileLoader implements CachedExportPackageLoader
459     {
460         private final String path;
461 
462         public PackageScannerExportsFileLoader(final String path)
463         {
464             this.path = path;
465         }
466 
467         @Override
468         public Collection<ExportPackage> load()
469         {
470             final URL exportsUrl = getClass().getClassLoader().getResource(path);
471             if (exportsUrl != null)
472             {
473                 log.debug("Precalculated exports found, loading...");
474                 final List<ExportPackage> result = newArrayList();
475                 try
476                 {
477                     final Document doc = new SAXReader().read(exportsUrl);
478                     // Our version of dom4j does not have a generic safe API
479                     //noinspection unchecked
480                     for (final Element export : ((List<Element>)doc.getRootElement().elements()))
481                     {
482                         final String packageName = export.attributeValue("package");
483                         final String version = export.attributeValue("version");
484                         final String location = export.attributeValue("location");
485 
486                         if (packageName == null || location == null)
487                         {
488                             log.warn("Invalid configuration: package({}) and location({}) are required, " +
489                                     "aborting precalculated exports and reverting to normal scanning",
490                                     packageName, location);
491                             return Collections.emptyList();
492                         }
493                         result.add(new ExportPackage(packageName, version, new File(location)));
494                     }
495                     log.debug("Loaded {} precalculated exports", result.size());
496 
497                     return result;
498                 }
499                 catch (final DocumentException e)
500                 {
501                     log.warn("Unable to load exports from " + path + " due to malformed XML", e);
502                 }
503             }
504             log.debug("No precalculated exports found");
505             return null;
506         }
507     }
508 }