View Javadoc

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   * Generates an OSGi manifest if not already defined.  Should be the last stage.
40   *
41   * @since 2.2.0
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              // We don't care about the modules, so we pass null
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                      // skip any manifest manipulation by bnd
84                      return;
85                  }
86                  else
87                  {
88                      // Possibly necessary due to Spring XML creation
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                 // Setup defaults
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                 // Don't export anything by default
117                 //properties.put(Analyzer.EXPORT_PACKAGE, "*");
118 
119                 properties.put(Analyzer.BUNDLE_VERSION, info.getVersion());
120 
121                 // remove the verbose Include-Resource entry from generated manifest
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                 // the jar root.
132                 bundleClassPaths.add(".");
133 
134                 // inner jars. make the order deterministic here.
135                 List<String> innerClassPaths = new ArrayList<String>(context.getBundleClassPathJars());
136                 Collections.sort(innerClassPaths);
137                 bundleClassPaths.addAll(innerClassPaths);
138 
139                 // generate bundle classpath.
140                 header(properties, Analyzer.BUNDLE_CLASSPATH, StringUtils.join(bundleClassPaths, ','));
141 
142                 // Process any bundle instructions in atlassian-plugin.xml
143                 properties.putAll(context.getBndInstructions());
144 
145                 // Add extra imports to the imports list
146                 properties.put(Analyzer.IMPORT_PACKAGE, addExtraImports(properties.getProperty(Analyzer.IMPORT_PACKAGE), context.getExtraImports()));
147 
148                 // Add extra exports to the exports list
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         // Check for the explicit context value
202         final String header = context.getManifest().getMainAttributes().getValue(SPRING_CONTEXT);
203         if (header != null)
204         {
205             return ensureDefaultTimeout(header);
206         }
207 
208         // Check for the spring files, as the default header value looks here
209         // TODO: This is probly not correct since if there is no META-INF/spring/*.xml, it's still not spring-powered.
210         if (context.getPluginArtifact().doesResourceExist("META-INF/spring/") ||
211             context.shouldRequireSpring() ||
212             context.getDescriptorDocument() != null)
213         {
214             return SPRING_CONTEXT_DEFAULT;
215         }
216         return null;
217     }
218 
219     private String ensureDefaultTimeout(final String header)
220     {
221         final boolean noTimeOutSpecified = StringUtils.isEmpty(System.getProperty(PluginUtils.ATLASSIAN_PLUGINS_ENABLE_WAIT));
222 
223         if (noTimeOutSpecified)
224         {
225             return header;
226         }
227         StringBuilder headerBuf;
228         //Override existing timeout
229         if (header.contains(SPRING_CONTEXT_TIMEOUT))
230         {
231             StringTokenizer tokenizer = new StringTokenizer(header, SPRING_CONTEXT_DELIM);
232             headerBuf = new StringBuilder();
233             while (tokenizer.hasMoreElements())
234             {
235                 String directive = (String) tokenizer.nextElement();
236                 if (directive.startsWith(SPRING_CONTEXT_TIMEOUT))
237                 {
238                     if (!directive.equals(SPRING_CONTEXT_TIMEOUT + PluginUtils.DEFAULT_ATLASSIAN_PLUGINS_ENABLE_WAIT_SECONDS))
239                     {
240                         log.debug("Overriding configured timeout {} seconds", directive.substring(SPRING_CONTEXT_TIMEOUT.length()));
241                     }
242                     directive = SPRING_CONTEXT_TIMEOUT + SPRING_TIMEOUT;
243                 }
244                 headerBuf.append(directive);
245                 if (tokenizer.hasMoreElements())
246                 {
247                     headerBuf.append(SPRING_CONTEXT_DELIM);
248                 }
249             }
250         }
251         else
252         {
253             //Append new timeout
254             headerBuf = new StringBuilder(header);
255             headerBuf.append(SPRING_CONTEXT_DELIM + SPRING_CONTEXT_TIMEOUT + SPRING_TIMEOUT);
256         }
257         return headerBuf.toString();
258     }
259 
260     private void validateOsgiVersionIsValid(Manifest mf)
261     {
262         String version = mf.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
263         try
264         {
265             if (Version.parseVersion(version) == Version.emptyVersion)
266             {
267                 // we still consider an empty version to be bad
268                 throw new IllegalArgumentException();
269             }
270         }
271         catch (IllegalArgumentException ex)
272         {
273             throw new IllegalArgumentException("Plugin version '" + version + "' is required and must be able to be " +
274                     "parsed as an OSGi version - MAJOR.MINOR.MICRO.QUALIFIER");
275         }
276     }
277 
278     private void writeManifestOverride(final TransformContext context, final Manifest mf)
279             throws IOException
280     {
281         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
282         mf.write(bout);
283         context.getFileOverrides().put("META-INF/MANIFEST.MF", bout.toByteArray());
284     }
285 
286     /**
287      * Scans for any imports with no version specified and locks them into the specific version exported by the host
288      * container
289      * @param manifest The manifest to read and manipulate
290      * @param exports The list of host exports
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         // OSGi core spec 4.2 section 3.5.2: The Bundle-SymbolicName manifest header is a mandatory header.
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                 // the extraImportPackage here can be in the form 'package;version=blah'. We only use the package component to check if it's already required.
341                 final String extraImportPackage = StringUtils.split(exImport, ';')[0];
342 
343                 Map attrs = originalImports.get(extraImportPackage);
344                 // if the package is already required by the import directive supplied by plugin developer, we use the supplied one.
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                 // otherwise, it is system determined.
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 }