package com.atlassian.labs.speakeasy.manager.convention; import com.atlassian.labs.speakeasy.manager.convention.external.ConventionDescriptorGenerator; import com.atlassian.labs.speakeasy.manager.PluginOperationFailedException; import com.atlassian.labs.speakeasy.model.JsonManifest; import com.atlassian.plugin.JarPluginArtifact; import com.atlassian.plugin.PluginArtifact; import com.atlassian.plugin.osgi.factory.OsgiPlugin; import com.atlassian.plugin.util.PluginUtils; import org.apache.commons.io.IOUtils; import org.osgi.framework.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.*; import java.util.Map; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import static com.google.common.collect.Maps.newHashMap; /** * */ @Component public class ZipTransformer { private final JsonManifestHandler jsonHandler; private final JsonToElementParser jsonToElementParser; @Autowired public ZipTransformer(JsonManifestHandler jsonHandler, JsonToElementParser jsonToElementParser) { this.jsonHandler = jsonHandler; this.jsonToElementParser = jsonToElementParser; } public JarPluginArtifact convertConventionZipToPluginJar(JsonManifest descriptor, PluginArtifact artifact) { Map<String,byte[]> additions = newHashMap(); additions.put("META-INF/MANIFEST.MF", generateManifest(descriptor)); additions.put("META-INF/spring/speakeasy-context.xml", getResourceContents("speakeasy-context.xml")); // this exists to force parse errors to happen earlier jsonToElementParser.createWebItems(artifact.getResourceAsStream("ui/web-items.json")); try { return new JarPluginArtifact(addFilesToExistingZip(artifact.toFile(), additions)); } catch (IOException e) { throw new PluginOperationFailedException("Unable to transform zip", e, descriptor.getKey()); } } public String extractPluginKey(PluginArtifact pluginArtifact) { if (pluginArtifact.doesResourceExist(JsonManifest.ATLASSIAN_EXTENSION_PATH)) { JsonManifest descriptor = jsonHandler.read(pluginArtifact); return descriptor.getKey(); } return null; } public JsonManifest readManifest(String key, InputStream in) { return jsonHandler.read(key, in); } public void writeManifest(JsonManifest manifest, OutputStream out) throws IOException { jsonHandler.write(manifest, out); } private byte[] getResourceContents(String path) { ByteArrayOutputStream bout = new ByteArrayOutputStream(); InputStream in = null; try { in = getClass().getResourceAsStream(path); IOUtils.copy(in, bout); return bout.toByteArray(); } catch (IOException e) { throw new RuntimeException("Should never happen", e); } finally { IOUtils.closeQuietly(in); } } private byte[] generateManifest(JsonManifest descriptor) { Manifest mf = new Manifest(); mf.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0"); mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); mf.getMainAttributes().putValue(OsgiPlugin.ATLASSIAN_PLUGIN_KEY, descriptor.getKey()); mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, descriptor.getKey()); mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, cleanManifestValue(descriptor.getVersion())); mf.getMainAttributes().putValue(Constants.BUNDLE_NAME, cleanManifestValue(descriptor.getName())); mf.getMainAttributes().putValue(Constants.BUNDLE_DESCRIPTION, cleanManifestValue(descriptor.getDescription())); mf.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, ConventionDescriptorGenerator.class.getPackage().getName()); mf.getMainAttributes().putValue("Spring-Context", "*;timeout:=" + PluginUtils.getDefaultEnablingWaitPeriod()); if (descriptor.getVendor() != null) { mf.getMainAttributes().putValue(Constants.BUNDLE_VENDOR, descriptor.getVendor().getName()); } ByteArrayOutputStream bout = new ByteArrayOutputStream(); try { mf.write(bout); } catch (IOException e) { throw new RuntimeException("Should never happen", e); } return bout.toByteArray(); } private String cleanManifestValue(String name) { return name == null ? null : name.replaceAll("\n|\r", " "); } /** * Creates a new jar by overriding the specified files in the existing one * * @param zipFile The existing zip file * @param files The files to override * @return The new zip * @throws IOException If there are any problems processing the streams */ File addFilesToExistingZip(File zipFile, Map<String, byte[]> files) throws IOException { File tempFile = new File(zipFile.getPath() + ".jar"); byte[] buf = new byte[64 * 1024]; ZipInputStream zin = null; ZipOutputStream out = null; try { zin = new ZipInputStream(new FileInputStream(zipFile)); out = new ZipOutputStream(new FileOutputStream(tempFile)); out.setLevel(Deflater.NO_COMPRESSION); ZipEntry entry = zin.getNextEntry(); while (entry != null) { String name = entry.getName(); if (!files.containsKey(name)) { // Add ZIP entry to output stream. out.putNextEntry(new ZipEntry(name)); // Transfer bytes from the ZIP file to the output file int len; while ((len = zin.read(buf)) > 0) out.write(buf, 0, len); } entry = zin.getNextEntry(); } // Close the streams zin.close(); // Compress the files for (Map.Entry<String, byte[]> fentry : files.entrySet()) { InputStream in = null; try { in = new ByteArrayInputStream(fentry.getValue()); // Add ZIP entry to output stream. out.putNextEntry(new ZipEntry(fentry.getKey())); // Transfer bytes from the file to the ZIP file int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } // Complete the entry out.closeEntry(); } finally { IOUtils.closeQuietly(in); } } // Complete the ZIP file out.close(); zipFile.delete(); } finally { // Close just in case IOUtils.closeQuietly(zin); IOUtils.closeQuietly(out); } return tempFile; } }