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
36
37
38 public class DefaultPluginTransformer implements PluginTransformer
39 {
40
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
54
55
56
57
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
84 Map<String,byte[]> filesToAdd = new HashMap<String, byte[]>();
85
86
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
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
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
147
148
149
150
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
191
192
193
194
195
196
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
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
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
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
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
249 properties.putAll(processBundleInstructions(parser.getDocument()));
250
251
252 properties.put(Analyzer.IMPORT_PACKAGE, addReferrersToImports(properties.getProperty(Analyzer.IMPORT_PACKAGE), referrers));
253 builder.setProperties(properties);
254 }
255
256
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
306
307
308
309
310
311
312 static File addFilesToExistingZip(File zipFile,
313 Map<String,byte[]> files) throws IOException {
314
315 File tempFile = File.createTempFile(zipFile.getName(), null);
316
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
329 out.putNextEntry(new ZipEntry(name));
330
331 int len;
332 while ((len = zin.read(buf)) > 0)
333 out.write(buf, 0, len);
334 }
335 entry = zin.getNextEntry();
336 }
337
338 zin.close();
339
340 for (Map.Entry<String,byte[]> fentry : files.entrySet())
341 {
342 InputStream in = new ByteArrayInputStream(fentry.getValue());
343
344 out.putNextEntry(new ZipEntry(fentry.getKey()));
345
346 int len;
347 while ((len = in.read(buf)) > 0) {
348 out.write(buf, 0, len);
349 }
350
351 out.closeEntry();
352 in.close();
353 }
354
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
373
374 private static class PluginInformationDescriptorParser extends XmlDescriptorParser
375 {
376
377
378
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 }