1 package com.atlassian.plugin.osgi.factory.transform.stage;
2
3 import aQute.lib.osgi.Analyzer;
4 import aQute.lib.osgi.Builder;
5 import aQute.lib.osgi.Jar;
6 import com.atlassian.plugin.Application;
7 import com.atlassian.plugin.PluginInformation;
8 import com.atlassian.plugin.PluginParseException;
9 import com.atlassian.plugin.osgi.factory.OsgiPlugin;
10 import com.atlassian.plugin.osgi.factory.transform.PluginTransformationException;
11 import com.atlassian.plugin.osgi.factory.transform.TransformContext;
12 import com.atlassian.plugin.osgi.factory.transform.TransformStage;
13 import com.atlassian.plugin.osgi.factory.transform.model.SystemExports;
14 import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
15 import com.atlassian.plugin.parsers.XmlDescriptorParser;
16 import com.atlassian.plugin.util.PluginUtils;
17 import com.google.common.collect.ImmutableSet;
18 import org.apache.commons.lang.StringUtils;
19 import org.osgi.framework.Constants;
20 import org.osgi.framework.Version;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
23
24 import java.io.ByteArrayOutputStream;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.Properties;
34 import java.util.StringTokenizer;
35 import java.util.jar.Attributes;
36 import java.util.jar.Manifest;
37
38
39
40
41
42
43 public class GenerateManifestStage implements TransformStage
44 {
45 private final int SPRING_TIMEOUT = PluginUtils.getDefaultEnablingWaitPeriod();
46 private final String SPRING_CONTEXT_DEFAULT = "*;"+ SPRING_CONTEXT_TIMEOUT + SPRING_TIMEOUT;
47 static Logger log = LoggerFactory.getLogger(GenerateManifestStage.class);
48 public static final String SPRING_CONTEXT = "Spring-Context";
49 private static final String SPRING_CONTEXT_TIMEOUT = "timeout:=";
50 private static final String SPRING_CONTEXT_DELIM = ";";
51 private static final String RESOLUTION_DIRECTIVE = "resolution:";
52
53 public void execute(final TransformContext context) throws PluginTransformationException
54 {
55 final Builder builder = new Builder();
56 try
57 {
58 builder.setJar(context.getPluginFile());
59
60
61 final XmlDescriptorParser parser = new XmlDescriptorParser(context.getDescriptorDocument(), ImmutableSet.<Application>of());
62
63 Manifest mf;
64 if (isOsgiBundle(context.getManifest()))
65 {
66 if (context.getExtraImports().isEmpty())
67 {
68 boolean modified = false;
69 mf = builder.getJar().getManifest();
70 for (Map.Entry<String,String> entry : getRequiredOsgiHeaders(context, parser.getKey()).entrySet())
71 {
72 if (manifestDoesntHaveRequiredOsgiHeader(mf, entry))
73 {
74 mf.getMainAttributes().putValue(entry.getKey(), entry.getValue());
75 modified = true;
76 }
77 }
78 validateOsgiVersionIsValid(mf);
79 if (modified)
80 {
81 writeManifestOverride(context, mf);
82 }
83
84 return;
85 }
86 else
87 {
88
89
90 assertSpringAvailableIfRequired(context);
91 mf = builder.getJar().getManifest();
92 final String imports = addExtraImports(builder.getJar().getManifest().getMainAttributes().getValue(Constants.IMPORT_PACKAGE), context.getExtraImports());
93 mf.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, imports);
94
95 for (Map.Entry<String,String> entry : getRequiredOsgiHeaders(context, parser.getKey()).entrySet())
96 {
97 mf.getMainAttributes().putValue(entry.getKey(), entry.getValue());
98 }
99 }
100 }
101 else
102 {
103 final PluginInformation info = parser.getPluginInformation();
104
105 final Properties properties = new Properties();
106
107
108 for (Map.Entry<String,String> entry : getRequiredOsgiHeaders(context, parser.getKey()).entrySet())
109 {
110 properties.put(entry.getKey(), entry.getValue());
111 }
112
113 properties.put(Analyzer.BUNDLE_SYMBOLICNAME, parser.getKey());
114 properties.put(Analyzer.IMPORT_PACKAGE, "*;resolution:=optional");
115
116
117
118
119 properties.put(Analyzer.BUNDLE_VERSION, info.getVersion());
120
121
122 properties.put(Analyzer.REMOVEHEADERS, Analyzer.INCLUDE_RESOURCE);
123
124 header(properties, Analyzer.BUNDLE_DESCRIPTION, info.getDescription());
125 header(properties, Analyzer.BUNDLE_NAME, parser.getKey());
126 header(properties, Analyzer.BUNDLE_VENDOR, info.getVendorName());
127 header(properties, Analyzer.BUNDLE_DOCURL, info.getVendorUrl());
128
129 List<String> bundleClassPaths = new ArrayList<String>();
130
131
132 bundleClassPaths.add(".");
133
134
135 List<String> innerClassPaths = new ArrayList<String>(context.getBundleClassPathJars());
136 Collections.sort(innerClassPaths);
137 bundleClassPaths.addAll(innerClassPaths);
138
139
140 header(properties, Analyzer.BUNDLE_CLASSPATH, StringUtils.join(bundleClassPaths, ','));
141
142
143 properties.putAll(context.getBndInstructions());
144
145
146 properties.put(Analyzer.IMPORT_PACKAGE, addExtraImports(properties.getProperty(Analyzer.IMPORT_PACKAGE), context.getExtraImports()));
147
148
149 if (!properties.containsKey(Analyzer.EXPORT_PACKAGE))
150 {
151 properties.put(Analyzer.EXPORT_PACKAGE, StringUtils.join(context.getExtraExports(), ','));
152 }
153 builder.setProperties(properties);
154 builder.calcManifest();
155 builder.getJar().close();
156 Jar jar = null;
157 try
158 {
159 jar = builder.build();
160 mf = jar.getManifest();
161 }
162 finally
163 {
164 if (jar != null)
165 {
166 jar.close();
167 }
168 }
169 }
170
171 enforceHostVersionsForUnknownImports(mf, context.getSystemExports());
172 validateOsgiVersionIsValid(mf);
173
174 writeManifestOverride(context, mf);
175 }
176 catch (final Exception t)
177 {
178 throw new PluginParseException("Unable to process plugin to generate OSGi manifest", t);
179 } finally
180 {
181 builder.getJar().close();
182 builder.close();
183 }
184
185 }
186
187 private Map<String,String> getRequiredOsgiHeaders(TransformContext context, String pluginKey)
188 {
189 Map<String, String> props = new HashMap<String, String>();
190 props.put(OsgiPlugin.ATLASSIAN_PLUGIN_KEY, pluginKey);
191 String springHeader = getDesiredSpringContextValue(context);
192 if (springHeader != null)
193 {
194 props.put(SPRING_CONTEXT, springHeader);
195 }
196 return props;
197 }
198
199 private String getDesiredSpringContextValue(TransformContext context)
200 {
201
202 final String header = context.getManifest().getMainAttributes().getValue(SPRING_CONTEXT);
203 if (header != null)
204 {
205 return ensureDefaultTimeout(header);
206 }
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291 private void enforceHostVersionsForUnknownImports(final Manifest manifest, final SystemExports exports)
292 {
293 final String origImports = manifest.getMainAttributes().getValue(Constants.IMPORT_PACKAGE);
294 if (origImports != null)
295 {
296 final StringBuilder imports = new StringBuilder();
297 final Map<String,Map<String,String>> header = OsgiHeaderUtil.parseHeader(origImports);
298 for (final Map.Entry<String,Map<String,String>> pkgImport : header.entrySet())
299 {
300 String imp = null;
301 if (pkgImport.getValue().isEmpty())
302 {
303 final String export = exports.getFullExport(pkgImport.getKey());
304 if (!export.equals(imp))
305 {
306 imp = export;
307 }
308
309 }
310 if (imp == null)
311 {
312 imp = OsgiHeaderUtil.buildHeader(pkgImport.getKey(), pkgImport.getValue());
313 }
314 imports.append(imp);
315 imports.append(",");
316 }
317 if (imports.length() > 0)
318 {
319 imports.deleteCharAt(imports.length() - 1);
320 }
321
322 manifest.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, imports.toString());
323 }
324 }
325
326 private boolean isOsgiBundle(final Manifest manifest) throws IOException
327 {
328
329 return manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME) != null;
330 }
331
332 private String addExtraImports(String importsLine, final List<String> extraImports)
333 {
334 Map<String,Map<String,String>> originalImports = OsgiHeaderUtil.parseHeader(importsLine);
335 for (String exImport : extraImports)
336 {
337 if (!exImport.startsWith("java."))
338 {
339
340 final String extraImportPackage = StringUtils.split(exImport, ';')[0];
341
342 Map attrs = originalImports.get(extraImportPackage);
343
344 if (attrs != null)
345 {
346 Object resolution = attrs.get(RESOLUTION_DIRECTIVE);
347 if (Constants.RESOLUTION_OPTIONAL.equals(resolution))
348 {
349 attrs.put(RESOLUTION_DIRECTIVE, Constants.RESOLUTION_MANDATORY);
350 }
351 }
352
353 else
354 {
355 originalImports.put(exImport, Collections.<String, String>emptyMap());
356 }
357 }
358 }
359
360 return OsgiHeaderUtil.buildHeader(originalImports);
361 }
362
363 private boolean manifestDoesntHaveRequiredOsgiHeader(Manifest mf, Entry<String, String> entry)
364 {
365 if (mf.getMainAttributes().containsKey(new Attributes.Name(entry.getKey())))
366 {
367 return !entry.getValue().equals(mf.getMainAttributes().getValue(entry.getKey()));
368 }
369 return true;
370 }
371
372 private static void header(final Properties properties, final String key, final Object value)
373 {
374 if (value == null)
375 {
376 return;
377 }
378
379 if (value instanceof Collection && ((Collection) value).isEmpty())
380 {
381 return;
382 }
383
384 properties.put(key, value.toString().replaceAll("[\r\n]", ""));
385 }
386
387 private void assertSpringAvailableIfRequired(final TransformContext context)
388 {
389 if (context.shouldRequireSpring())
390 {
391 final String header = context.getManifest().getMainAttributes().getValue(SPRING_CONTEXT);
392 if (header == null)
393 {
394 log.debug("The Spring Manifest header 'Spring-Context' is missing in jar '" +
395 context.getPluginArtifact().toString() + "'. If you experience any problems, please add it and set it to '" +
396 SPRING_CONTEXT_DEFAULT + "'");
397 }
398 else if (!header.contains(";timeout:=" + SPRING_TIMEOUT))
399 {
400 log.warn("The Spring Manifest header in jar '" + context.getPluginArtifact().toString() + "' isn't " +
401 "set for a " + SPRING_TIMEOUT + " second timeout waiting for dependencies. " +
402 "Please add ';timeout:=" + SPRING_TIMEOUT + "'");
403 }
404 }
405 }
406
407 }