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.collect.ImmutableList;
9 import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
10 import org.apache.commons.io.FileUtils;
11 import org.dom4j.Document;
12 import org.dom4j.DocumentException;
13 import org.dom4j.Element;
14 import org.dom4j.io.SAXReader;
15 import org.slf4j.Logger;
16 import org.slf4j.LoggerFactory;
17 import org.twdata.pkgscanner.DefaultOsgiVersionConverter;
18 import org.twdata.pkgscanner.ExportPackage;
19 import org.twdata.pkgscanner.PackageScanner;
20
21 import javax.servlet.ServletContext;
22 import java.io.File;
23 import java.io.IOException;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.Deque;
30 import java.util.HashMap;
31 import java.util.LinkedList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.StringTokenizer;
35 import java.util.jar.Attributes;
36 import java.util.jar.JarFile;
37 import java.util.jar.Manifest;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40
41 import static com.atlassian.plugin.osgi.container.felix.ExportBuilderUtils.copyUnlessExist;
42 import static com.atlassian.plugin.osgi.container.felix.ExportBuilderUtils.parseExportFile;
43 import static com.google.common.collect.Lists.newArrayList;
44 import static org.twdata.pkgscanner.PackageScanner.exclude;
45 import static org.twdata.pkgscanner.PackageScanner.include;
46 import static org.twdata.pkgscanner.PackageScanner.jars;
47 import static org.twdata.pkgscanner.PackageScanner.packages;
48
49
50
51
52 class ExportsBuilder {
53
54 static final String JDK8_PACKAGES_PATH = "jdk8-packages.txt";
55 static final String JDK9_PACKAGES_PATH = "jdk9-packages.txt";
56 static final String JDK11_PACKAGES_PATH = "jdk11-packages.txt";
57
58 private static final List<String> FRAMEWORK_PACKAGES = ImmutableList.of(
59 "com.atlassian.plugin.remotable",
60
61 "com.atlassian.plugin.cache.filecache",
62 "com.atlassian.plugin.webresource",
63 "com.atlassian.plugin.web"
64 );
65 private static final String OSGI_PACKAGES_PATH = "osgi-packages.txt";
66
67
68
69
70
71
72 private static final Pattern PATTERN_JAR_FILE_URL = Pattern.compile("jar:(file:.+\\.jar)!/");
73 private static final Logger log = LoggerFactory.getLogger(ExportsBuilder.class);
74
75
76 static String getLegacyScanModeProperty() {
77 return "com.atlassian.plugin.export.legacy.scan.mode";
78 }
79
80 private static String exportStringCache;
81
82 public interface CachedExportPackageLoader {
83 Collection<ExportPackage> load();
84 }
85
86 private final CachedExportPackageLoader cachedExportPackageLoader;
87
88 ExportsBuilder() {
89 this(new PackageScannerExportsFileLoader("package-scanner-exports.xml"));
90 }
91
92 ExportsBuilder(final CachedExportPackageLoader loader) {
93 this.cachedExportPackageLoader = loader;
94 }
95
96
97
98
99
100
101
102
103
104
105
106 @VisibleForTesting
107 static URL maybeUnwrapJarFileUrl(final URL url) {
108 final Matcher matcher = PATTERN_JAR_FILE_URL.matcher(url.toString());
109 if (matcher.matches()) {
110 final String fileUrl = matcher.group(1);
111 log.debug("Unwrapped Spring Boot URL: {} -> {}", url, fileUrl);
112
113 try {
114 return new URL(fileUrl);
115 } catch (final MalformedURLException e) {
116 log.warn("Could not create URL from apparent Spring Boot jar:file: {}->{}", url, fileUrl);
117 }
118 }
119
120 return url;
121 }
122
123 @VisibleForTesting
124 static boolean isPluginFrameworkPackage(String pkg) {
125 return pkg.startsWith("com.atlassian.plugin.") &&
126 FRAMEWORK_PACKAGES.stream().noneMatch(
127 frameworkPackage -> pkg.equals(frameworkPackage) || pkg.startsWith(frameworkPackage + "."));
128 }
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 String getExports(final List<HostComponentRegistration> regs, final PackageScannerConfiguration packageScannerConfig) {
144 if (exportStringCache == null) {
145 exportStringCache = determineExports(regs, packageScannerConfig);
146 }
147 return exportStringCache;
148 }
149
150
151
152
153
154
155
156 void clearExportCache() {
157 exportStringCache = null;
158 }
159
160
161
162
163
164
165
166
167 String determineExports(final List<HostComponentRegistration> regs, final PackageScannerConfiguration packageScannerConfig) {
168 final Map<String, String> exportPackages = new HashMap<>();
169
170
171 copyUnlessExist(exportPackages, parseExportFile(OSGI_PACKAGES_PATH));
172
173
174 copyUnlessExist(exportPackages, parseExportFile(getJdkPackagesPath()));
175
176
177 final Collection<ExportPackage> scannedPackages = generateExports(packageScannerConfig);
178 copyUnlessExist(exportPackages, ExportBuilderUtils.toMap(scannedPackages));
179
180
181 try {
182 final Map<String, String> referredPackages = OsgiHeaderUtil.findReferredPackageVersions(
183 regs, packageScannerConfig.getPackageVersions());
184 copyUnlessExist(exportPackages, referredPackages);
185 } catch (final IOException ex) {
186 log.error("Unable to calculate necessary exports based on host components", ex);
187 }
188
189
190 enforceFrameworkVersion(exportPackages);
191
192
193 final String exports = OsgiHeaderUtil.generatePackageVersionString(exportPackages);
194
195 if (log.isDebugEnabled()) {
196 log.debug("Exports:\n" + exports.replaceAll(",", "\r\n"));
197 }
198
199 return exports;
200 }
201
202 private void enforceFrameworkVersion(final Map<String, String> exportPackages) {
203 final String frameworkVersion = PluginFrameworkUtils.getPluginFrameworkVersion();
204
205
206 final DefaultOsgiVersionConverter converter = new DefaultOsgiVersionConverter();
207 final String frameworkVersionOsgi = converter.getVersion(frameworkVersion);
208
209 exportPackages.keySet().stream()
210 .filter(ExportsBuilder::isPluginFrameworkPackage)
211 .forEach(pkg -> exportPackages.put(pkg, frameworkVersionOsgi));
212 }
213
214 Collection<ExportPackage> generateExports(final PackageScannerConfiguration packageScannerConfig) {
215 final String[] arrType = new String[0];
216
217 final Map<String, String> pkgVersions = new HashMap<>(packageScannerConfig.getPackageVersions());
218
219 final String javaxServletPattern = "javax.servlet*";
220 final ServletContext servletContext = packageScannerConfig.getServletContext();
221 if ((null == pkgVersions.get(javaxServletPattern)) && (null != servletContext)) {
222 final String servletVersion = servletContext.getMajorVersion() + "." + servletContext.getMinorVersion();
223 pkgVersions.put(javaxServletPattern, servletVersion);
224 }
225
226 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
227 final PackageScanner scanner = new PackageScanner()
228 .useClassLoader(contextClassLoader)
229 .select(
230 jars(
231 include(packageScannerConfig.getJarIncludes().toArray(arrType)),
232 exclude(packageScannerConfig.getJarExcludes().toArray(arrType))),
233 packages(
234 include(packageScannerConfig.getPackageIncludes().toArray(arrType)),
235 exclude(packageScannerConfig.getPackageExcludes().toArray(arrType)))
236 )
237 .withMappings(pkgVersions);
238
239 if (log.isDebugEnabled()) {
240 scanner.enableDebug();
241 }
242
243 Collection<ExportPackage> exports = cachedExportPackageLoader.load();
244 if (exports == null) {
245 final boolean legacyMode = Boolean.getBoolean(getLegacyScanModeProperty());
246 if (legacyMode) {
247
248
249
250 exports = scanner.scan();
251 } else {
252 final URL[] urls = getClassPathUrls(contextClassLoader);
253 exports = scanner.scan(urls);
254 }
255 }
256 log.info("Package scan completed. Found " + exports.size() + " packages to export.");
257
258 if (packageScanFailed(exports) && servletContext != null) {
259 log.warn("Unable to find expected packages via classloader scanning. Trying ServletContext scanning...");
260 try {
261 exports = scanner.scan(servletContext.getResource("/WEB-INF/lib"), servletContext.getResource("/WEB-INF/classes"));
262 } catch (final MalformedURLException e) {
263 log.warn("Unable to scan webapp for packages", e);
264 }
265 }
266
267 if (packageScanFailed(exports)) {
268 throw new IllegalStateException("Unable to find required packages via classloader or servlet context"
269 + " scanning, most likely due to an application server bug.");
270 }
271 return exports;
272 }
273
274 private URL[] getClassPathUrls(ClassLoader contextClassLoader) {
275
276
277 final Deque<URL> loaderUrls = new LinkedList<>();
278
279 new FastClasspathScanner("")
280 .addClassLoader(contextClassLoader)
281 .disableRecursiveScanning()
282 .getUniqueClasspathElementURLs()
283 .forEach(loaderUrls::push);
284
285
286
287
288
289
290
291 final List<URL> allUrls = new ArrayList<>();
292 while (!loaderUrls.isEmpty()) {
293 final URL url = maybeUnwrapJarFileUrl(loaderUrls.pop());
294 try {
295 final File file = FileUtils.toFile(url);
296 if (null == file) {
297 log.warn("Cannot deep scan non file '{}'", url);
298 } else if (!file.exists()) {
299
300 log.debug("Cannot deep scan missing file '{}'", url);
301 } else if (file.isDirectory()) {
302
303 allUrls.add(url);
304 } else if (file.isFile() && file.getName().endsWith(".jar")) {
305
306 allUrls.add(url);
307
308 final JarFile jar = new JarFile(file);
309 collectClassPath(loaderUrls, url, jar);
310 } else {
311
312
313 log.debug("Skipping deep scan of non jar-file ");
314 }
315 } catch (final Exception exception) {
316
317
318
319
320
321
322
323
324 log.warn("Failed to deep scan '{}'", url, exception);
325 }
326 }
327 return allUrls.toArray(new URL[0]);
328 }
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344 private void collectClassPath(final Deque<URL> loaderUrls, final URL url, final JarFile jar) throws IOException {
345 final Manifest manifest = jar.getManifest();
346 if (null == manifest) {
347 log.debug("Missing manifest prevents deep scan of '{}'", url);
348 return;
349 }
350
351 final String classPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
352 if (null != classPath) {
353 final StringTokenizer tokenizer = new StringTokenizer(classPath);
354 while (tokenizer.hasMoreTokens()) {
355 final String classPathEntry = tokenizer.nextToken();
356 try {
357 loaderUrls.push(new URL(url, classPathEntry));
358 log.debug("Deep scan found url '{}'", loaderUrls.peekFirst());
359 } catch (final MalformedURLException emu) {
360
361 log.warn("Cannot deep scan unparseable Class-Path entry '{}' in '{}'", url, classPath);
362 }
363 }
364 }
365
366 }
367
368 private String getJdkPackagesPath() {
369 String versionString = System.getProperty("java.specification.version");
370 if (versionString == null) {
371 versionString = System.getProperty("java.version", "11");
372 }
373 if (versionString.startsWith("1.")) {
374 versionString = versionString.substring(2);
375 }
376 int version = 0;
377 for (char c : versionString.toCharArray()) {
378 if (Character.isDigit(c)) {
379 version = 10 * version + Character.digit(c, 10);
380 }
381 }
382
383 if (version >= 11) {
384 return JDK11_PACKAGES_PATH;
385 }
386 if (version >= 9) {
387 return JDK9_PACKAGES_PATH;
388 }
389 return JDK8_PACKAGES_PATH;
390 }
391
392
393
394
395
396
397
398 private static boolean packageScanFailed(final Collection<ExportPackage> exports) {
399 return exports.stream().noneMatch(export -> export.getPackageName().equals("org.slf4j"));
400 }
401
402 static class PackageScannerExportsFileLoader implements CachedExportPackageLoader {
403 private final String path;
404
405 PackageScannerExportsFileLoader(final String path) {
406 this.path = path;
407 }
408
409 @Override
410 public Collection<ExportPackage> load() {
411 final URL exportsUrl = getClass().getClassLoader().getResource(path);
412 if (exportsUrl != null) {
413 log.debug("Precalculated exports found, loading...");
414 final List<ExportPackage> result = newArrayList();
415 try {
416 final Document doc = new SAXReader().read(exportsUrl);
417
418
419 for (final Element export : ((List<Element>) doc.getRootElement().elements())) {
420 final String packageName = export.attributeValue("package");
421 final String version = export.attributeValue("version");
422 final String location = export.attributeValue("location");
423
424 if (packageName == null || location == null) {
425 log.warn("Invalid configuration: package({}) and location({}) are required, " +
426 "aborting precalculated exports and reverting to normal scanning",
427 packageName, location);
428 return Collections.emptyList();
429 }
430 result.add(new ExportPackage(packageName, version, new File(location)));
431 }
432 log.debug("Loaded {} precalculated exports", result.size());
433
434 return result;
435 } catch (final DocumentException e) {
436 log.warn("Unable to load exports from " + path + " due to malformed XML", e);
437 }
438 }
439 log.debug("No precalculated exports found");
440 return null;
441 }
442 }
443 }