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.SortedMap;
36  import java.util.TreeMap;
37  import java.util.zip.Deflater;
38  import java.util.zip.ZipEntry;
39  import java.util.zip.ZipInputStream;
40  import java.util.zip.ZipOutputStream;
41  
42  import static com.google.common.base.Preconditions.checkNotNull;
43  
44  /**
45   * Default implementation of plugin transformation that uses stages to convert a plain JAR into an OSGi bundle.
46   */
47  public class DefaultPluginTransformer implements PluginTransformer {
48      private static final Logger log = LoggerFactory.getLogger(DefaultPluginTransformer.class);
49  
50      public static final String TRANSFORM_COMPRESSION_LEVEL = "atlassian.plugins.plugin.transformer.compression";
51  
52      private final String pluginDescriptorPath;
53      private final List<TransformStage> stages;
54      private final File bundleCacheDir;
55      private final SystemExports systemExports;
56      private final Set<Application> applications;
57      private final OsgiContainerManager osgiContainerManager;
58  
59      /**
60       * Gets the default list of transform stages performed by the transformer. Clients wishing to add stages to the
61       * transformation process should use this list as a template rather than creating their own from scratch.
62       */
63      public static ArrayList<TransformStage> getDefaultTransformStages() {
64          return new ArrayList<>(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          this(cache, systemExports, applications, pluginDescriptorPath, osgiContainerManager, getDefaultTransformStages());
86      }
87  
88      /**
89       * Constructs a transformer and its stages
90       *
91       * @param cache                The OSGi cache configuration for transformed plugins
92       * @param systemExports        The packages the system bundle exports
93       * @param pluginDescriptorPath The descriptor path
94       * @param stages               A set of stages
95       * @since 2.2.0
96       */
97      public DefaultPluginTransformer(OsgiPersistentCache cache, SystemExports systemExports, Set<Application> applications, String pluginDescriptorPath, OsgiContainerManager osgiContainerManager, List<TransformStage> stages) {
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         return transform(new JarPluginArtifact(pluginJar), regs);
116     }
117 
118     /**
119      * Transforms the file into an OSGi bundle
120      *
121      * @param pluginArtifact The plugin artifact, usually a jar
122      * @param regs           The list of registered host components
123      * @return The new OSGi-enabled plugin jar
124      * @throws PluginTransformationException If anything goes wrong
125      */
126     public File transform(PluginArtifact pluginArtifact, List<HostComponentRegistration> regs) throws PluginTransformationException {
127         checkNotNull(pluginArtifact, "The plugin artifact is required");
128         checkNotNull(regs, "The host component registrations are required");
129 
130         File artifactFile = pluginArtifact.toFile();
131 
132         // Look in cache first
133         File cachedPlugin = getFromCache(artifactFile);
134         if (cachedPlugin != null) {
135             return cachedPlugin;
136         }
137 
138         final TransformContext context = new TransformContext(regs, systemExports, pluginArtifact, applications, pluginDescriptorPath, osgiContainerManager);
139         for (TransformStage stage : stages) {
140             stage.execute(context);
141         }
142 
143         // Create a new jar by overriding the specified files
144         try {
145             if (log.isDebugEnabled()) {
146                 StringBuilder sb = new StringBuilder();
147                 sb.append("Overriding files in ").append(pluginArtifact.toString()).append(":\n");
148                 for (Map.Entry<String, byte[]> entry : context.getFileOverrides().entrySet()) {
149                     sb.append("==").append(entry.getKey()).append("==\n");
150 
151                     // Yes, this doesn't take into account encoding, but since only text files are overridden, that
152                     // should be fine
153                     sb.append(new String(entry.getValue()));
154                 }
155                 log.debug(sb.toString());
156             }
157             return addFilesToExistingZip(artifactFile, context.getFileOverrides());
158         } catch (IOException e) {
159             throw new PluginTransformationException("Unable to add files to plugin jar", e);
160         }
161     }
162 
163     private File getFromCache(File artifact) {
164         String name = generateCacheName(artifact);
165         File[] files = bundleCacheDir.listFiles();
166         if (files == null) {
167             return null;
168         }
169         for (File child : files) {
170             if (child.getName().equals(name))
171                 return child;
172         }
173         return null;
174     }
175 
176     /**
177      * Generate a cache name that incorporates the timestap and preserves the extension
178      *
179      * @param file The original file to cache
180      * @return The new file name
181      */
182     static String generateCacheName(File file) {
183         int dotPos = file.getName().lastIndexOf('.');
184         if (dotPos > 0 && file.getName().length() - 1 > dotPos) {
185             return file.getName().substring(0, dotPos) + "_" + file.lastModified() + file.getName().substring(dotPos);
186         } else {
187             return file.getName() + "_" + file.lastModified();
188         }
189     }
190 
191 
192     /**
193      * Creates a new jar by overriding the specified files in the existing one
194      *
195      * @param zipFile The existing zip file
196      * @param files   The files to override
197      * @return The new zip
198      * @throws IOException If there are any problems processing the streams
199      */
200     File addFilesToExistingZip(File zipFile,
201                                Map<String, byte[]> files) throws IOException {
202         // get a temp file
203         File tempFile = new File(bundleCacheDir, generateCacheName(zipFile));
204 
205         ZipInputStream zin = null;
206         ZipOutputStream out = null;
207         try {
208             zin = new ZipInputStream(new FileInputStream(zipFile));
209             out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tempFile)));
210             final int requestedCompressionLevel = Integer.getInteger(TRANSFORM_COMPRESSION_LEVEL, Deflater.NO_COMPRESSION);
211             final int clampedCompressionLevel =
212                     Math.max(Deflater.NO_COMPRESSION, Math.min(requestedCompressionLevel, Deflater.BEST_COMPRESSION));
213             out.setLevel(clampedCompressionLevel);
214 
215             // Copy any entries that are unmodified from the existing JAR to the new JAR
216             ZipEntry entry = zin.getNextEntry();
217             while (entry != null) {
218                 String name = entry.getName();
219                 if (!files.containsKey(name)) {
220                     // Add ZIP entry to output stream, preserve existing modification time
221                     final ZipEntry newEntry = new ZipEntry(name);
222                     newEntry.setTime(entry.getTime());
223                     out.putNextEntry(newEntry);
224 
225                     // Transfer bytes from the ZIP file to the output file
226                     IOUtils.copyLarge(zin, out);
227                 }
228                 entry = zin.getNextEntry();
229             }
230             // Close the streams
231             zin.close();
232 
233             // Override/append any entries present in files
234             final SortedMap<String, byte[]> sortedFiles = new TreeMap<>(files);
235             for (Map.Entry<String, byte[]> fentry : sortedFiles.entrySet()) {
236                 InputStream in = null;
237                 try {
238                     in = new ByteArrayInputStream(fentry.getValue());
239 
240                     // Add ZIP entry to output stream, use the last modified time of the original jar
241                     // for any new files
242                     final ZipEntry newEntry = new ZipEntry(fentry.getKey());
243                     newEntry.setTime(zipFile.lastModified());
244                     out.putNextEntry(newEntry);
245 
246                     // Transfer bytes from the file to the ZIP file
247                     IOUtils.copyLarge(in, out);
248 
249                     // Complete the entry
250                     out.closeEntry();
251                 } finally {
252                     IOUtils.closeQuietly(in);
253                 }
254             }
255             // Complete the ZIP file
256             out.close();
257         } finally {
258             // Close just in case
259             IOUtils.closeQuietly(zin);
260             IOUtils.closeQuietly(out);
261         }
262         return tempFile;
263     }
264 
265 }