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