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   * Generates an OSGi manifest if not already defined.  Should be the last stage.
35   *
36   * @since 2.2.0
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              // We don't care about the modules, so we pass null
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                      // skip any manifest manipulation by bnd
63                      return;
64                  }
65                  else
66                  {
67                      // Possibly necessary due to Spring XML creation
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                  // Setup defaults
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                  // Don't export anything by default
92                  //properties.put(Analyzer.EXPORT_PACKAGE, "*");
93  
94                  properties.put(Analyzer.BUNDLE_VERSION, info.getVersion());
95  
96                  // remove the verbose Include-Resource entry from generated manifest
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                 // Scan for embedded jars
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                 // Process any bundle instructions in atlassian-plugin.xml
118                 properties.putAll(context.getBndInstructions());
119 
120                 // Add extra imports to the imports list
121                 properties.put(Analyzer.IMPORT_PACKAGE, addExtraImports(properties.getProperty(Analyzer.IMPORT_PACKAGE), context.getExtraImports()));
122 
123                 // Add extra exports to the exports list
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                 // we still consider an empty version to be bad
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      * Scans for any imports with no version specified and locks them into the specific version exported by the host
181      * container
182      * @param manifest The manifest to read and manipulate
183      * @param exports The list of host exports
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 }