View Javadoc

1   package com.atlassian.plugin.osgi.util;
2   
3   import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
4   import com.atlassian.plugin.osgi.container.PackageScannerConfiguration;
5   import com.atlassian.plugin.util.ClassLoaderUtils;
6   import com.atlassian.plugin.util.ClassUtils;
7   
8   import java.util.*;
9   import java.util.jar.Manifest;
10  import java.io.IOException;
11  import java.io.InputStream;
12  import java.io.BufferedReader;
13  import java.io.InputStreamReader;
14  import java.net.MalformedURLException;
15  
16  import org.twdata.pkgscanner.ExportPackage;
17  import org.twdata.pkgscanner.PackageScanner;
18  import static org.twdata.pkgscanner.PackageScanner.jars;
19  import static org.twdata.pkgscanner.PackageScanner.include;
20  import static org.twdata.pkgscanner.PackageScanner.exclude;
21  import static org.twdata.pkgscanner.PackageScanner.packages;
22  import org.osgi.framework.Version;
23  import org.osgi.framework.Constants;
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.commons.io.IOUtils;
27  import aQute.lib.osgi.Analyzer;
28  import aQute.lib.osgi.Jar;
29  
30  import javax.servlet.ServletContext;
31  
32  /**
33   * Utilities to help create OSGi headers
34   */
35  public class OsgiHeaderUtil
36  {
37      static final String JDK_PACKAGES_PATH = "jdk-packages.txt";
38      static final String JDK6_PACKAGES_PATH = "jdk6-packages.txt";
39      static Log log = LogFactory.getLog(OsgiHeaderUtil.class);
40  
41      /**
42       * Finds all referred packages for host component registrations by scanning their declared interfaces' bytecode.
43       *
44       * @param registrations A list of host component registrations
45       * @return The referred packages in a format compatible with an OSGi header
46       * @throws IOException If there are any problems scanning bytecode
47       */
48      public static String findReferredPackages(List<HostComponentRegistration> registrations) throws IOException
49      {
50          StringBuffer sb = new StringBuffer();
51          Set<String> referredPackages = new HashSet<String>();
52          Set<String> referredClasses = new HashSet<String>();
53          if (registrations == null)
54          {
55              sb.append(",");
56          }
57          else
58          {
59              for (HostComponentRegistration reg : registrations)
60              {
61                  Set<Class> classesToScan = new HashSet<Class>();
62  
63                  // Make sure we scan all extended interfaces as well
64                  for (Class inf : reg.getMainInterfaceClasses())
65                      ClassUtils.findAllTypes(inf, classesToScan);
66  
67                  for (Class inf : classesToScan)
68                  {
69                      String clsName = inf.getName().replace('.','/')+".class";
70                      crawlReferenceTree(clsName, referredClasses, referredPackages, 1);
71                  }
72              }
73              for (String pkg : referredPackages)
74              {
75                  sb.append(pkg).append(",");
76              }
77          }
78          return sb.toString();
79      }
80  
81      /**
82       * This will crawl the class interfaces to the desired level.
83       *
84       * @param className name of the class.
85       * @param scannedClasses set of classes that have been scanned.
86       * @param packageImports set of imports that have been found.
87       * @param level depth of scan (recursion).
88       * @throws IOException error loading a class.
89       */
90      static void crawlReferenceTree(String className, Set<String> scannedClasses, Set<String> packageImports, int level) throws IOException
91      {
92          if (level <= 0)
93          {
94              return;
95          }
96  
97          if (className.startsWith("java/"))
98              return;
99  
100         if (scannedClasses.contains(className))
101             return;
102         else
103             scannedClasses.add(className);
104 
105         if (log.isDebugEnabled())
106             log.debug("Crawling "+className);
107 
108         InputStream in = ClassLoaderUtils.getResourceAsStream(className, OsgiHeaderUtil.class);
109         if (in == null)
110         {
111             log.error("Cannot find interface "+className);
112             return;
113         }
114         Clazz clz = new Clazz(className, in);
115         packageImports.addAll(clz.getReferred().keySet());
116 
117         Set<String> referredClasses = clz.getReferredClasses();
118         for (String ref : referredClasses)
119             crawlReferenceTree(ref, scannedClasses, packageImports, level-1);
120 
121     }
122 
123     /**
124      * Determines framework exports taking into account host components and package scanner configuration.
125      *
126      * @param regs The list of host component registrations
127      * @param packageScannerConfig The configuration for the package scanning
128      * @return A list of exports, in a format compatible with OSGi headers
129      */
130     public static String determineExports(List<HostComponentRegistration> regs, PackageScannerConfiguration packageScannerConfig){
131         String exports = null;
132 
133         StringBuilder origExports = new StringBuilder();
134         origExports.append("org.osgi.framework; version=1.4.1,");
135         origExports.append("org.osgi.service.packageadmin; version=1.2.0," );
136         origExports.append("org.osgi.service.startlevel; version=1.0.0,");
137         origExports.append("org.osgi.service.url; version=1.0.0,");
138         origExports.append("org.osgi.util; version=1.4.1,");
139         origExports.append("org.osgi.util.tracker; version=1.4.1,");
140         origExports.append("host.service.command; version=1.0.0,");
141 
142         constructJdkExports(origExports, JDK_PACKAGES_PATH);
143         origExports.append(",");
144 
145         if (System.getProperty("java.specification.version").equals("1.6")) {
146             constructJdkExports(origExports, JDK6_PACKAGES_PATH);
147             origExports.append(",");
148         }
149 
150         Collection<ExportPackage> exportList = generateExports(packageScannerConfig);
151         constructAutoExports(origExports, exportList);
152 
153 
154         try
155         {
156             origExports.append(findReferredPackages(regs));
157 
158             Analyzer analyzer = new Analyzer();
159             analyzer.setJar(new Jar("somename.jar"));
160             
161             // we pretend the exports are imports for the sake of the bnd tool, which would otherwise cut out
162             // exports that weren't actually in the jar
163             analyzer.setProperty(Constants.IMPORT_PACKAGE, origExports.toString());
164             Manifest mf = analyzer.calcManifest();
165 
166             exports = mf.getMainAttributes().getValue(Constants.IMPORT_PACKAGE);
167         } catch (IOException ex)
168         {
169             log.error("Unable to calculate necessary exports based on host components", ex);
170             exports = origExports.toString();
171         }
172 
173         if (log.isDebugEnabled()) {
174             log.debug("Exports:\n"+exports.replaceAll(",", "\r\n"));
175         }
176         return exports;
177     }
178 
179     static void constructAutoExports(StringBuilder sb, Collection<ExportPackage> packageExports) {
180         for (Iterator<ExportPackage> i = packageExports.iterator(); i.hasNext(); ) {
181             ExportPackage pkg = i.next();
182             sb.append(pkg.getPackageName());
183             if (pkg.getVersion() != null) {
184                 try {
185                     Version.parseVersion(pkg.getVersion());
186                     sb.append(";version=").append(pkg.getVersion());
187                 } catch (IllegalArgumentException ex) {
188                     log.info("Unable to parse version: "+pkg.getVersion());
189                 }
190             }
191             sb.append(",");
192         }
193     }
194 
195     static Collection<ExportPackage> generateExports(PackageScannerConfiguration packageScannerConfig)
196     {
197         String[] arrType = new String[0];
198 
199         PackageScanner scanner = new PackageScanner()
200            .select(
201                jars(
202                        include(packageScannerConfig.getJarIncludes().toArray(arrType)),
203                        exclude(packageScannerConfig.getJarExcludes().toArray(arrType))),
204                packages(
205                        include(packageScannerConfig.getPackageIncludes().toArray(arrType)),
206                        exclude(packageScannerConfig.getPackageExcludes().toArray(arrType)))
207            )
208            .withMappings(packageScannerConfig.getPackageVersions());
209 
210         Collection<ExportPackage> exports = scanner.scan();
211 
212         if (!isPackageScanSuccessful(exports) && packageScannerConfig.getServletContext() != null)
213         {
214             log.warn("Unable to find expected packages via classloader scanning.  Trying ServletContext scanning...");
215             ServletContext ctx = packageScannerConfig.getServletContext();
216             try
217             {
218                 exports = scanner.scan(ctx.getResource("/WEB-INF/lib"), ctx.getResource("/WEB-INF/classes"));
219             }
220             catch (MalformedURLException e)
221             {
222                 log.warn(e);
223             }
224         }
225         
226         if (!isPackageScanSuccessful(exports))
227         {
228             throw new IllegalStateException("Unable to find required packages via classloader or servlet context"
229                     + " scanning, most likely due to an application server bug.");
230         }
231         return exports;
232     }
233 
234     /**
235      * Tests to see if a scan of packages to export was successful, using the presence of log4j as the criteria.
236      *
237      * @param exports The exports found so far
238      * @return True if log4j is present, false otherwise
239      */
240     private static boolean isPackageScanSuccessful(Collection<ExportPackage> exports)
241     {
242         boolean log4jFound = false;
243         for (ExportPackage export : exports)
244         {
245             if (export.getPackageName().equals("org.apache.log4j"))
246             {
247                 log4jFound = true;
248                 break;
249             }
250         }
251         return log4jFound;
252     }
253 
254     static void constructJdkExports(StringBuilder sb, String packageListPath)
255     {
256         InputStream in = null;
257         try
258         {
259             in = ClassLoaderUtils.getResourceAsStream(packageListPath, OsgiHeaderUtil.class);
260             BufferedReader reader = new BufferedReader(new InputStreamReader(in));
261             String line;
262             while ((line = reader.readLine()) != null)
263             {
264                 line = line.trim();
265                 if (line.length() > 0)
266                 {
267                     if (line.charAt(0) != '#')
268                     {
269                         if (sb.length() > 0)
270                             sb.append(',');
271                         sb.append(line);
272                     }
273                 }
274             }
275         } catch (IOException e)
276         {
277             IOUtils.closeQuietly(in);
278         }
279     }
280 
281 }