1 package com.atlassian.plugin.osgi.factory.transform.stage;
2
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.util.Collection;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.Properties;
9 import java.util.jar.JarEntry;
10 import java.util.jar.Manifest;
11
12 import org.apache.commons.lang.StringUtils;
13 import org.apache.commons.logging.Log;
14 import org.apache.commons.logging.LogFactory;
15 import org.osgi.framework.Constants;
16 import org.osgi.framework.Version;
17
18 import aQute.lib.osgi.Analyzer;
19 import aQute.lib.osgi.Builder;
20 import aQute.lib.osgi.Jar;
21
22 import com.atlassian.plugin.PluginInformation;
23 import com.atlassian.plugin.PluginParseException;
24 import com.atlassian.plugin.util.PluginUtils;
25 import com.atlassian.plugin.osgi.factory.OsgiPlugin;
26 import com.atlassian.plugin.osgi.factory.transform.PluginTransformationException;
27 import com.atlassian.plugin.osgi.factory.transform.TransformContext;
28 import com.atlassian.plugin.osgi.factory.transform.TransformStage;
29 import com.atlassian.plugin.osgi.factory.transform.model.SystemExports;
30 import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
31 import com.atlassian.plugin.parsers.XmlDescriptorParser;
32
33
34
35
36
37
38 public class GenerateManifestStage implements TransformStage
39 {
40 private final int SPRING_TIMEOUT = PluginUtils.getDefaultEnablingWaitPeriod();
41 private final String SPRING_CONTEXT_DEFAULT = "*;timeout:=" + SPRING_TIMEOUT;
42 static Log log = LogFactory.getLog(GenerateManifestStage.class);
43
44 public void execute(final TransformContext context) throws PluginTransformationException
45 {
46 final Builder builder = new Builder();
47 try
48 {
49 builder.setJar(context.getPluginFile());
50
51
52 final XmlDescriptorParser parser = new XmlDescriptorParser(context.getDescriptorDocument(), null);
53
54 if (isOsgiBundle(builder.getJar().getManifest()))
55 {
56 if (context.getExtraImports().isEmpty())
57 {
58 final Manifest mf = builder.getJar().getManifest();
59 mf.getMainAttributes().putValue(OsgiPlugin.ATLASSIAN_PLUGIN_KEY, parser.getKey());
60 validateOsgiVersionIsValid(mf);
61 writeManifestOverride(context, mf);
62
63 return;
64 }
65 else
66 {
67
68 assertSpringAvailableIfRequired(context);
69 final String imports = addExtraImports(builder.getJar().getManifest().getMainAttributes().getValue(Constants.IMPORT_PACKAGE), context.getExtraImports());
70 builder.setProperty(Constants.IMPORT_PACKAGE, imports);
71
72 builder.setProperty(OsgiPlugin.ATLASSIAN_PLUGIN_KEY, parser.getKey());
73 builder.mergeManifest(builder.getJar().getManifest());
74 }
75 }
76 else
77 {
78 final PluginInformation info = parser.getPluginInformation();
79
80 final Properties properties = new Properties();
81
82
83 if (SpringHelper.isSpringUsed(context))
84 {
85 properties.put("Spring-Context", SPRING_CONTEXT_DEFAULT);
86 }
87
88 properties.put(Analyzer.BUNDLE_SYMBOLICNAME, parser.getKey());
89 properties.put(Analyzer.IMPORT_PACKAGE, "*;resolution:=optional");
90
91
92
93
94 properties.put(Analyzer.BUNDLE_VERSION, info.getVersion());
95
96
97 properties.put(Analyzer.REMOVE_HEADERS, Analyzer.INCLUDE_RESOURCE);
98
99 header(properties, Analyzer.BUNDLE_DESCRIPTION, info.getDescription());
100 header(properties, Analyzer.BUNDLE_NAME, parser.getKey());
101 header(properties, Analyzer.BUNDLE_VENDOR, info.getVendorName());
102 header(properties, Analyzer.BUNDLE_DOCURL, info.getVendorUrl());
103 header(properties, OsgiPlugin.ATLASSIAN_PLUGIN_KEY, parser.getKey());
104
105
106 final StringBuilder classpath = new StringBuilder();
107 classpath.append(".");
108 for (final JarEntry jarEntry : context.getPluginJarEntries())
109 {
110 if (jarEntry.getName().startsWith("META-INF/lib/") && jarEntry.getName().endsWith(".jar"))
111 {
112 classpath.append(",").append(jarEntry.getName());
113 }
114 }
115 header(properties, Analyzer.BUNDLE_CLASSPATH, classpath.toString());
116
117
118 properties.putAll(context.getBndInstructions());
119
120
121 properties.put(Analyzer.IMPORT_PACKAGE, addExtraImports(properties.getProperty(Analyzer.IMPORT_PACKAGE), context.getExtraImports()));
122
123
124 if (!properties.containsKey(Analyzer.EXPORT_PACKAGE))
125 {
126 properties.put(Analyzer.EXPORT_PACKAGE, StringUtils.join(context.getExtraExports(), ','));
127 }
128 builder.setProperties(properties);
129 }
130
131 builder.calcManifest();
132 builder.getJar().close();
133 final Jar jar = builder.build();
134 final Manifest mf = jar.getManifest();
135
136 enforceHostVersionsForUnknownImports(mf, context.getSystemExports());
137 validateOsgiVersionIsValid(mf);
138
139 writeManifestOverride(context, mf);
140 jar.close();
141 }
142 catch (final Exception t)
143 {
144 throw new PluginParseException("Unable to process plugin to generate OSGi manifest", t);
145 } finally
146 {
147 builder.getJar().close();
148 builder.close();
149 }
150
151 }
152
153 private void validateOsgiVersionIsValid(Manifest mf)
154 {
155 String version = mf.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
156 try
157 {
158 if (Version.parseVersion(version) == Version.emptyVersion)
159 {
160
161 throw new IllegalArgumentException();
162 }
163 }
164 catch (IllegalArgumentException ex)
165 {
166 throw new IllegalArgumentException("Plugin version '" + version + "' is required and must be able to be " +
167 "parsed as an OSGi version - MAJOR.MINOR.MICRO.QUALIFIER");
168 }
169 }
170
171 private void writeManifestOverride(final TransformContext context, final Manifest mf)
172 throws IOException
173 {
174 final ByteArrayOutputStream bout = new ByteArrayOutputStream();
175 mf.write(bout);
176 context.getFileOverrides().put("META-INF/MANIFEST.MF", bout.toByteArray());
177 }
178
179
180
181
182
183
184
185 private void enforceHostVersionsForUnknownImports(final Manifest manifest, final SystemExports exports)
186 {
187 final String origImports = manifest.getMainAttributes().getValue(Constants.IMPORT_PACKAGE);
188 if (origImports != null)
189 {
190 final StringBuilder imports = new StringBuilder();
191 final Map<String,Map<String,String>> header = OsgiHeaderUtil.parseHeader(origImports);
192 for (final Map.Entry<String,Map<String,String>> pkgImport : header.entrySet())
193 {
194 String imp = null;
195 if (pkgImport.getValue().isEmpty())
196 {
197 final String export = exports.getFullExport(pkgImport.getKey());
198 if (!export.equals(imp))
199 {
200 imp = export;
201 }
202
203 }
204 if (imp == null)
205 {
206 imp = OsgiHeaderUtil.buildHeader(pkgImport.getKey(), pkgImport.getValue());
207 }
208 imports.append(imp);
209 imports.append(",");
210 }
211 if (imports.length() > 0)
212 {
213 imports.deleteCharAt(imports.length() - 1);
214 }
215
216 manifest.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, imports.toString());
217 }
218 }
219
220 private boolean isOsgiBundle(final Manifest manifest) throws IOException
221 {
222 return manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME) != null;
223 }
224
225 private String addExtraImports(String imports, final List<String> extraImports)
226 {
227 final StringBuilder referrers = new StringBuilder();
228 for (final String imp : extraImports)
229 {
230 referrers.append(imp).append(",");
231 }
232
233 if (imports != null && imports.length() > 0)
234 {
235 imports = referrers + imports;
236 }
237 else
238 {
239 imports = referrers.toString();
240 }
241 return imports;
242 }
243
244 private static void header(final Properties properties, final String key, final Object value)
245 {
246 if (value == null)
247 {
248 return;
249 }
250
251 if (value instanceof Collection && ((Collection) value).isEmpty())
252 {
253 return;
254 }
255
256 properties.put(key, value.toString().replaceAll("[\r\n]", ""));
257 }
258
259 private void assertSpringAvailableIfRequired(final TransformContext context)
260 {
261 if (context.shouldRequireSpring())
262 {
263 final String header = context.getManifest().getMainAttributes().getValue("Spring-Context");
264 if (header == null)
265 {
266 log.debug("The Spring Manifest header 'Spring-Context' is missing in jar '" +
267 context.getPluginArtifact().toString() + "'. If you experience any problems, please add it and set it to '" +
268 SPRING_CONTEXT_DEFAULT + "'");
269 }
270 else if (!header.contains(";timeout:=" + SPRING_TIMEOUT))
271 {
272 log.warn("The Spring Manifest header in jar '" + context.getPluginArtifact().toString() + "' isn't " +
273 "set for a " + SPRING_TIMEOUT + " second timeout waiting for dependencies. " +
274 "Please add ';timeout:=" + SPRING_TIMEOUT + "'");
275 }
276 }
277 }
278
279 }