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