View Javadoc

1   package com.atlassian.plugin.osgi.factory.transform;
2   
3   import com.atlassian.plugin.Application;
4   import com.atlassian.plugin.JarPluginArtifact;
5   import com.atlassian.plugin.PluginArtifact;
6   import com.atlassian.plugin.osgi.container.OsgiContainerManager;
7   import com.atlassian.plugin.osgi.container.OsgiPersistentCache;
8   import com.atlassian.plugin.osgi.factory.transform.model.SystemExports;
9   import com.atlassian.plugin.osgi.factory.transform.stage.AddBundleOverridesStage;
10  import com.atlassian.plugin.osgi.factory.transform.stage.ComponentImportSpringStage;
11  import com.atlassian.plugin.osgi.factory.transform.stage.ComponentSpringStage;
12  import com.atlassian.plugin.osgi.factory.transform.stage.GenerateManifestStage;
13  import com.atlassian.plugin.osgi.factory.transform.stage.HostComponentSpringStage;
14  import com.atlassian.plugin.osgi.factory.transform.stage.ModuleTypeSpringStage;
15  import com.atlassian.plugin.osgi.factory.transform.stage.ScanDescriptorForHostClassesStage;
16  import com.atlassian.plugin.osgi.factory.transform.stage.ScanInnerJarsStage;
17  import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
18  import com.google.common.collect.ImmutableList;
19  import org.apache.commons.io.IOUtils;
20  import org.slf4j.Logger;
21  import org.slf4j.LoggerFactory;
22  
23  import java.io.BufferedOutputStream;
24  import java.io.ByteArrayInputStream;
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.FileOutputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.zip.Deflater;
36  import java.util.zip.ZipEntry;
37  import java.util.zip.ZipInputStream;
38  import java.util.zip.ZipOutputStream;
39  
40  import static com.google.common.base.Preconditions.checkNotNull;
41  
42  /**
43   * Default implementation of plugin transformation that uses stages to convert a plain JAR into an OSGi bundle.
44   */
45  public class DefaultPluginTransformer implements PluginTransformer
46  {
47      private static final Logger log = LoggerFactory.getLogger(DefaultPluginTransformer.class);
48  
49      private final String pluginDescriptorPath;
50      private final List<TransformStage> stages;
51      private final File bundleCacheDir;
52      private final SystemExports systemExports;
53      private final Set<Application> applications;
54      private final OsgiContainerManager osgiContainerManager;
55  
56      /**
57       * Gets the default list of transform stages performed by the transformer. Clients wishing to add stages to the
58       * transformation process should use this list as a template rather than creating their own from scratch.
59       */
60      public static ArrayList<TransformStage> getDefaultTransformStages()
61      {
62          return new ArrayList<TransformStage>(Arrays.asList(
63                  new AddBundleOverridesStage(),
64                  new ScanInnerJarsStage(),
65                  new ComponentImportSpringStage(),
66                  new ComponentSpringStage(),
67                  new ScanDescriptorForHostClassesStage(),
68                  new ModuleTypeSpringStage(),
69                  new HostComponentSpringStage(),
70                  new GenerateManifestStage()
71          ));
72      }
73  
74      /**
75       * Constructs a transformer with the default stages
76       *
77       * @param cache The OSGi cache configuration for transformed plugins
78       * @param systemExports The packages the system bundle exports
79       * @param pluginDescriptorPath The path to the plugin descriptor
80       * @since 2.2.0
81       */
82      public DefaultPluginTransformer(OsgiPersistentCache cache, SystemExports systemExports, Set<Application> applications, String pluginDescriptorPath, OsgiContainerManager osgiContainerManager)
83      {
84          this(cache, systemExports, applications, pluginDescriptorPath, osgiContainerManager, getDefaultTransformStages());
85      }
86  
87      /**
88       * Constructs a transformer and its stages
89       *
90       * @param cache The OSGi cache configuration for transformed plugins
91       * @param systemExports The packages the system bundle exports
92       * @param pluginDescriptorPath The descriptor path
93       * @param stages A set of stages
94       * @since 2.2.0
95       */
96      public DefaultPluginTransformer(OsgiPersistentCache cache, SystemExports systemExports, Set<Application> applications, String pluginDescriptorPath, OsgiContainerManager osgiContainerManager, List<TransformStage> stages)
97      {
98          this.pluginDescriptorPath = checkNotNull(pluginDescriptorPath, "The plugin descriptor path is required");
99          this.osgiContainerManager = checkNotNull(osgiContainerManager);
100         this.stages = ImmutableList.copyOf(checkNotNull(stages, "A list of stages is required"));
101         this.bundleCacheDir = checkNotNull(cache).getTransformedPluginCache();
102         this.systemExports = systemExports;
103         this.applications = applications;
104     }
105 
106     /**
107      * Transforms the file into an OSGi bundle
108      *
109      * @param pluginJar The plugin jar
110      * @param regs      The list of registered host components
111      * @return The new OSGi-enabled plugin jar
112      * @throws PluginTransformationException If anything goes wrong
113      */
114     public File transform(File pluginJar, List<HostComponentRegistration> regs) throws PluginTransformationException
115     {
116         return transform(new JarPluginArtifact(pluginJar), regs);
117     }
118 
119     /**
120      * Transforms the file into an OSGi bundle
121      *
122      * @param pluginArtifact The plugin artifact, usually a jar
123      * @param regs      The list of registered host components
124      * @return The new OSGi-enabled plugin jar
125      * @throws PluginTransformationException If anything goes wrong
126      */
127     public File transform(PluginArtifact pluginArtifact, List<HostComponentRegistration> regs) throws PluginTransformationException
128     {
129         checkNotNull(pluginArtifact, "The plugin artifact is required");
130         checkNotNull(regs, "The host component registrations are required");
131 
132         File artifactFile = pluginArtifact.toFile();
133 
134         // Look in cache first
135         File cachedPlugin = getFromCache(artifactFile);
136         if (cachedPlugin != null)
137         {
138             return cachedPlugin;
139         }
140 
141         final TransformContext context = new TransformContext(regs, systemExports, pluginArtifact, applications, pluginDescriptorPath, osgiContainerManager);
142         for (TransformStage stage : stages)
143         {
144             stage.execute(context);
145         }
146 
147         // Create a new jar by overriding the specified files
148         try
149         {
150             if (log.isDebugEnabled())
151             {
152                 StringBuilder sb = new StringBuilder();
153                 sb.append("Overriding files in ").append(pluginArtifact.toString()).append(":\n");
154                 for (Map.Entry<String, byte[]> entry : context.getFileOverrides().entrySet())
155                 {
156                     sb.append("==").append(entry.getKey()).append("==\n");
157 
158                     // Yes, this doesn't take into account encoding, but since only text files are overridden, that
159                     // should be fine
160                     sb.append(new String(entry.getValue()));
161                 }
162                 log.debug(sb.toString());
163             }
164             return addFilesToExistingZip(artifactFile, context.getFileOverrides());
165         }
166         catch (IOException e)
167         {
168             throw new PluginTransformationException("Unable to add files to plugin jar");
169         }
170     }
171 
172     private File getFromCache(File artifact)
173     {
174         String name = generateCacheName(artifact);
175         for (File child : bundleCacheDir.listFiles())
176         {
177             if (child.getName().equals(name))
178                 return child;
179         }
180         return null;
181     }
182 
183     /**
184      * Generate a cache name that incorporates the timestap and preserves the extension
185      * @param file The original file to cache
186      * @return The new file name
187      */
188     static String generateCacheName(File file)
189     {
190         int dotPos = file.getName().lastIndexOf('.');
191         if (dotPos > 0 && file.getName().length() - 1 > dotPos)
192         {
193             return file.getName().substring(0, dotPos) + "_" + file.lastModified() + file.getName().substring(dotPos);
194         }
195         else
196         {
197             return file.getName() + "_" + file.lastModified();
198         }
199     }
200 
201 
202     /**
203      * Creates a new jar by overriding the specified files in the existing one
204      *
205      * @param zipFile The existing zip file
206      * @param files   The files to override
207      * @return The new zip
208      * @throws IOException If there are any problems processing the streams
209      */
210     File addFilesToExistingZip(File zipFile,
211                                       Map<String, byte[]> files) throws IOException
212     {
213         // get a temp file
214         File tempFile = new File(bundleCacheDir, generateCacheName(zipFile));
215         
216         ZipInputStream zin = null;
217         ZipOutputStream out = null;
218         try
219         {
220             zin = new ZipInputStream(new FileInputStream(zipFile));
221             out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tempFile)));
222             out.setLevel(Deflater.NO_COMPRESSION);
223 
224             ZipEntry entry = zin.getNextEntry();
225             while (entry != null)
226             {
227                 String name = entry.getName();
228                 if (!files.containsKey(name))
229                 {
230                     // Add ZIP entry to output stream.
231                     out.putNextEntry(new ZipEntry(name));
232                     // Transfer bytes from the ZIP file to the output file
233                     IOUtils.copyLarge(zin,out);
234                 }
235                 entry = zin.getNextEntry();
236             }
237             // Close the streams
238             zin.close();
239             // Compress the files
240             for (Map.Entry<String, byte[]> fentry : files.entrySet())
241             {
242                 InputStream in = null;
243                 try
244                 {
245                     in = new ByteArrayInputStream(fentry.getValue());
246                     // Add ZIP entry to output stream.
247                     out.putNextEntry(new ZipEntry(fentry.getKey()));
248                     // Transfer bytes from the file to the ZIP file
249                     IOUtils.copyLarge(in,out);
250                     // Complete the entry
251                     out.closeEntry();
252                 }
253                 finally
254                 {
255                     IOUtils.closeQuietly(in);
256                 }
257             }
258             // Complete the ZIP file
259             out.close();
260         }
261         finally
262         {
263             // Close just in case
264             IOUtils.closeQuietly(zin);
265             IOUtils.closeQuietly(out);
266         }
267         return tempFile;
268     }
269 
270 }