View Javadoc

1   package com.atlassian.plugin.osgi.factory.transform;
2   
3   import aQute.lib.osgi.Analyzer;
4   import aQute.lib.osgi.Builder;
5   import aQute.lib.osgi.Jar;
6   import com.atlassian.plugin.PluginInformation;
7   import com.atlassian.plugin.PluginManager;
8   import com.atlassian.plugin.PluginParseException;
9   import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
10  import com.atlassian.plugin.osgi.hostcomponents.PropertyBuilder;
11  import com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy;
12  import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
13  import com.atlassian.plugin.parsers.XmlDescriptorParser;
14  import org.apache.log4j.Level;
15  import org.apache.log4j.Logger;
16  import org.apache.commons.lang.Validate;
17  import org.dom4j.*;
18  import org.dom4j.io.OutputFormat;
19  import org.dom4j.io.SAXReader;
20  import org.dom4j.io.XMLWriter;
21  import org.osgi.framework.Constants;
22  
23  import java.io.*;
24  import java.net.URL;
25  import java.net.URLClassLoader;
26  import java.util.*;
27  import java.util.jar.JarEntry;
28  import java.util.jar.JarFile;
29  import java.util.jar.Manifest;
30  import java.util.zip.ZipEntry;
31  import java.util.zip.ZipInputStream;
32  import java.util.zip.ZipOutputStream;
33  
34  /**
35   * Default implementation of plugin transformation that uses BND to generate the manifest and manually creates the
36   * spring configuration file.
37   */
38  public class DefaultPluginTransformer implements PluginTransformer
39  {
40      // The spring configuration containing exported components and imported host components
41      static final String ATLASSIAN_PLUGIN_SPRING_XML = "META-INF/spring/atlassian-plugins-spring.xml";
42  
43      private static final Logger log = Logger.getLogger(DefaultPluginTransformer.class);
44  
45      private final List<SpringTransformer> springTransformers = Arrays.asList(
46              new ComponentSpringTransformer(),
47              new ComponentImportSpringTransformer(),
48              new HostComponentSpringTransformer(),
49              new ModuleTypeSpringTransformer()
50      );
51  
52      /**
53       * Transforms the file into an OSGi bundle
54       * @param pluginJar The plugin jar
55       * @param regs The list of registered host components
56       * @return The new OSGi-enabled plugin jar
57       * @throws PluginTransformationException If anything goes wrong
58       */
59      public File transform(File pluginJar, List<HostComponentRegistration> regs) throws PluginTransformationException
60      {
61          Validate.notNull(pluginJar, "The plugin jar is required");
62          Validate.notNull(regs, "The host component registrations are required");
63          final JarFile jar;
64          try
65          {
66              jar = new JarFile(pluginJar);
67          }
68          catch (IOException e)
69          {
70              throw new PluginTransformationException("Plugin is not a valid jar file", e);
71          }
72  
73          // List of all files to add/override in the new jar
74          Map<String,byte[]> filesToAdd = new HashMap<String, byte[]>();
75  
76          // Try to generate a manifest if none available or merge with an existing one to add host component imports
77          URL atlassianPluginsXmlUrl = null;
78          try
79          {
80              final ClassLoader cl = new URLClassLoader(new URL[]{pluginJar.toURL()}, null);
81              atlassianPluginsXmlUrl = cl.getResource(PluginManager.PLUGIN_DESCRIPTOR_FILENAME);
82              if (atlassianPluginsXmlUrl == null)
83                  throw new IllegalStateException("Cannot find atlassian-plugins.xml in jar");
84  
85              log.info("Generating the manifest for plugin "+pluginJar.getName());
86              filesToAdd.put("META-INF/MANIFEST.MF", generateManifest(atlassianPluginsXmlUrl.openStream(), pluginJar, regs));
87          }
88          catch (PluginParseException e)
89          {
90              throw new PluginTransformationException("Unable to generate manifest", e);
91          }
92          catch (IOException e)
93          {
94              throw new PluginTransformationException("Unable to read existing plugin jar manifest", e);
95          }
96  
97          // Try to generate the spring config that pulls in host components and exports plugin components
98          if (jar.getEntry(ATLASSIAN_PLUGIN_SPRING_XML) == null) {
99              try
100             {
101                 log.info("Generating "+ATLASSIAN_PLUGIN_SPRING_XML + " for plugin "+pluginJar.getName());
102                 filesToAdd.put(ATLASSIAN_PLUGIN_SPRING_XML, generateSpringXml(atlassianPluginsXmlUrl.openStream(), regs));
103             }
104             catch (DocumentException e)
105             {
106                 throw new PluginTransformationException("Unable to generate host component spring XML", e);
107             }
108             catch (IOException e)
109             {
110                 throw new PluginTransformationException("Unable to open atlassian-plugins.xml", e);
111             }
112         }
113 
114         // Create a new jar by overriding the specified files
115         try
116         {
117             if (log.isDebugEnabled())
118             {
119                 StringBuilder sb = new StringBuilder();
120                 sb.append("Overriding files in ").append(pluginJar.getName()).append(":\n");
121                 for (Map.Entry<String,byte[]> entry : filesToAdd.entrySet())
122                 {
123                     sb.append("==").append(entry.getKey()).append("==\n");
124                     sb.append(new String(entry.getValue()));
125                 }
126                 log.debug(sb.toString());
127             }
128             return addFilesToExistingZip(pluginJar, filesToAdd);
129         } catch (IOException e)
130         {
131             throw new PluginTransformationException("Unable to add files to plugin jar");
132         }
133 
134     }
135 
136     /**
137      * Generate the spring xml by processing the atlassian-plugins.xml file
138      * @param in The stream of the atlassian-plugins.xml file
139      * @param regs The list of registered host components
140      * @return The new spring xml in bytes
141      * @throws DocumentException If there are any errors processing the atlassian-plugins.xml document
142      */
143     byte[] generateSpringXml(InputStream in, List<HostComponentRegistration> regs) throws DocumentException
144     {
145         Document springDoc = DocumentHelper.createDocument();
146         Element root = springDoc.addElement("beans");
147 
148         root.addNamespace("beans", "http://www.springframework.org/schema/beans");
149         root.addNamespace("osgi", "http://www.springframework.org/schema/osgi");
150         root.addNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
151         root.addAttribute(new QName("schemaLocation", new Namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")),
152                 "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd\n" +
153                 "http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi.xsd");
154         root.setName("beans:beans");
155         root.addAttribute("default-autowire", "autodetect");
156 
157         SAXReader reader = new SAXReader();
158         Document pluginDoc = reader.read(in);
159 
160         for (SpringTransformer springTransformer : springTransformers)
161         {
162             springTransformer.transform(regs, pluginDoc, springDoc);
163         }
164 
165         ByteArrayOutputStream bout = new ByteArrayOutputStream();
166         OutputFormat format = OutputFormat.createPrettyPrint();
167 
168         try
169         {
170             XMLWriter writer = new XMLWriter(bout, format );
171             writer.write(springDoc );
172         } catch (IOException e)
173         {
174             throw new PluginTransformationException("Unable to print generated Spring XML", e);
175         }
176 
177         return bout.toByteArray();
178     }
179 
180     /**
181      * Generates a new manifest file
182      *
183      * @param descriptorStream The existing manifest
184      * @param file The jar
185      * @param regs The list of host component registrations
186      * @return The new manifest file in bytes
187      * @throws PluginParseException If there is any problems parsing atlassian-plugin.xml
188      */
189     byte[] generateManifest(InputStream descriptorStream, File file, List<HostComponentRegistration> regs) throws PluginParseException
190     {
191 
192         Builder builder = new Builder();
193         try
194         {
195             builder.setJar(file);
196             String referrers = OsgiHeaderUtil.findReferredPackages(regs);
197             
198             // Possibly necessary due to Spring XML creation
199             referrers += "com.atlassian.plugin.osgi.external,com.atlassian.plugin,";
200             if (isOsgiBundle(builder.getJar().getManifest()))
201             {
202                 String imports = addReferrersToImports(builder.getJar().getManifest().getMainAttributes().getValue(Constants.IMPORT_PACKAGE), referrers);
203                 builder.setProperty(Constants.IMPORT_PACKAGE, imports);
204                 builder.mergeManifest(builder.getJar().getManifest());
205             } else
206             {
207                 PluginInformationDescriptorParser parser = new PluginInformationDescriptorParser(descriptorStream);
208                 PluginInformation info = parser.getPluginInformation();
209 
210                 Properties properties = new Properties();
211 
212                 // Setup defaults
213                 properties.put("Spring-Context", "*;timeout=60");
214                 properties.put(Analyzer.BUNDLE_SYMBOLICNAME, parser.getKey());
215                 properties.put(Analyzer.IMPORT_PACKAGE, "*;resolution:=optional");
216                 properties.put(Analyzer.EXPORT_PACKAGE, "*");
217                 properties.put(Analyzer.BUNDLE_VERSION, info.getVersion());
218 
219                 // remove the verbose Include-Resource entry from generated manifest
220                 properties.put(Analyzer.REMOVE_HEADERS, Analyzer.INCLUDE_RESOURCE);
221 
222                 header(properties, Analyzer.BUNDLE_DESCRIPTION, info.getDescription());
223                 header(properties, Analyzer.BUNDLE_NAME, parser.getKey());
224                 header(properties, Analyzer.BUNDLE_VENDOR, info.getVendorName());
225                 header(properties, Analyzer.BUNDLE_DOCURL, info.getVendorUrl());
226 
227                 // Scan for embedded jars
228                 StringBuilder classpath = new StringBuilder();
229                 classpath.append(".");
230                 JarFile jarfile = new JarFile(file);
231                 for (Enumeration<JarEntry> e = jarfile.entries(); e.hasMoreElements(); )
232                 {
233                     JarEntry entry = e.nextElement();
234                     if (entry.getName().startsWith("META-INF/lib/") && entry.getName().endsWith(".jar"))
235                         classpath.append(",").append(entry.getName());
236                 }
237                 header(properties, Analyzer.BUNDLE_CLASSPATH, classpath.toString());
238 
239                 // Process any bundle instructions in atlassian-plugin.xml
240                 properties.putAll(processBundleInstructions(parser.getDocument()));
241 
242                 // Add referrers to the imports list
243                 properties.put(Analyzer.IMPORT_PACKAGE, addReferrersToImports(properties.getProperty(Analyzer.IMPORT_PACKAGE), referrers));
244                 builder.setProperties(properties);
245             }
246 
247             // Not sure if this is the best incantation of bnd, but as I don't have the source, it'll have to do
248             builder.calcManifest();
249             Jar jar = builder.build();
250             ByteArrayOutputStream bout = new ByteArrayOutputStream();
251             jar.writeManifest(bout);
252             return bout.toByteArray();
253 
254         } catch (Exception t)
255         {
256             throw new PluginParseException("Unable to process plugin to generate OSGi manifest", t);
257         }
258     }
259 
260     private boolean isOsgiBundle(Manifest manifest) throws IOException
261     {
262         return manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME) != null;
263     }
264 
265     private Map<String,String> processBundleInstructions(Document document)
266     {
267         Map<String,String> instructions = new HashMap<String,String>();
268         Element pluginInfo = document.getRootElement().element("plugin-info");
269         if (pluginInfo != null)
270         {
271             Element instructionRoot = pluginInfo.element("bundle-instructions");
272             if (instructionRoot != null)
273             {
274                 List<Element> instructionsElement = instructionRoot.elements();
275                 for (Element instructionElement : instructionsElement)
276                 {
277                     String name = instructionElement.getName();
278                     String value = instructionElement.getTextTrim();
279                     instructions.put(name, value);
280                 }
281             }
282         }
283         return instructions;
284     }
285 
286     private String addReferrersToImports(String imports, String referrers)
287     {
288         if (imports != null && imports.length() > 0)
289             imports = referrers + imports;
290         else
291             imports = referrers.substring(0, referrers.length() - 1);
292         return imports;
293     }
294 
295     /**
296      * Creates a new jar by overriding the specified files in the existing one
297      *
298      * @param zipFile The existing zip file
299      * @param files The files to override
300      * @return The new zip
301      * @throws IOException If there are any problems processing the streams
302      */
303     static File addFilesToExistingZip(File zipFile,
304 			 Map<String,byte[]> files) throws IOException {
305                 // get a temp file
306 		File tempFile = File.createTempFile(zipFile.getName(), null);
307                 // delete it, otherwise you cannot rename your existing zip to it.
308 		byte[] buf = new byte[1024];
309 
310 		ZipInputStream zin = new ZipInputStream(new FileInputStream(zipFile));
311 		ZipOutputStream out = new ZipOutputStream(new FileOutputStream(tempFile));
312 
313 		ZipEntry entry = zin.getNextEntry();
314 		while (entry != null)
315         {
316 			String name = entry.getName();
317 			if (!files.containsKey(name))
318             {
319 				// Add ZIP entry to output stream.
320 				out.putNextEntry(new ZipEntry(name));
321 				// Transfer bytes from the ZIP file to the output file
322 				int len;
323 				while ((len = zin.read(buf)) > 0)
324 					out.write(buf, 0, len);
325 			}
326 			entry = zin.getNextEntry();
327 		}
328 		// Close the streams
329 		zin.close();
330 		// Compress the files
331 		for (Map.Entry<String,byte[]> fentry : files.entrySet())
332         {
333             InputStream in = new ByteArrayInputStream(fentry.getValue());
334 			// Add ZIP entry to output stream.
335 			out.putNextEntry(new ZipEntry(fentry.getKey()));
336 			// Transfer bytes from the file to the ZIP file
337 			int len;
338 			while ((len = in.read(buf)) > 0) {
339 				out.write(buf, 0, len);
340 			}
341 			// Complete the entry
342 			out.closeEntry();
343 			in.close();
344 		}
345 		// Complete the ZIP file
346 		out.close();
347         return tempFile;
348     }
349 
350 
351     private static void header(Properties properties, String key, Object value)
352     {
353         if (value == null)
354             return;
355 
356         if (value instanceof Collection && ((Collection) value).isEmpty())
357             return;
358 
359         properties.put(key, value.toString().replaceAll("[\r\n]", ""));
360     }
361 
362     /**
363      * Descriptor parser that exposes the PluginInformation object directly
364      */
365     private static class PluginInformationDescriptorParser extends XmlDescriptorParser
366     {
367         /**
368          * @throws com.atlassian.plugin.PluginParseException
369          *          if there is a problem reading the descriptor from the XML {@link java.io.InputStream}.
370          */
371         public PluginInformationDescriptorParser(InputStream source) throws PluginParseException
372         {
373             super(source);
374         }
375 
376         public PluginInformation getPluginInformation()
377         {
378             return createPluginInformation(getDocument().getRootElement().element("plugin-info"));
379         }
380 
381         @Override
382         public Document getDocument()
383         {
384             return super.getDocument();
385         }
386     }
387 }