/* * Copyright (C) 2014-2015 CS SI * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package org.esa.snap.modules; import org.esa.snap.core.gpf.descriptor.ToolAdapterOperatorDescriptor; import org.esa.snap.core.gpf.operators.tooladapter.ToolAdapterIO; import java.io.*; import java.net.URL; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import java.util.jar.*; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Utility class for creating at runtime a jar module * for a tool adapter, so that it can be independently deployed. * * @author Cosmin Cara */ public final class ModulePackager { private static final Manifest _manifest; private static final Attributes.Name ATTR_DESCRIPTION_NAME; private static final Attributes.Name ATTR_MODULE_NAME; private static final Attributes.Name ATTR_MODULE_TYPE; private static final Attributes.Name ATTR_MODULE_VERSION; private static final Attributes.Name ATTR_MODULE_ALIAS; private static final File modulesPath; private static final String layerXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE filesystem PUBLIC \"-//NetBeans//DTD Filesystem 1.1//EN\" \"http://www.netbeans.org/dtds/filesystem-1_1.dtd\">\n" + "<filesystem>\n" + " <folder name=\"Actions\">\n" + " <folder name=\"Tools\">\n" + " <file name=\"org-esa-snap-ui-tooladapter-actions-ExecuteToolAdapterAction.instance\"/>\n" + " <attr name=\"displayName\" stringvalue=\"#NAME#\"/>\n" + " <attr name=\"instanceCreate\" methodvalue=\"org.openide.awt.Actions.alwaysEnabled\"/>\n" + " </folder>\n" + " </folder>\n" + " <folder name=\"Menu\">\n" + " <folder name=\"Tools\">\n" + " <folder name=\"External Tools\">\n" + " <file name=\"org-esa-snap-ui-tooladapter-actions-ExecuteToolAdapterAction.shadow\">\n" + " <attr name=\"originalFile\" stringvalue=\"Actions/Tools/org-esa-snap-ui-tooladapter-actions-ExecuteToolAdapterAction.instance\"/>\n" + " <attr name=\"position\" intvalue=\"1000\"/>\n" + " </file>\n" + " </folder>\n" + " </folder>\n" + " </folder>\n" + "</filesystem>"; private static final String LAYER_XML_PATH = "org/esa/snap/ui/tooladapter/layer.xml"; static { _manifest = new Manifest(); Attributes attributes = _manifest.getMainAttributes(); ATTR_DESCRIPTION_NAME = new Attributes.Name("OpenIDE-Module-Short-Description"); ATTR_MODULE_NAME = new Attributes.Name("OpenIDE-Module"); ATTR_MODULE_TYPE = new Attributes.Name("OpenIDE-Module-Type"); ATTR_MODULE_VERSION = new Attributes.Name("OpenIDE-Module-Implementation-Version"); ATTR_MODULE_ALIAS = new Attributes.Name("OpenIDE-Module-Alias"); attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); attributes.put(new Attributes.Name("OpenIDE-Module-Java-Dependencies"), "Java > 1.8"); attributes.put(new Attributes.Name("OpenIDE-Module-Module-Dependencies"), "org.esa.snap.snap.sta, org.esa.snap.snap.sta.ui"); attributes.put(new Attributes.Name("OpenIDE-Module-Display-Category"), "SNAP"); attributes.put(ATTR_MODULE_TYPE, "STA"); //attributes.put(new Attributes.Name("OpenIDE-Module-Layer"), LAYER_XML_PATH); attributes.put(ATTR_DESCRIPTION_NAME, "External tool adapter"); modulesPath = ToolAdapterIO.getAdaptersPath().toFile(); } /** * Packs the files associated with the given tool adapter operator descriptor into * a NetBeans module file (nbm) * * @param descriptor The tool adapter descriptor * @param nbmFile The target module file * @throws IOException */ public static void packModule(ToolAdapterOperatorDescriptor descriptor, File nbmFile) throws IOException { StringBuilder xmlBuilder = new StringBuilder(); byte[] byteBuffer = null; try (final ZipOutputStream zipStream = new ZipOutputStream(new FileOutputStream(nbmFile))) { // create Info section ZipEntry entry = new ZipEntry("Info/info.xml"); zipStream.putNextEntry(entry); xmlBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") .append("<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Autoupdate Module Info 2.5//EN\" \"http://www.netbeans.org/dtds/autoupdate-info-2_5.dtd\">"); xmlBuilder.append("<module codenamebase=\"") .append(descriptor.getName().toLowerCase()) .append("\" distribution=\"") .append(nbmFile.getName()) .append("\" downloadsize=\"0\" homepage=\"https://github.com/senbox-org/s2tbx\" needsrestart=\"true\" releasedate=\"") .append(new SimpleDateFormat("yyyy/MM/dd").format(new Date())) .append("\">\n") .append("<manifest AutoUpdate-Essential-Module=\"true\" AutoUpdate-Show-In-Client=\"false\" OpenIDE-Module=\"") .append(descriptor.getName()) .append("\" OpenIDE-Module-Display-Category=\"SNAP\" OpenIDE-Module-Implementation-Version=\"2.0.0-") .append(new SimpleDateFormat("yyyyMMdd").format(new Date())) .append("\" OpenIDE-Module-Java-Dependencies=\"Java > 1.8\" OpenIDE-Module-Long-Description=\"<p>") .append(descriptor.getDescription()) .append("</p>\" OpenIDE-Module-Module-Dependencies=\"org.esa.snap.snap.sta > 2.0.0, org.esa.snap.snap.sta.ui > 2.0.0, org.esa.snap.snap.rcp > 2.0.0, org.esa.snap.snap.core > 2.0.0\" OpenIDE-Module-Name=\"") .append(descriptor.getName()) .append("\" OpenIDE-Module-Requires=\"org.openide.modules.ModuleFormat1\" OpenIDE-Module-Short-Description=\"") .append(descriptor.getDescription()) .append("\" OpenIDE-Module-Specification-Version=\"2.0.0\"/>\n</module>"); byteBuffer = xmlBuilder.toString().getBytes(); zipStream.write(byteBuffer, 0, byteBuffer.length); zipStream.closeEntry(); // create META-INF section xmlBuilder.setLength(0); entry = new ZipEntry("META-INF/MANIFEST.MF"); zipStream.putNextEntry(entry); xmlBuilder.append("Manifest-Version: 1.0\nCreated-By: 1.8.0_31-b13 (Oracle Corporation)\n"); byteBuffer = xmlBuilder.toString().getBytes(); zipStream.write(byteBuffer, 0, byteBuffer.length); zipStream.closeEntry(); String jarName = descriptor.getName().replace(".", "-") + ".jar"; // create config section xmlBuilder.setLength(0); entry = new ZipEntry("netbeans/config/Modules/" + descriptor.getName().replace(".", "-") + ".xml"); zipStream.putNextEntry(entry); xmlBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>") .append("<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"\n\"http://www.netbeans.org/dtds/module-status-1_0.dtd\">\n") .append("<module name=\"") .append(descriptor.getName()) .append("\">\n<param name=\"autoload\">false</param><param name=\"eager\">false</param><param name=\"enabled\">true</param>\n") .append("<param name=\"jar\">modules/") .append(jarName) .append("</param><param name=\"reloadable\">false</param>\n</module>"); byteBuffer = xmlBuilder.toString().getBytes(); zipStream.write(byteBuffer, 0, byteBuffer.length); zipStream.closeEntry(); // create modules section xmlBuilder.setLength(0); entry = new ZipEntry("netbeans/modules/ext/"); zipStream.putNextEntry(entry); zipStream.closeEntry(); entry = new ZipEntry("netbeans/modules/" + jarName); zipStream.putNextEntry(entry); zipStream.write(packAdapterJar(descriptor)); zipStream.closeEntry(); // create update_tracking section entry = new ZipEntry("netbeans/update_tracking/"); zipStream.putNextEntry(entry); zipStream.closeEntry(); } } /** * Unpacks a jar file into the user modules location. * * @param jarFile The jar file to be unpacked * @param unpackFolder The destination folder. If null, then the jar name will be used * @throws IOException */ public static void unpackAdapterJar(File jarFile, File unpackFolder) throws IOException { JarFile jar = new JarFile(jarFile); Enumeration enumEntries = jar.entries(); if (unpackFolder == null) { unpackFolder = new File(modulesPath, jarFile.getName().replace(".jar", "")); } if (!unpackFolder.exists()) unpackFolder.mkdir(); Attributes attributes = jar.getManifest().getMainAttributes(); if (attributes.containsKey(ATTR_MODULE_VERSION)) { String version = attributes.getValue(ATTR_MODULE_VERSION); File versionFile = new File(unpackFolder, "version.txt"); try (FileOutputStream fos = new FileOutputStream(versionFile)) { fos.write(version.getBytes()); fos.close(); } } while (enumEntries.hasMoreElements()) { JarEntry file = (JarEntry) enumEntries.nextElement(); File f = new File(unpackFolder, file.getName()); if (file.isDirectory()) { f.mkdir(); continue; } else { f.getParentFile().mkdirs(); } try (InputStream is = jar.getInputStream(file)) { try (FileOutputStream fos = new FileOutputStream(f)) { while (is.available() > 0) { fos.write(is.read()); } fos.close(); } is.close(); } } } public static String getAdapterVersion(File jarFile) throws IOException { String version = null; JarFile jar = new JarFile(jarFile); Attributes attributes = jar.getManifest().getMainAttributes(); if (attributes.containsKey(ATTR_MODULE_VERSION)) { version = attributes.getValue(ATTR_MODULE_VERSION); } jar.close(); return version; } public static String getAdapterAlias(File jarFile) throws IOException { String version = null; JarFile jar = new JarFile(jarFile); Attributes attributes = jar.getManifest().getMainAttributes(); if (attributes.containsKey(ATTR_MODULE_ALIAS)) { version = attributes.getValue(ATTR_MODULE_ALIAS); } jar.close(); return version; } private static byte[] packAdapterJar(ToolAdapterOperatorDescriptor descriptor) throws IOException { _manifest.getMainAttributes().put(ATTR_DESCRIPTION_NAME, descriptor.getAlias()); _manifest.getMainAttributes().put(ATTR_MODULE_NAME, descriptor.getName()); _manifest.getMainAttributes().put(ATTR_MODULE_VERSION, descriptor.getVersion()); _manifest.getMainAttributes().put(ATTR_MODULE_ALIAS, descriptor.getAlias()); File moduleFolder = new File(modulesPath, descriptor.getAlias()); ByteArrayOutputStream fOut = new ByteArrayOutputStream(); //_manifest.getMainAttributes().put(new Attributes.Name("OpenIDE-Module-Install"), ModuleInstaller.class.getName().replace('.', '/') + ".class"); try (JarOutputStream jarOut = new JarOutputStream(fOut, _manifest)) { File[] files = moduleFolder.listFiles(); if (files != null) { for (File child : files) { try { // ModuleInstaller from adapter folder should not be included if (child.getName().endsWith("ModuleInstaller.class")) { child.delete(); } else { addFile(child, jarOut); } } catch (Exception ignored) { } } /*try { addFile(ModuleInstaller.class, jarOut); } catch (Exception ignored) { // the module possibly had ModuleInsteller.class }*/ } try { String contents = layerXml.replace("#NAME#", descriptor.getLabel()); JarEntry entry = new JarEntry(LAYER_XML_PATH); jarOut.putNextEntry(entry); byte[] buffer = contents.getBytes(); jarOut.write(buffer, 0, buffer.length); jarOut.closeEntry(); } catch (Exception ignored) { ignored.printStackTrace(); } jarOut.close(); } return fOut.toByteArray(); } /** * Adds a file to the target jar stream. * * @param source The file to be added * @param target The target jar stream * @throws IOException */ private static void addFile(File source, JarOutputStream target) throws IOException { String entryName = source.getPath().replace(modulesPath.getAbsolutePath(), "").replace("\\", "/").substring(1); entryName = entryName.substring(entryName.indexOf("/") + 1); if (!entryName.toLowerCase().endsWith("manifest.mf")) { if (source.isDirectory()) { if (!entryName.isEmpty()) { if (!entryName.endsWith("/")) { entryName += "/"; } JarEntry entry = new JarEntry(entryName); entry.setTime(source.lastModified()); target.putNextEntry(entry); target.closeEntry(); } File[] files = source.listFiles(); if (files != null) { for (File nestedFile : files) { addFile(nestedFile, target); } } return; } JarEntry entry = new JarEntry(entryName); entry.setTime(source.lastModified()); target.putNextEntry(entry); writeBytes(source, target); target.closeEntry(); } } /** * Adds a compiled class file to the target jar stream. * * @param fromClass The class to be added * @param target The target jar stream * @throws IOException */ private static void addFile(Class fromClass, JarOutputStream target) throws IOException { String classEntry = fromClass.getName().replace('.', '/') + ".class"; URL classURL = fromClass.getClassLoader().getResource(classEntry); if (classURL != null) { JarEntry entry = new JarEntry(classEntry); target.putNextEntry(entry); if (!classURL.toString().contains("!")) { String fileName = classURL.getFile(); writeBytes(fileName, target); } else { try (InputStream stream = fromClass.getClassLoader().getResourceAsStream(classEntry)) { writeBytes(stream, target); } } target.closeEntry(); } } private static void writeBytes(String fileName, JarOutputStream target) throws IOException { writeBytes(new File(fileName), target); } private static void writeBytes(File file, JarOutputStream target) throws IOException { try (FileInputStream fileStream = new FileInputStream(file)) { try (BufferedInputStream inputStream = new BufferedInputStream(fileStream)) { byte[] buffer = new byte[1024]; while (true) { int count = inputStream.read(buffer); if (count == -1) { break; } target.write(buffer, 0, count); } } } } private static void writeBytes(InputStream stream, JarOutputStream target) throws IOException { byte[] buffer = new byte[1024]; while (true) { int count = stream.read(buffer); if (count == -1) { break; } target.write(buffer, 0, count); } } }