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