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         else
228         {
229             StringBuilder headerBuf;
230             //Override existing timeout
231             if (header.contains(SPRING_CONTEXT_TIMEOUT))
232             {
233                 StringTokenizer tokenizer = new StringTokenizer(header, SPRING_CONTEXT_DELIM);
234                 headerBuf = new StringBuilder();
235                 while (tokenizer.hasMoreElements())
236                 {
237                     String directive = (String) tokenizer.nextElement();
238                     if (directive.startsWith(SPRING_CONTEXT_TIMEOUT))
239                     {
240                         directive = SPRING_CONTEXT_TIMEOUT + SPRING_TIMEOUT;
241                     }
242                     headerBuf.append(directive);
243                     if (tokenizer.hasMoreElements())
244                     {
245                         headerBuf.append(SPRING_CONTEXT_DELIM);
246                     }
247                 }
248             }
249             else
250             {
251                 //Append new timeout
252                 headerBuf = new StringBuilder(header);
253                 headerBuf.append(SPRING_CONTEXT_DELIM + SPRING_CONTEXT_TIMEOUT + SPRING_TIMEOUT);
254             }
255             return headerBuf.toString();
256         }
257     }
258 
259     private void validateOsgiVersionIsValid(Manifest mf)
260     {
261         String version = mf.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
262         try
263         {
264             if (Version.parseVersion(version) == Version.emptyVersion)
265             {
266                 // we still consider an empty version to be bad
267                 throw new IllegalArgumentException();
268             }
269         }
270         catch (IllegalArgumentException ex)
271         {
272             throw new IllegalArgumentException("Plugin version '" + version + "' is required and must be able to be " +
273                     "parsed as an OSGi version - MAJOR.MINOR.MICRO.QUALIFIER");
274         }
275     }
276 
277     private void writeManifestOverride(final TransformContext context, final Manifest mf)
278             throws IOException
279     {
280         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
281         mf.write(bout);
282         context.getFileOverrides().put("META-INF/MANIFEST.MF", bout.toByteArray());
283     }
284 
285     /**
286      * Scans for any imports with no version specified and locks them into the specific version exported by the host
287      * container
288      * @param manifest The manifest to read and manipulate
289      * @param exports The list of host exports
290      */
291     private void enforceHostVersionsForUnknownImports(final Manifest manifest, final SystemExports exports)
292     {
293         final String origImports = manifest.getMainAttributes().getValue(Constants.IMPORT_PACKAGE);
294         if (origImports != null)
295         {
296             final StringBuilder imports = new StringBuilder();
297             final Map<String,Map<String,String>> header = OsgiHeaderUtil.parseHeader(origImports);
298             for (final Map.Entry<String,Map<String,String>> pkgImport : header.entrySet())
299             {
300                 String imp = null;
301                 if (pkgImport.getValue().isEmpty())
302                 {
303                     final String export = exports.getFullExport(pkgImport.getKey());
304                     if (!export.equals(imp))
305                     {
306                         imp = export;
307                     }
308 
309                 }
310                 if (imp == null)
311                 {
312                     imp = OsgiHeaderUtil.buildHeader(pkgImport.getKey(), pkgImport.getValue());
313                 }
314                 imports.append(imp);
315                 imports.append(",");
316             }
317             if (imports.length() > 0)
318             {
319                 imports.deleteCharAt(imports.length() - 1);
320             }
321 
322             manifest.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, imports.toString());
323         }
324     }
325 
326     private boolean isOsgiBundle(final Manifest manifest) throws IOException
327     {
328         // OSGi core spec 4.2 section 3.5.2: The Bundle-SymbolicName manifest header is a mandatory header.
329         return manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME) != null;
330     }
331 
332     private String addExtraImports(String importsLine, final List<String> extraImports)
333     {
334         Map<String,Map<String,String>> originalImports = OsgiHeaderUtil.parseHeader(importsLine);
335         for (String exImport : extraImports)
336         {
337             if (!exImport.startsWith("java."))
338             {
339                 // the extraImportPackage here can be in the form 'package;version=blah'. We only use the package component to check if it's already required.
340                 final String extraImportPackage = StringUtils.split(exImport, ';')[0];
341 
342                 Map attrs = originalImports.get(extraImportPackage);
343                 // if the package is already required by the import directive supplied by plugin developer, we use the supplied one.
344                 if (attrs != null)
345                 {
346                     Object resolution = attrs.get(RESOLUTION_DIRECTIVE);
347                     if (Constants.RESOLUTION_OPTIONAL.equals(resolution))
348                     {
349                         attrs.put(RESOLUTION_DIRECTIVE, Constants.RESOLUTION_MANDATORY);
350                     }
351                 }
352                 // otherwise, it is system determined.
353                 else
354                 {
355                     originalImports.put(exImport, Collections.<String, String>emptyMap());
356                 }
357             }
358         }
359 
360         return OsgiHeaderUtil.buildHeader(originalImports);
361     }
362 
363     private boolean manifestDoesntHaveRequiredOsgiHeader(Manifest mf, Entry<String, String> entry)
364     {
365         if (mf.getMainAttributes().containsKey(new Attributes.Name(entry.getKey())))
366         {
367             return !entry.getValue().equals(mf.getMainAttributes().getValue(entry.getKey()));
368         }
369         return true;
370     }
371 
372     private static void header(final Properties properties, final String key, final Object value)
373     {
374         if (value == null)
375         {
376             return;
377         }
378 
379         if (value instanceof Collection && ((Collection) value).isEmpty())
380         {
381             return;
382         }
383 
384         properties.put(key, value.toString().replaceAll("[\r\n]", ""));
385     }
386 
387     private void assertSpringAvailableIfRequired(final TransformContext context)
388     {
389         if (context.shouldRequireSpring())
390         {
391             final String header = context.getManifest().getMainAttributes().getValue(SPRING_CONTEXT);
392             if (header == null)
393             {
394                 log.debug("The Spring Manifest header 'Spring-Context' is missing in jar '" +
395                         context.getPluginArtifact().toString() + "'.  If you experience any problems, please add it and set it to '" +
396                         SPRING_CONTEXT_DEFAULT + "'");
397             }
398             else if (!header.contains(";timeout:=" + SPRING_TIMEOUT))
399             {
400                 log.warn("The Spring Manifest header in jar '" +  context.getPluginArtifact().toString() + "' isn't " +
401                         "set for a " + SPRING_TIMEOUT + " second timeout waiting for  dependencies.  " +
402                         "Please add ';timeout:=" + SPRING_TIMEOUT + "'");
403             }
404         }
405     }
406 
407 }