View Javadoc

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