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
32
33
34 public class DefaultPluginTransformer implements PluginTransformer
35 {
36
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
43
44
45
46
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
62 Map<String,byte[]> filesToAdd = new HashMap<String, byte[]>();
63
64
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
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
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
122
123
124
125
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
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
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
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
242
243
244
245
246
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() - 2);
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
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
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
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
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
317
318
319
320
321
322
323 static File addFilesToExistingZip(File zipFile,
324 Map<String,byte[]> files) throws IOException {
325
326 File tempFile = File.createTempFile(zipFile.getName(), null);
327
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
339 out.putNextEntry(new ZipEntry(name));
340
341 int len;
342 while ((len = zin.read(buf)) > 0) {
343 out.write(buf, 0, len);
344 }
345 }
346 entry = zin.getNextEntry();
347 }
348
349 zin.close();
350
351 for (Map.Entry<String,byte[]> fentry : files.entrySet())
352 {
353 InputStream in = new ByteArrayInputStream(fentry.getValue());
354
355 out.putNextEntry(new ZipEntry(fentry.getKey()));
356
357 int len;
358 while ((len = in.read(buf)) > 0) {
359 out.write(buf, 0, len);
360 }
361
362 out.closeEntry();
363 in.close();
364 }
365
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
384
385 static class PluginInformationDescriptorParser extends XmlDescriptorParser
386 {
387
388
389
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 }