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