View Javadoc

1   package com.atlassian.plugin.osgi.util;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.util.ArrayList;
7   import java.util.Collections;
8   import java.util.HashMap;
9   import java.util.HashSet;
10  import java.util.Iterator;
11  import java.util.LinkedHashMap;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.Set;
15  import java.util.jar.JarFile;
16  import java.util.jar.Manifest;
17  
18  import com.atlassian.annotations.Internal;
19  import com.atlassian.plugin.PluginArtifact;
20  import com.atlassian.plugin.PluginInformation;
21  import com.atlassian.plugin.PluginParseException;
22  import com.atlassian.plugin.PluginPermission;
23  import com.atlassian.plugin.osgi.factory.OsgiPlugin;
24  import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
25  import com.atlassian.plugin.osgi.util.ClassBinaryScanner.InputStreamResource;
26  import com.atlassian.plugin.osgi.util.ClassBinaryScanner.ScanResult;
27  import com.atlassian.plugin.util.ClassLoaderUtils;
28  import com.atlassian.plugin.util.ClassUtils;
29  
30  import com.google.common.base.Predicate;
31  import com.google.common.collect.ImmutableMap;
32  import com.google.common.collect.ImmutableSet;
33  import com.google.common.collect.Sets;
34  
35  import org.apache.commons.io.IOUtils;
36  import org.osgi.framework.Bundle;
37  import org.osgi.framework.Constants;
38  import org.osgi.framework.Version;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import aQute.lib.osgi.Clazz;
43  import aQute.libg.header.OSGiHeader;
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  {
54      static Logger log = LoggerFactory.getLogger(OsgiHeaderUtil.class);
55      private static final String EMPTY_OSGI_VERSION = Version.emptyVersion.toString();
56      private static final String STAR_PACKAGE = "*";
57  
58      /**
59       * Finds all referred packages for host component registrations by scanning their declared interfaces' bytecode.
60       * Packages starting with "java." are ignored.
61       *
62       * @param registrations A list of host component registrations
63       * @return The set of referred packages..
64       * @throws IOException If there are any problems scanning bytecode
65       * @since 2.7.0
66       */
67      public static Set<String> findReferredPackageNames(List<HostComponentRegistration> registrations) throws IOException
68      {
69          return findReferredPackagesInternal(registrations);
70      }
71  
72      /**
73       * Finds all referred packages for host component registrations by scanning their declared interfaces' bytecode.
74       *
75       * @param registrations A list of host component registrations
76       * @return The referred package map ( package-> version ).
77       * @throws IOException If there are any problems scanning bytecode
78       * @since 2.7.0
79       */
80      public static Map<String, String> findReferredPackageVersions(List<HostComponentRegistration> registrations, Map<String, String> packageVersions) throws IOException
81      {
82          Set<String> referredPackages = findReferredPackagesInternal(registrations);
83          return matchPackageVersions(referredPackages, packageVersions);
84      }
85  
86      /**
87       * Finds all referred packages for host component registrations by scanning their declared interfaces' bytecode.
88       *
89       * @param registrations A list of host component registrations
90       * @return The referred packages in a format compatible with an OSGi header
91       * @throws IOException If there are any problems scanning bytecode
92       * @since 2.4.0
93       * @deprecated Since 2.7.0, use {@link #findReferredPackageNames(java.util.List)} instead.
94       */
95      @Deprecated
96      public static String findReferredPackages(List<HostComponentRegistration> registrations) throws IOException
97      {
98          return findReferredPackages(registrations, Collections.<String, String>emptyMap());
99      }
100 
101     /**
102      * Finds all referred packages for host component registrations by scanning their declared interfaces' bytecode.
103      *
104      * @param registrations A list of host component registrations
105      * @return The referred packages in a format compatible with an OSGi header
106      * @throws IOException If there are any problems scanning bytecode
107      * @deprecated Since 2.7.0, use {@link #findReferredPackageVersions(java.util.List, java.util.Map)} instead.
108      */
109     @Deprecated
110     public static String findReferredPackages(List<HostComponentRegistration> registrations, Map<String, String> packageVersions) throws IOException
111     {
112         StringBuffer sb = new StringBuffer();
113         Set<String> referredPackages = new HashSet<String>();
114         Set<String> referredClasses = new HashSet<String>();
115         if (registrations == null)
116         {
117             sb.append(",");
118         }
119         else
120         {
121             for (HostComponentRegistration reg : registrations)
122             {
123                 Set<Class> classesToScan = new HashSet<Class>();
124 
125                 // Make sure we scan all extended interfaces as well
126                 for (Class inf : reg.getMainInterfaceClasses())
127                     ClassUtils.findAllTypes(inf, classesToScan);
128 
129                 for (Class inf : classesToScan)
130                 {
131                     String clsName = inf.getName().replace('.','/')+".class";
132                     crawlReferenceTree(clsName, referredClasses, referredPackages, 1);
133                 }
134             }
135             for (String pkg : referredPackages)
136             {
137                 String version = packageVersions.get(pkg);
138                 sb.append(pkg);
139                 if (version != null) {
140                     try {
141                         Version.parseVersion(version);
142                         sb.append(";version=").append(version);
143                     } catch (IllegalArgumentException ex) {
144                         log.info("Unable to parse version: "+version);
145                     }
146                 }
147                 sb.append(",");
148             }
149         }
150         return sb.toString();
151     }
152 
153     static Map<String, String> matchPackageVersions(Set<String> packageNames, Map<String, String> packageVersions)
154     {
155         Map<String, String> output = new HashMap<String, String>();
156 
157         for (String pkg : packageNames)
158         {
159             String version = packageVersions.get(pkg);
160 
161             String effectiveKey = pkg;
162             String effectiveValue = EMPTY_OSGI_VERSION;
163 
164             if (version != null)
165             {
166                 try
167                 {
168                     Version.parseVersion(version);
169                     effectiveValue = version;
170                 }
171                 catch (IllegalArgumentException ex)
172                 {
173                     log.info("Unable to parse version: "+version);
174                 }
175             }
176             output.put(effectiveKey, effectiveValue);
177         }
178 
179         return ImmutableMap.copyOf(output);
180     }
181 
182     static Set<String> findReferredPackagesInternal(List<HostComponentRegistration> registrations) throws IOException
183     {
184         final Set<String> referredPackages = new HashSet<String>();
185         final Set<String> referredClasses = new HashSet<String>();
186 
187         if (registrations != null)
188         {
189             for (HostComponentRegistration reg : registrations)
190             {
191                 Set<Class> classesToScan = new HashSet<Class>();
192 
193                 // Make sure we scan all extended interfaces as well
194                 for (Class inf : reg.getMainInterfaceClasses())
195                 {
196                     ClassUtils.findAllTypes(inf, classesToScan);
197                 }
198 
199                 for (Class inf : classesToScan)
200                 {
201                     String clsName = inf.getName().replace('.','/')+".class";
202                     crawlReferenceTree(clsName, referredClasses, referredPackages, 1);
203                 }
204             }
205         }
206 
207         return ImmutableSet.copyOf(referredPackages);
208     }
209 
210     /**
211      * Helps filter "java." packages.
212      */
213     private static final Predicate<String> JAVA_PACKAGE_FILTER = new Predicate<String>()
214     {
215         public boolean apply(String pkg)
216         {
217             return !pkg.startsWith("java.");
218         }
219     };
220 
221     /**
222      * Helps filter class entries under "java." packages.
223      */
224     private static final Predicate<String> JAVA_CLASS_FILTER = new Predicate<String>()
225     {
226         public boolean apply(String classEntry)
227         {
228             return !classEntry.startsWith("java/");
229         }
230     };
231 
232     /**
233      * This will crawl the class interfaces to the desired level.
234      *
235      * @param className name of the class.
236      * @param scannedClasses set of classes that have been scanned.
237      * @param packageImports set of imports that have been found.
238      * @param level depth of scan (recursion).
239      * @throws IOException error loading a class.
240      */
241     static void crawlReferenceTree(String className, Set<String> scannedClasses, Set<String> packageImports, int level) throws IOException
242     {
243         if (level <= 0)
244         {
245             return;
246         }
247 
248         if (className.startsWith("java/"))
249             return;
250 
251         if (scannedClasses.contains(className))
252             return;
253         else
254             scannedClasses.add(className);
255 
256         if (log.isDebugEnabled())
257             log.debug("Crawling "+className);
258 
259         InputStreamResource classBinaryResource = null;
260         try
261         {
262             // look for the class binary by asking class loader.
263             InputStream in = ClassLoaderUtils.getResourceAsStream(className, OsgiHeaderUtil.class);
264             if (in == null)
265             {
266                 log.error("Cannot find class: [" + className + "]");
267                 return;
268             }
269 
270             // read the class binary and scan it.
271             classBinaryResource = new InputStreamResource(in);
272             final ScanResult scanResult = ClassBinaryScanner.scanClassBinary(new Clazz(className, classBinaryResource));
273 
274             // remember all the imported packages. ignore java packages.
275             packageImports.addAll(Sets.filter(scanResult.getReferredPackages(), JAVA_PACKAGE_FILTER));
276 
277             // crawl
278             Set<String> referredClasses = Sets.filter(scanResult.getReferredClasses(), JAVA_CLASS_FILTER);
279             for (String ref : referredClasses)
280             {
281                 crawlReferenceTree(ref + ".class", scannedClasses, packageImports, level-1);
282             }
283         }
284         finally
285         {
286             if (classBinaryResource != null)
287             {
288                 classBinaryResource.close();
289             }
290         }
291     }
292 
293 
294     /**
295      * Parses an OSGi header line into a map structure
296      *
297      * @param header The header line
298      * @return A map with the key the entry value and the value a map of attributes
299      * @since 2.2.0
300      */
301     public static Map<String,Map<String,String>> parseHeader(String header)
302     {
303         return OSGiHeader.parseHeader(header);
304     }
305 
306     /**
307      * Builds the header string from a map
308      * @param values The header values
309      * @return A string, suitable for inclusion into an OSGI header string
310      * @since 2.6
311      */
312     public static String buildHeader(Map<String,Map<String,String>> values)
313     {
314         StringBuilder header = new StringBuilder();
315         for (Iterator<Map.Entry<String,Map<String,String>>> i = values.entrySet().iterator(); i.hasNext(); )
316         {
317             Map.Entry<String,Map<String,String>> entry = i.next();
318             buildHeader(entry.getKey(), entry.getValue(), header);
319             if (i.hasNext())
320             {
321                 header.append(",");
322             }
323         }
324         return header.toString();
325     }
326 
327     /**
328      * Builds the header string from a map
329      * @param key The header value
330      * @param attrs The map of attributes
331      * @return A string, suitable for inclusion into an OSGI header string
332      * @since 2.2.0
333      */
334     public static String buildHeader(String key, Map<String,String> attrs)
335     {
336         StringBuilder fullPkg = new StringBuilder();
337         buildHeader(key, attrs, fullPkg);
338         return fullPkg.toString();
339     }
340 
341     /**
342      * Builds the header string from a map
343      * @since 2.6
344      */
345     private static void buildHeader(String key, Map<String,String> attrs, StringBuilder builder)
346     {
347         builder.append(key);
348         if (attrs != null && !attrs.isEmpty())
349         {
350             for (Map.Entry<String,String> entry : attrs.entrySet())
351             {
352                 builder.append(";");
353                 builder.append(entry.getKey());
354                 builder.append("=\"");
355                 builder.append(entry.getValue());
356                 builder.append("\"");
357             }
358         }
359     }
360 
361     /**
362      * Gets the plugin key from the bundle
363      *
364      * WARNING: shamelessly copied at {@link com.atlassian.plugin.osgi.bridge.PluginBundleUtils}, which can't use
365      * this class due to creating a cyclic build dependency.  Ensure these two implementations are in sync.
366      *
367      * This method shouldn't be used directly.  Instead consider consuming the {@link com.atlassian.plugin.osgi.bridge.external.PluginRetrievalService}.
368      *
369      * @param bundle The plugin bundle
370      * @return The plugin key, cannot be null
371      * @since 2.2.0
372      */
373     public static String getPluginKey(Bundle bundle)
374     {
375         return getPluginKey(
376                 bundle.getSymbolicName(),
377                 bundle.getHeaders().get(OsgiPlugin.ATLASSIAN_PLUGIN_KEY),
378                 bundle.getHeaders().get(Constants.BUNDLE_VERSION)
379         );
380     }
381 
382     /**
383      * Obtain the plugin key for a bundle in the given File.
384      *
385      * @param file the file containing the bundle.
386      * @return the pluginKey, or null if the bundle is malformed.
387      */
388     public static String getPluginKey(final File file)
389     {
390         JarFile jar = null;
391         try
392         {
393             jar = new JarFile(file);
394             final Manifest manifest = jar.getManifest();
395             if (manifest != null)
396             {
397                 return getPluginKey(manifest);
398             }
399         }
400         catch(final IOException eio)
401         {
402             log.warn("Cannot read jar file '" + file + "': " + eio.getMessage());
403         }
404         finally
405         {
406             closeQuietly(jar);
407         }
408         return null;
409     }
410 
411     /**
412      * Gets the plugin key from the jar manifest
413      *
414      * @param mf The plugin jar manifest
415      * @return The plugin key, cannot be null
416      * @since 2.2.0
417      */
418     public static String getPluginKey(Manifest mf)
419     {
420         return getPluginKey(
421                 getAttributeWithoutValidation(mf, Constants.BUNDLE_SYMBOLICNAME),
422                 getAttributeWithoutValidation(mf, OsgiPlugin.ATLASSIAN_PLUGIN_KEY),
423                 getAttributeWithoutValidation(mf, Constants.BUNDLE_VERSION)
424         );
425     }
426 
427     private static String getPluginKey(Object bundleName, Object atlKey, Object version)
428     {
429         Object key = atlKey;
430         if (key == null)
431         {
432             key = bundleName + "-" + version;
433         }
434 
435         return key.toString();
436     }
437 
438     /**
439      * Generate package version string such as "com.abc;version=1.2,com.atlassian".
440      * The output can be used for import or export.
441      *
442      * @param packages map of packagename->version.
443      */
444     public static String generatePackageVersionString(Map<String, String> packages)
445     {
446         if (packages == null || packages.size()==0)
447         {
448             return "";
449         }
450 
451         final StringBuilder sb = new StringBuilder();
452 
453         // add deterministism to string generation.
454         List<String> packageNames = new ArrayList<String>(packages.keySet());
455         Collections.sort(packageNames);
456 
457         for(String packageName:packageNames)
458         {
459             sb.append(",");
460             sb.append(packageName);
461 
462             String version = packages.get(packageName);
463 
464             // we can drop the version component if it's empty for a slight performance gain.
465             if (version != null && !version.equals(EMPTY_OSGI_VERSION))
466             {
467                 sb.append(";version=").append(version);
468             }
469         }
470 
471         // delete the initial ",".
472         sb.delete(0, 1);
473 
474         return sb.toString();
475     }
476 
477     /**
478      * Extract the attribute from manifest and validate it's not null and not empty
479      * @param manifest
480      * @param key
481      * @return value for the matching attribute key or will raise an Exception in case constraints are not met
482      * @throws NullPointerException if attribute value is null
483      * @throws IllegalArgumentException if attribute is empty
484      */
485     public static String getValidatedAttribute(final Manifest manifest, String key)
486     {
487         String value = getAttributeWithoutValidation(manifest, key);
488         checkNotNull(value);
489         checkArgument(!value.isEmpty());
490         return value;
491     }
492 
493     /**
494      * Extract the attribute from manifest and validate if it's not empty
495      * @param manifest
496      * @param key
497      * @return value for the matching attribute key
498      * @throws IllegalArgumentException if attribute value is empty
499      */
500     public static String getNonEmptyAttribute(final Manifest manifest, String key)
501     {
502         String attributeWithoutValidation = getAttributeWithoutValidation(manifest, key);
503         checkArgument(!attributeWithoutValidation.isEmpty());
504         return attributeWithoutValidation;
505     }
506 
507     /**
508      * Extract the attribute from manifest(no-validations)
509      * @param manifest
510      * @param key
511      * @return value for the matching attribute key
512      */
513     public static String getAttributeWithoutValidation(final Manifest manifest, String key)
514     {
515         return manifest.getMainAttributes().getValue(key);
516     }
517 
518     /**
519      * Extract a PluginInformation from the Manifest of a jar containing an OSGi bundle.
520      *
521      * @param manifest the manifest to parse.
522      * @return the parsed PluginInformation.
523      */
524     @Internal
525     public static PluginInformation extractOsgiPluginInformation(final Manifest manifest, final boolean requireVersion)
526     {
527         final String bundleVersion = requireVersion
528                 ? getValidatedAttribute(manifest, Constants.BUNDLE_VERSION)
529                 : getAttributeWithoutValidation(manifest, Constants.BUNDLE_VERSION);
530         final String bundleVendor = getAttributeWithoutValidation(manifest, Constants.BUNDLE_VENDOR);
531         final String bundleDescription = getAttributeWithoutValidation(manifest, Constants.BUNDLE_DESCRIPTION);
532         final PluginInformation pluginInformation = new PluginInformation();
533         pluginInformation.setVersion(bundleVersion);
534         pluginInformation.setDescription(bundleDescription);
535         pluginInformation.setVendorName(bundleVendor);
536         // OSGi plugins require execute Java.
537         pluginInformation.setPermissions(ImmutableSet.of(PluginPermission.EXECUTE_JAVA));
538         return pluginInformation;
539     }
540 
541     /**
542      * Extract the manifest of a PluginArtifact.
543      *
544      * @param pluginArtifact the plugin artifact who manifest is required.
545      * @return the manifest from pluginArtifact, or null if the manifest is missing, cannot be read, or is corrupted.
546      */
547     public static Manifest getManifest(final PluginArtifact pluginArtifact)
548     {
549         try
550         {
551             final InputStream manifestStream = pluginArtifact.getResourceAsStream(JarFile.MANIFEST_NAME);
552             if (manifestStream != null)
553             {
554                 try
555                 {
556                     return new Manifest(manifestStream);
557                 }
558                 catch (final IOException eio)
559                 {
560                     log.error("Cannot read manifest from plugin artifact '" + pluginArtifact.getName() + "': " + eio.getMessage());
561                 }
562                 finally
563                 {
564                     IOUtils.closeQuietly(manifestStream);
565                 }
566             }
567         }
568         catch (final PluginParseException epp)
569         {
570             log.error("Cannot get manifest resource from plugin artifact '" + pluginArtifact.getName() + "': " + epp.getMessage());
571         }
572         return null;
573     }
574 
575     /**
576      * Parse the Import-Package or Export-Package or Private-Package value passed and determine whether there is a
577      * star-value i.e. '*' package.
578      *
579      * @param packageString value of the whole package string
580      * @return true if '*' package is present
581      */
582     public static boolean containsStarPackage(final String packageString)
583     {
584         final Map<String, Map<String, String>> pkgs = OsgiHeaderUtil.parseHeader(packageString);
585         return pkgs.containsKey(STAR_PACKAGE);
586     }
587 
588     /**
589      * Reorder any Import-Package or Export-Package or Private-Package value passed, so that packages containing a
590      * star-value i.e. '*' to the end, preserving their current order.
591      * <p/>
592      * Note that any reordered packages will be logged at debug level.
593      *
594      * @param packageString value of the whole package string
595      * @param pluginKey only used for debug logging
596      * @return reordered package string, empty if input is null or empty
597      */
598     public static String moveStarPackageToEnd(final String packageString, final String pluginKey)
599     {
600         // assumes that the implementation of asMapMap (inside OsgiHeaderUtil.parseHeader) returns an ordered Map
601         final Map<String, Map<String, String>> currentPkgs = OsgiHeaderUtil.parseHeader(packageString);
602 
603         final LinkedHashMap<String, Map<String, String>> starPkgs = new LinkedHashMap<String, Map<String, String>>();
604         final LinkedHashMap<String, Map<String, String>> orderedPkgs = new LinkedHashMap<String, Map<String, String>>();
605 
606         // separate into packages containing a '*' and packages without
607         for (Map.Entry<String, Map<String, String>> pkg : currentPkgs.entrySet())
608         {
609             if (pkg.getKey().contains(STAR_PACKAGE))
610             {
611                 starPkgs.put(pkg.getKey(), pkg.getValue());
612             }
613             else
614             {
615                 orderedPkgs.put(pkg.getKey(), pkg.getValue());
616             }
617         }
618 
619         // put '*' package at the end in order
620         for (Map.Entry<String, Map<String, String>> starPkg : starPkgs.entrySet())
621         {
622             log.debug("moving {} package to end for plugin {}", starPkg.getKey(), pluginKey);
623             orderedPkgs.put(starPkg.getKey(), starPkg.getValue());
624         }
625 
626         // stringify it
627         return buildHeader(orderedPkgs);
628     }
629 }