View Javadoc

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