1   package com.atlassian.plugin.osgi.container.felix;
2   
3   import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
4   import com.atlassian.plugin.osgi.container.PackageScannerConfiguration;
5   import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
6   import com.atlassian.plugin.util.ClassLoaderUtils;
7   
8   import java.util.List;
9   import java.util.Collection;
10  import java.util.Iterator;
11  import java.util.jar.Manifest;
12  import java.io.*;
13  import java.net.MalformedURLException;
14  
15  import org.twdata.pkgscanner.ExportPackage;
16  import org.twdata.pkgscanner.PackageScanner;
17  import static org.twdata.pkgscanner.PackageScanner.jars;
18  import static org.twdata.pkgscanner.PackageScanner.include;
19  import static org.twdata.pkgscanner.PackageScanner.exclude;
20  import static org.twdata.pkgscanner.PackageScanner.packages;
21  import org.osgi.framework.Constants;
22  import org.osgi.framework.Version;
23  import org.apache.commons.io.IOUtils;
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import aQute.lib.osgi.Analyzer;
27  import aQute.lib.osgi.Jar;
28  
29  import javax.servlet.ServletContext;
30  
31  /**
32   * Builds the OSGi package exports string.  Uses a file to cache the scanned results, keyed by the application version.
33   */
34  class ExportsBuilder
35  {
36  
37      static final String JDK_PACKAGES_PATH = "jdk-packages.txt";
38      static final String JDK6_PACKAGES_PATH = "jdk6-packages.txt";
39      private static Log log = LogFactory.getLog(ExportsBuilder.class);
40      private static final String EXPORTS_TXT = "exports.txt";
41      private static String exportStringCache;
42  
43      /**
44       * Gets the framework exports taking into account host components and package scanner configuration.
45       * <p>
46       * This information cannot change without a system restart, so we determine this once and then cache the value.
47       * The cache is only useful if the plugin system is thrown away and re-initialised. This is done thousands of times
48       * during JIRA functional testing, and the cache was added to speed this up.
49       *
50       * @param regs The list of host component registrations
51       * @param packageScannerConfig The configuration for the package scanning
52       * @return A list of exports, in a format compatible with OSGi headers
53       */
54      public String getExports(List<HostComponentRegistration> regs, PackageScannerConfiguration packageScannerConfig) {
55          if (exportStringCache == null)
56          {
57              exportStringCache = determineExports(regs, packageScannerConfig);
58          }
59          return exportStringCache;
60      }
61  
62      /**
63       * Determines framework exports taking into account host components and package scanner configuration.
64       *
65       * @param regs The list of host component registrations
66       * @param packageScannerConfig The configuration for the package scanning
67       * @param cacheDir No longer used. (method deprecated).
68       * @return A list of exports, in a format compatible with OSGi headers
69       * @deprecated Please use {@link #getExports}. Deprecated since 2.3.6
70       */
71      @SuppressWarnings ({ "UnusedDeclaration" })
72      public String determineExports(List<HostComponentRegistration> regs, PackageScannerConfiguration packageScannerConfig, File cacheDir){
73          return determineExports(regs, packageScannerConfig);
74      }
75  
76      /**
77       * Determines framework exports taking into account host components and package scanner configuration.
78       *
79       * @param regs The list of host component registrations
80       * @param packageScannerConfig The configuration for the package scanning
81       * @return A list of exports, in a format compatible with OSGi headers
82       */
83      private String determineExports(List<HostComponentRegistration> regs, PackageScannerConfiguration packageScannerConfig){
84  
85          String exports = null;
86  
87          StringBuilder origExports = new StringBuilder();
88          origExports.append("org.osgi.framework; version=1.4.1,");
89          origExports.append("org.osgi.service.packageadmin; version=1.2.0," );
90          origExports.append("org.osgi.service.startlevel; version=1.1.0,");
91          origExports.append("org.osgi.service.url; version=1.0.0,");
92          origExports.append("org.osgi.util; version=1.4.1,");
93          origExports.append("org.osgi.util.tracker; version=1.4.1,");
94          origExports.append("host.service.command; version=1.0.0,");
95  
96          constructJdkExports(origExports, JDK_PACKAGES_PATH);
97          origExports.append(",");
98  
99          if (System.getProperty("java.specification.version").equals("1.6")) {
100             constructJdkExports(origExports, JDK6_PACKAGES_PATH);
101             origExports.append(",");
102         }
103 
104         Collection<ExportPackage> exportList = generateExports(packageScannerConfig);
105         constructAutoExports(origExports, exportList);
106 
107 
108         try
109         {
110             origExports.append(OsgiHeaderUtil.findReferredPackages(regs, packageScannerConfig.getPackageVersions()));
111 
112             Analyzer analyzer = new Analyzer();
113             analyzer.setJar(new Jar("somename.jar"));
114 
115             // we pretend the exports are imports for the sake of the bnd tool, which would otherwise cut out
116             // exports that weren't actually in the jar
117             analyzer.setProperty(Constants.IMPORT_PACKAGE, origExports.toString());
118             Manifest mf = analyzer.calcManifest();
119 
120             exports = mf.getMainAttributes().getValue(Constants.IMPORT_PACKAGE);
121         } catch (IOException ex)
122         {
123             log.error("Unable to calculate necessary exports based on host components", ex);
124             exports = origExports.toString();
125         }
126 
127         if (log.isDebugEnabled()) {
128             log.debug("Exports:\n"+exports.replaceAll(",", "\r\n"));
129         }
130         return exports;
131     }
132 
133     void constructAutoExports(StringBuilder sb, Collection<ExportPackage> packageExports) {
134         for (Iterator<ExportPackage> i = packageExports.iterator(); i.hasNext(); ) {
135             ExportPackage pkg = i.next();
136             sb.append(pkg.getPackageName());
137             if (pkg.getVersion() != null) {
138                 try {
139                     Version.parseVersion(pkg.getVersion());
140                     sb.append(";version=").append(pkg.getVersion());
141                 } catch (IllegalArgumentException ex) {
142                     log.info("Unable to parse version: "+pkg.getVersion());
143                 }
144             }
145             sb.append(",");
146         }
147     }
148 
149     Collection<ExportPackage> generateExports(PackageScannerConfiguration packageScannerConfig)
150     {
151         String[] arrType = new String[0];
152         PackageScanner scanner = new PackageScanner()
153            .select(
154                jars(
155                        include(packageScannerConfig.getJarIncludes().toArray(arrType)),
156                        exclude(packageScannerConfig.getJarExcludes().toArray(arrType))),
157                packages(
158                        include(packageScannerConfig.getPackageIncludes().toArray(arrType)),
159                        exclude(packageScannerConfig.getPackageExcludes().toArray(arrType)))
160            )
161            .withMappings(packageScannerConfig.getPackageVersions());
162 
163         if (log.isDebugEnabled())
164         {
165             scanner.enableDebug();
166         }
167 
168         Collection<ExportPackage> exports = scanner.scan();
169         log.info("Package scan completed. Found " + exports.size() + " packages to export.");
170 
171         if (!isPackageScanSuccessful(exports) && packageScannerConfig.getServletContext() != null)
172         {
173             log.warn("Unable to find expected packages via classloader scanning.  Trying ServletContext scanning...");
174             ServletContext ctx = packageScannerConfig.getServletContext();
175             try
176             {
177                 exports = scanner.scan(ctx.getResource("/WEB-INF/lib"), ctx.getResource("/WEB-INF/classes"));
178             }
179             catch (MalformedURLException e)
180             {
181                 log.warn(e);
182             }
183         }
184 
185         if (!isPackageScanSuccessful(exports))
186         {
187             throw new IllegalStateException("Unable to find required packages via classloader or servlet context"
188                     + " scanning, most likely due to an application server bug.");
189         }
190         return exports;
191     }
192 
193     /**
194      * Tests to see if a scan of packages to export was successful, using the presence of log4j as the criteria.
195      *
196      * @param exports The exports found so far
197      * @return True if log4j is present, false otherwise
198      */
199     private static boolean isPackageScanSuccessful(Collection<ExportPackage> exports)
200     {
201         boolean log4jFound = false;
202         for (ExportPackage export : exports)
203         {
204             if (export.getPackageName().equals("org.apache.log4j"))
205             {
206                 log4jFound = true;
207                 break;
208             }
209         }
210         return log4jFound;
211     }
212 
213     void constructJdkExports(StringBuilder sb, String packageListPath)
214     {
215         InputStream in = null;
216         try
217         {
218             in = ClassLoaderUtils.getResourceAsStream(packageListPath, ExportsBuilder.class);
219             BufferedReader reader = new BufferedReader(new InputStreamReader(in));
220             String line;
221             while ((line = reader.readLine()) != null)
222             {
223                 line = line.trim();
224                 if (line.length() > 0)
225                 {
226                     if (line.charAt(0) != '#')
227                     {
228                         if (sb.length() > 0)
229                             sb.append(',');
230                         sb.append(line);
231                     }
232                 }
233             }
234         } catch (IOException e)
235         {
236             IOUtils.closeQuietly(in);
237         }
238     }
239 }