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