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