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