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
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
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
114
115
116
117
118
119
120
121
122
123
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
136
137
138
139 public void clearExportCache()
140 {
141 exportStringCache = null;
142 }
143
144
145
146
147
148
149
150
151
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
163
164
165
166
167
168 String determineExports(final List<HostComponentRegistration> regs, final PackageScannerConfiguration packageScannerConfig)
169 {
170 final Map<String, String> exportPackages = new HashMap<String, String>();
171
172
173 copyUnlessExist(exportPackages, parseExportFile(OSGI_PACKAGES_PATH));
174
175
176 copyUnlessExist(exportPackages, parseExportFile(JDK_PACKAGES_PATH));
177
178
179 final Collection<ExportPackage> scannedPackages = generateExports(packageScannerConfig);
180 copyUnlessExist(exportPackages, ExportBuilderUtils.toMap(scannedPackages));
181
182
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
195 enforceFrameworkVersion(exportPackages);
196
197
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
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
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
261
262
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
297
298
299
300
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
315 log.warn("ignoring non-URLClassLoader {}", classLoader.getClass());
316 }
317 }
318
319
320 return urlClassLoaders.build().reverse();
321 }
322
323
324
325
326
327
328
329
330
331
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
339
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
354 log.debug("Cannot deep scan missing file '{}'", url);
355 }
356 else if (file.isDirectory())
357 {
358
359 allUrls.add(url);
360 }
361 else if (file.isFile() && file.getName().endsWith(".jar"))
362 {
363
364 allUrls.add(url);
365
366 final JarFile jar = new JarFile(file);
367 collectClassPath(loaderUrls, url, jar);
368 }
369 else
370 {
371
372
373 log.debug("Skipping deep scan of non jar-file ");
374 }
375 }
376 catch (final Exception exception)
377 {
378
379
380
381
382
383
384
385
386 log.warn("Failed to deep scan '{}'", url, exception);
387 }
388 }
389 }
390 return allUrls.toArray(new URL[allUrls.size()]);
391 }
392
393
394
395
396
397
398
399
400
401
402
403
404
405
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
431 log.warn("Cannot deep scan unparseable Class-Path entry '{}' in '{}'", url, classPath);
432 }
433 }
434 }
435
436 }
437
438
439
440
441
442
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
479
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 }