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
292 private void enforceHostVersionsForUnknownImports(final Manifest manifest, final SystemExports exports)
293 {
294 final String origImports = manifest.getMainAttributes().getValue(Constants.IMPORT_PACKAGE);
295 if (origImports != null)
296 {
297 final StringBuilder imports = new StringBuilder();
298 final Map<String,Map<String,String>> header = OsgiHeaderUtil.parseHeader(origImports);
299 for (final Map.Entry<String,Map<String,String>> pkgImport : header.entrySet())
300 {
301 String imp = null;
302 if (pkgImport.getValue().isEmpty())
303 {
304 final String export = exports.getFullExport(pkgImport.getKey());
305 if (!export.equals(imp))
306 {
307 imp = export;
308 }
309
310 }
311 if (imp == null)
312 {
313 imp = OsgiHeaderUtil.buildHeader(pkgImport.getKey(), pkgImport.getValue());
314 }
315 imports.append(imp);
316 imports.append(",");
317 }
318 if (imports.length() > 0)
319 {
320 imports.deleteCharAt(imports.length() - 1);
321 }
322
323 manifest.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, imports.toString());
324 }
325 }
326
327 private boolean isOsgiBundle(final Manifest manifest) throws IOException
328 {
329
330 return manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME) != null;
331 }
332
333 private String addExtraImports(String importsLine, final List<String> extraImports)
334 {
335 Map<String,Map<String,String>> originalImports = OsgiHeaderUtil.parseHeader(importsLine);
336 for (String exImport : extraImports)
337 {
338 if (!exImport.startsWith("java."))
339 {
340
341 final String extraImportPackage = StringUtils.split(exImport, ';')[0];
342
343 Map attrs = originalImports.get(extraImportPackage);
344
345 if (attrs != null)
346 {
347 Object resolution = attrs.get(RESOLUTION_DIRECTIVE);
348 if (Constants.RESOLUTION_OPTIONAL.equals(resolution))
349 {
350 attrs.put(RESOLUTION_DIRECTIVE, Constants.RESOLUTION_MANDATORY);
351 }
352 }
353
354 else
355 {
356 originalImports.put(exImport, Collections.<String, String>emptyMap());
357 }
358 }
359 }
360
361 return OsgiHeaderUtil.buildHeader(originalImports);
362 }
363
364 private boolean manifestDoesntHaveRequiredOsgiHeader(Manifest mf, Entry<String, String> entry)
365 {
366 if (mf.getMainAttributes().containsKey(new Attributes.Name(entry.getKey())))
367 {
368 return !entry.getValue().equals(mf.getMainAttributes().getValue(entry.getKey()));
369 }
370 return true;
371 }
372
373 private static void header(final Properties properties, final String key, final Object value)
374 {
375 if (value == null)
376 {
377 return;
378 }
379
380 if (value instanceof Collection && ((Collection) value).isEmpty())
381 {
382 return;
383 }
384
385 properties.put(key, value.toString().replaceAll("[\r\n]", ""));
386 }
387
388 private void assertSpringAvailableIfRequired(final TransformContext context)
389 {
390 if (context.shouldRequireSpring())
391 {
392 final String header = context.getManifest().getMainAttributes().getValue(SPRING_CONTEXT);
393 if (header == null)
394 {
395 log.debug("The Spring Manifest header 'Spring-Context' is missing in jar '{}'." +
396 " If you experience any problems, please add it and set it to '*;timeout:={}'",
397 context.getPluginArtifact(), PluginUtils.DEFAULT_ATLASSIAN_PLUGINS_ENABLE_WAIT_SECONDS);
398 return;
399 }
400 if (!header.contains(";timeout:="))
401 {
402 log.warn("The Spring Manifest header in jar '{}' isn't set for a timeout waiting for dependencies. " +
403 "Please add ';timeout:={}'", context.getPluginArtifact(),
404 PluginUtils.DEFAULT_ATLASSIAN_PLUGINS_ENABLE_WAIT_SECONDS);
405 }
406 }
407 }
408
409 }