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