View Javadoc
1   package com.atlassian.plugin.osgi.util;
2   
3   import aQute.bnd.header.OSGiHeader;
4   import aQute.bnd.osgi.Analyzer;
5   import aQute.bnd.osgi.Clazz;
6   import com.atlassian.annotations.Internal;
7   import com.atlassian.plugin.PluginArtifact;
8   import com.atlassian.plugin.PluginInformation;
9   import com.atlassian.plugin.PluginParseException;
10  import com.atlassian.plugin.PluginPermission;
11  import com.atlassian.plugin.osgi.factory.OsgiPlugin;
12  import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
13  import com.atlassian.plugin.osgi.util.ClassBinaryScanner.InputStreamResource;
14  import com.atlassian.plugin.osgi.util.ClassBinaryScanner.ScanResult;
15  import com.atlassian.plugin.util.ClassLoaderUtils;
16  import com.atlassian.plugin.util.ClassUtils;
17  import com.google.common.collect.ImmutableMap;
18  import com.google.common.collect.ImmutableSet;
19  import org.apache.commons.io.IOUtils;
20  import org.osgi.framework.Bundle;
21  import org.osgi.framework.Constants;
22  import org.osgi.framework.Version;
23  import org.slf4j.Logger;
24  import org.slf4j.LoggerFactory;
25  
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.util.Arrays;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.HashMap;
34  import java.util.HashSet;
35  import java.util.Iterator;
36  import java.util.LinkedHashMap;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.Set;
40  import java.util.function.Predicate;
41  import java.util.jar.JarFile;
42  import java.util.jar.Manifest;
43  import java.util.stream.Collectors;
44  
45  import static com.atlassian.plugin.osgi.factory.transform.JarUtils.closeQuietly;
46  import static com.google.common.base.Preconditions.checkArgument;
47  import static com.google.common.base.Preconditions.checkNotNull;
48  
49  /**
50   * Utilities to help create OSGi headers
51   */
52  public class OsgiHeaderUtil {
53      static Logger log = LoggerFactory.getLogger(OsgiHeaderUtil.class);
54      private static final String EMPTY_OSGI_VERSION = Version.emptyVersion.toString();
55      private static final String STAR_PACKAGE = "*";
56      private static final String DUPLICATE_PACKAGE_SUFFIX = "~";
57  
58      /**
59       * Finds all referred packages for the specified set of classes/interfaces by scanning their bytecode.
60       * Packages starting with "java." are ignored.
61       *
62       * @param classes The set of classes/interfaces to scan
63       * @return The set of referred packages
64       * @throws IOException If there are any problems scanning bytecode
65       * @since 5.0.0
66       */
67      public static Set<String> findReferredPackageNames(Collection<Class<?>> classes) throws IOException {
68          if (classes == null || classes.isEmpty()) {
69              return Collections.emptySet();
70          }
71  
72          Set<Class> classesToScan = new HashSet<>();
73          for (Class<?> clazz : classes) {
74              ClassUtils.findAllTypes(clazz, classesToScan);
75          }
76  
77          Set<String> referredClasses = new HashSet<>();
78          Set<String> referredPackages = new HashSet<>();
79          for (Class inf : classesToScan) {
80              String clsName = inf.getName().replace('.', '/') + ".class";
81              crawlReferenceTree(clsName, referredClasses, referredPackages, 1);
82          }
83  
84          return ImmutableSet.copyOf(referredPackages);
85      }
86  
87      /**
88       * Finds all referred packages for host component registrations by scanning their declared interfaces' bytecode.
89       *
90       * @param registrations A list of host component registrations
91       * @return The referred package map ( package-&gt; version ).
92       * @throws IOException If there are any problems scanning bytecode
93       * @since 2.7.0
94       */
95      public static Map<String, String> findReferredPackageVersions(List<HostComponentRegistration> registrations,
96                                                                    Map<String, String> packageVersions) throws IOException {
97          if (registrations == null || registrations.isEmpty()) {
98              return Collections.emptyMap();
99          }
100 
101         Set<Class<?>> declaredInterfaces = registrations.stream()
102                 .flatMap(reg -> Arrays.stream(reg.getMainInterfaceClasses()))
103                 .collect(Collectors.toSet());
104         return matchPackageVersions(findReferredPackageNames(declaredInterfaces), packageVersions);
105     }
106 
107     static Map<String, String> matchPackageVersions(Set<String> packageNames, Map<String, String> packageVersions) {
108         Map<String, String> output = new HashMap<>();
109 
110         for (String pkg : packageNames) {
111             String version = packageVersions.get(pkg);
112 
113             String effectiveValue = EMPTY_OSGI_VERSION;
114 
115             if (version != null) {
116                 try {
117                     Version.parseVersion(version);
118                     effectiveValue = version;
119                 } catch (IllegalArgumentException ex) {
120                     log.info("Unable to parse version: " + version);
121                 }
122             }
123             output.put(pkg, effectiveValue);
124         }
125 
126         return ImmutableMap.copyOf(output);
127     }
128 
129     /**
130      * Helps filter "java." packages.
131      */
132     private static final Predicate<String> JAVA_PACKAGE_FILTER = pkg -> !pkg.startsWith("java.");
133 
134     /**
135      * Helps filter class entries under "java." packages.
136      */
137     private static final Predicate<String> JAVA_CLASS_FILTER = classEntry -> !classEntry.startsWith("java/");
138 
139     /**
140      * This will crawl the class interfaces to the desired level.
141      *
142      * @param className      name of the class.
143      * @param scannedClasses set of classes that have been scanned.
144      * @param packageImports set of imports that have been found.
145      * @param level          depth of scan (recursion).
146      * @throws IOException error loading a class.
147      */
148     static void crawlReferenceTree(String className, Set<String> scannedClasses, Set<String> packageImports, int level) throws IOException {
149         if (level <= 0) {
150             return;
151         }
152 
153         if (className.startsWith("java/"))
154             return;
155 
156         if (scannedClasses.contains(className))
157             return;
158         else
159             scannedClasses.add(className);
160 
161         if (log.isDebugEnabled())
162             log.debug("Crawling " + className);
163 
164         InputStream in = ClassLoaderUtils.getResourceAsStream(className, OsgiHeaderUtil.class);
165         if (in == null) {
166             log.error("Cannot find class: [" + className + "]");
167             return;
168         }
169         try (InputStreamResource classBinaryResource = new InputStreamResource(in)) {
170             // look for the class binary by asking class loader.
171 
172             // read the class binary and scan it.
173             final ScanResult scanResult = ClassBinaryScanner.scanClassBinary(new Clazz(new Analyzer(), className, classBinaryResource));
174 
175             // remember all the imported packages. ignore java packages.
176             scanResult.getReferredPackages().stream().filter(JAVA_PACKAGE_FILTER).forEach(packageImports::add);
177 
178             // crawl
179             Set<String> referredClasses = scanResult.getReferredClasses().stream()
180                     .filter(JAVA_CLASS_FILTER)
181                     .collect(Collectors.toSet());
182             for (String ref : referredClasses) {
183                 crawlReferenceTree(ref + ".class", scannedClasses, packageImports, level - 1);
184             }
185         }
186     }
187 
188 
189     /**
190      * Parses an OSGi header line into a map structure
191      *
192      * @param header The header line
193      * @return A map with the key the entry value and the value a map of attributes
194      * @since 2.2.0
195      */
196     public static Map<String, Map<String, String>> parseHeader(String header) {
197         return new LinkedHashMap<>(OSGiHeader.parseHeader(header).asMapMap());
198     }
199 
200     /**
201      * Builds the header string from a map
202      *
203      * @param values The header values
204      * @return A string, suitable for inclusion into an OSGI header string
205      * @since 2.6
206      */
207     public static String buildHeader(Map<String, Map<String, String>> values) {
208         StringBuilder header = new StringBuilder();
209         for (Iterator<Map.Entry<String, Map<String, String>>> i = values.entrySet().iterator(); i.hasNext(); ) {
210             Map.Entry<String, Map<String, String>> entry = i.next();
211             buildHeader(entry.getKey(), entry.getValue(), header);
212             if (i.hasNext()) {
213                 header.append(",");
214             }
215         }
216         return header.toString();
217     }
218 
219     /**
220      * Builds the header string from a map
221      *
222      * @param key   The header value
223      * @param attrs The map of attributes
224      * @return A string, suitable for inclusion into an OSGI header string
225      * @since 2.2.0
226      */
227     public static String buildHeader(String key, Map<String, String> attrs) {
228         StringBuilder fullPkg = new StringBuilder();
229         buildHeader(key, attrs, fullPkg);
230         return fullPkg.toString();
231     }
232 
233     /**
234      * Builds the header string from a map
235      *
236      * @since 2.6
237      */
238     private static void buildHeader(String key, Map<String, String> attrs, StringBuilder builder) {
239         builder.append(key);
240         if (attrs != null && !attrs.isEmpty()) {
241             for (Map.Entry<String, String> entry : attrs.entrySet()) {
242                 builder.append(";");
243                 builder.append(entry.getKey());
244                 builder.append("=\"");
245                 builder.append(entry.getValue());
246                 builder.append("\"");
247             }
248         }
249     }
250 
251     /**
252      * Gets the plugin key from the bundle
253      *
254      * WARNING: shamelessly copied at {@link com.atlassian.plugin.osgi.bridge.PluginBundleUtils}, which can't use
255      * this class due to creating a cyclic build dependency. Ensure these two implementations are in sync.
256      *
257      * This method shouldn't be used directly. Instead consider consuming the {@link com.atlassian.plugin.osgi.bridge.external.PluginRetrievalService}.
258      *
259      * @param bundle The plugin bundle
260      * @return The plugin key, cannot be null
261      * @since 2.2.0
262      */
263     public static String getPluginKey(Bundle bundle) {
264         return getPluginKey(
265                 bundle.getSymbolicName(),
266                 bundle.getHeaders().get(OsgiPlugin.ATLASSIAN_PLUGIN_KEY),
267                 bundle.getHeaders().get(Constants.BUNDLE_VERSION)
268         );
269     }
270 
271     /**
272      * Obtain the plugin key for a bundle in the given File.
273      *
274      * @param file the file containing the bundle.
275      * @return the pluginKey, or null if the bundle is malformed.
276      */
277     public static String getPluginKey(final File file) {
278         JarFile jar = null;
279         try {
280             jar = new JarFile(file);
281             final Manifest manifest = jar.getManifest();
282             if (manifest != null) {
283                 return getPluginKey(manifest);
284             }
285         } catch (final IOException eio) {
286             log.warn("Cannot read jar file '" + file + "': " + eio.getMessage());
287         } finally {
288             closeQuietly(jar);
289         }
290         return null;
291     }
292 
293     /**
294      * Gets the plugin key from the jar manifest
295      *
296      * @param mf The plugin jar manifest
297      * @return The plugin key, cannot be null
298      * @since 2.2.0
299      */
300     public static String getPluginKey(Manifest mf) {
301         return getPluginKey(
302                 getAttributeWithoutValidation(mf, Constants.BUNDLE_SYMBOLICNAME),
303                 getAttributeWithoutValidation(mf, OsgiPlugin.ATLASSIAN_PLUGIN_KEY),
304                 getAttributeWithoutValidation(mf, Constants.BUNDLE_VERSION)
305         );
306     }
307 
308     private static String getPluginKey(Object bundleName, Object atlKey, Object version) {
309         Object key = atlKey;
310         if (key == null) {
311             String bName = bundleName.toString();
312             final int scPos = bName.indexOf(';');
313             if (scPos > -1) {
314                 bName = bName.substring(0, scPos);
315             }
316             key = bName + "-" + version;
317         }
318 
319         return key.toString();
320     }
321 
322     /**
323      * Generate package version string such as "com.abc;version=1.2,com.atlassian".
324      * The output can be used for import or export.
325      *
326      * @param packages map of packagename -&t; version.
327      */
328     public static String generatePackageVersionString(Map<String, String> packages) {
329         if (packages == null || packages.size() == 0) {
330             return "";
331         }
332 
333         final StringBuilder sb = new StringBuilder();
334 
335         // add deterministism to string generation.
336         List<String> packageNames = new ArrayList<>(packages.keySet());
337         Collections.sort(packageNames);
338 
339         for (String packageName : packageNames) {
340             sb.append(",");
341             sb.append(packageName);
342 
343             String version = packages.get(packageName);
344 
345             // we can drop the version component if it's empty for a slight performance gain.
346             if (version != null && !version.equals(EMPTY_OSGI_VERSION)) {
347                 sb.append(";version=").append(version);
348             }
349         }
350 
351         // delete the initial ",".
352         sb.delete(0, 1);
353 
354         return sb.toString();
355     }
356 
357     /**
358      * Extract the attribute from manifest and validate it's not null and not empty
359      *
360      * @return value for the matching attribute key or will raise an Exception in case constraints are not met
361      * @throws NullPointerException     if attribute value is null
362      * @throws IllegalArgumentException if attribute is empty
363      */
364     public static String getValidatedAttribute(final Manifest manifest, String key) {
365         String value = getAttributeWithoutValidation(manifest, key);
366         checkNotNull(value);
367         checkArgument(!value.isEmpty());
368         return value;
369     }
370 
371     /**
372      * Extract the attribute from manifest and validate if it's not empty
373      *
374      * @return value for the matching attribute key
375      * @throws IllegalArgumentException if attribute value is empty
376      */
377     public static String getNonEmptyAttribute(final Manifest manifest, String key) {
378         String attributeWithoutValidation = getAttributeWithoutValidation(manifest, key);
379         checkArgument(!attributeWithoutValidation.isEmpty());
380         return attributeWithoutValidation;
381     }
382 
383     /**
384      * Extract the attribute from manifest(no-validations)
385      *
386      * @return value for the matching attribute key
387      */
388     public static String getAttributeWithoutValidation(final Manifest manifest, String key) {
389         return manifest.getMainAttributes().getValue(key);
390     }
391 
392     /**
393      * Extract a PluginInformation from the Manifest of a jar containing an OSGi bundle.
394      *
395      * @param manifest the manifest to parse.
396      * @return the parsed PluginInformation.
397      */
398     @Internal
399     public static PluginInformation extractOsgiPluginInformation(final Manifest manifest, final boolean requireVersion) {
400         final String bundleVersion = requireVersion
401                 ? getValidatedAttribute(manifest, Constants.BUNDLE_VERSION)
402                 : getAttributeWithoutValidation(manifest, Constants.BUNDLE_VERSION);
403         final String bundleVendor = getAttributeWithoutValidation(manifest, Constants.BUNDLE_VENDOR);
404         final String bundleDescription = getAttributeWithoutValidation(manifest, Constants.BUNDLE_DESCRIPTION);
405         final PluginInformation pluginInformation = new PluginInformation();
406         pluginInformation.setVersion(bundleVersion);
407         pluginInformation.setDescription(bundleDescription);
408         pluginInformation.setVendorName(bundleVendor);
409         // OSGi plugins require execute Java.
410         pluginInformation.setPermissions(ImmutableSet.of(PluginPermission.EXECUTE_JAVA));
411         return pluginInformation;
412     }
413 
414     /**
415      * Extract the manifest of a PluginArtifact.
416      *
417      * @param pluginArtifact the plugin artifact who manifest is required.
418      * @return the manifest from pluginArtifact, or null if the manifest is missing, cannot be read, or is corrupted.
419      */
420     public static Manifest getManifest(final PluginArtifact pluginArtifact) {
421         try {
422             final InputStream manifestStream = pluginArtifact.getResourceAsStream(JarFile.MANIFEST_NAME);
423             if (manifestStream != null) {
424                 try {
425                     return new Manifest(manifestStream);
426                 } catch (final IOException eio) {
427                     log.error("Cannot read manifest from plugin artifact '" + pluginArtifact.getName() + "': " + eio.getMessage());
428                 } finally {
429                     IOUtils.closeQuietly(manifestStream);
430                 }
431             }
432         } catch (final PluginParseException epp) {
433             log.error("Cannot get manifest resource from plugin artifact '" + pluginArtifact.getName() + "': " + epp.getMessage());
434         }
435         return null;
436     }
437 
438     /**
439      * Reorder any Import-Package or Export-Package or Private-Package value passed, so that packages containing a
440      * star-value i.e. '*' to the end, preserving their current order.
441      * <p>
442      * Note that any reordered packages will be logged at debug level.
443      *
444      * @param packages  map of package declarations, keyed by package name
445      * @param pluginKey only used for debug logging
446      * @return copy of packages with star packages at the end
447      */
448     public static Map<String, Map<String, String>> moveStarPackageToEnd(final Map<String, Map<String, String>> packages, final String pluginKey) {
449         final Map<String, Map<String, String>> orderedPkgs = new LinkedHashMap<>();
450         final Map<String, Map<String, String>> starPkgs = new LinkedHashMap<>();
451 
452         // separate into packages containing a '*' and packages without
453         for (Map.Entry<String, Map<String, String>> pkg : packages.entrySet()) {
454             if (pkg.getKey().contains(STAR_PACKAGE)) {
455                 starPkgs.put(pkg.getKey(), pkg.getValue());
456             } else {
457                 orderedPkgs.put(pkg.getKey(), pkg.getValue());
458             }
459         }
460 
461         // put '*' package at the end in order
462         for (Map.Entry<String, Map<String, String>> starPkg : starPkgs.entrySet()) {
463             log.debug("moving {} package to end for plugin {}", starPkg.getKey(), pluginKey);
464             orderedPkgs.put(starPkg.getKey(), starPkg.getValue());
465         }
466 
467         return orderedPkgs;
468     }
469 
470     /**
471      * Remove any duplicate Import-Package or Export-Package or Private-Package value passed. Duplicates are packages
472      * that end with a ~. Order is preserved.
473      * <p>
474      * Note that any removed packages will be logged at warning level.
475      *
476      * @param packages  map of package declarations, keyed by package name
477      * @param pluginKey only used for debug logging
478      * @param action    only used for debug logging - "import", "export" etc.
479      * @return de-duplicated copy of packages
480      */
481     public static Map<String, Map<String, String>> stripDuplicatePackages(final Map<String, Map<String, String>> packages, final String pluginKey, final String action) {
482         final Map<String, Map<String, String>> deduplicatedPackages = new LinkedHashMap<>();
483 
484         // add each package, skipping duplicates
485         for (Map.Entry<String, Map<String, String>> pkg : packages.entrySet()) {
486             if (pkg.getKey().endsWith(DUPLICATE_PACKAGE_SUFFIX)) {
487                 log.warn("removing duplicate {} package {} for plugin {} - it is likely that a duplicate package was supplied in the OSGi instructions in the plugin's MANIFEST.MF",
488                         action, pkg.getKey(), pluginKey);
489             } else {
490                 deduplicatedPackages.put(pkg.getKey(), pkg.getValue());
491             }
492         }
493 
494         return deduplicatedPackages;
495     }
496 }