/* * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Bogdan Stefanescu * Julien Carsique * Florent Guillaume */ package org.nuxeo.runtime.deployment.preprocessor; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.Environment; import org.nuxeo.launcher.config.ConfigurationException; import org.nuxeo.launcher.config.ConfigurationGenerator; import org.nuxeo.launcher.config.TomcatConfigurator; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.deployment.NuxeoStarter; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.SAXException; /** * Packs a Nuxeo Tomcat instance into a WAR file inside a ZIP. */ public class PackWar { private static Log log = LogFactory.getLog(PackWar.class); private static final List<String> MISSING_WEBINF_LIBS = Arrays.asList( // "mail", // "freemarker"); private static final List<String> MISSING_LIBS = Arrays.asList( // // WSS "nuxeo-wss-front", // // Commons and logging "log4j", // "commons-logging", // "commons-lang", // "commons-lang3", // "jcl-over-slf4j", // "slf4j-api", // "slf4j-log4j12", // "tomcat-juli-adapters", // // JDBC "derby", // Derby "h2", // H2 "ojdbc", // Oracle "postgresql", // PostgreSQL "mysql-connector-java", // MySQL "nuxeo-core-storage-sql-extensions", // for Derby/H2 "lucene", // for H2 "xercesImpl", "xml-apis", "elasticsearch"); private static final String ZIP_LIB = "lib/"; private static final String ZIP_WEBAPPS = "webapps/"; private static final String ZIP_WEBINF = "WEB-INF/"; private static final String ZIP_WEBINF_LIB = ZIP_WEBINF + "lib/"; private static final String ZIP_README = "README-NUXEO.txt"; private static final String README_BEGIN = // "This ZIP must be uncompressed at the root of your Tomcat instance.\n" // + "\n" // + "In order for Nuxeo to run, the following Resource defining your JDBC datasource configuration\n" // + "must be added inside the <GlobalNamingResources> section of the file conf/server.xml\n" // + "\n "; private static final String README_END = "\n\n" // + "Make sure that the 'url' attribute above is correct.\n" // + "Note that the following file can also contains database configuration:\n" // + "\n" // + " webapps/nuxeo/WEB-INF/default-repository-config.xml\n" // + "\n" // + "Also note that you should start Tomcat with more memory than its default, for instance:\n" // + "\n" // + " JAVA_OPTS=\"-Xms512m -Xmx1024m -Dnuxeo.log.dir=logs\" bin/catalina.sh start\n" // + "\n" // + ""; private static final String COMMAND_PREPROCESSING = "preprocessing"; private static final String COMMAND_PACKAGING = "packaging"; protected File nxserver; protected File tomcat; protected File zip; private ConfigurationGenerator cg; private TomcatConfigurator tomcatConfigurator; public PackWar(File nxserver, File zip) { if (!nxserver.isDirectory() || !nxserver.getName().equals("nxserver")) { fail("No nxserver found at " + nxserver); } if (zip.exists()) { fail("Target ZIP file " + zip + " already exists"); } this.nxserver = nxserver; tomcat = nxserver.getParentFile(); this.zip = zip; } public void execute(String command) throws ConfigurationException, IOException { boolean preprocessing = COMMAND_PREPROCESSING.equals(command) || StringUtils.isBlank(command); boolean packaging = COMMAND_PACKAGING.equals(command) || StringUtils.isBlank(command); if (!preprocessing && !packaging) { fail("Command parameter should be empty or " + COMMAND_PREPROCESSING + " or " + COMMAND_PACKAGING); } if (preprocessing) { executePreprocessing(); } if (packaging) { executePackaging(); } } protected void executePreprocessing() throws ConfigurationException, IOException { runTemplatePreprocessor(); runDeploymentPreprocessor(); } protected void runTemplatePreprocessor() throws ConfigurationException { if (System.getProperty(Environment.NUXEO_HOME) == null) { System.setProperty(Environment.NUXEO_HOME, tomcat.getAbsolutePath()); } if (System.getProperty(ConfigurationGenerator.NUXEO_CONF) == null) { System.setProperty(ConfigurationGenerator.NUXEO_CONF, new File(tomcat, "bin/nuxeo.conf").getPath()); } cg = new ConfigurationGenerator(); cg.run(); tomcatConfigurator = ((TomcatConfigurator) cg.getServerConfigurator()); } protected void runDeploymentPreprocessor() throws IOException { DeploymentPreprocessor processor = new DeploymentPreprocessor(nxserver); processor.init(); processor.predeploy(); } protected void executePackaging() throws IOException { OutputStream out = new FileOutputStream(zip); ZipOutputStream zout = new ZipOutputStream(out); try { // extract jdbc datasource from server.xml into README ByteArrayOutputStream bout = new ByteArrayOutputStream(); bout.write(README_BEGIN.getBytes("UTF-8")); ServerXmlProcessor.INSTANCE.process(newFile(tomcat, "conf/server.xml"), bout); bout.write(README_END.replace("webapps/nuxeo", "webapps/" + tomcatConfigurator.getContextName()).getBytes( "UTF-8")); zipBytes(ZIP_README, bout.toByteArray(), zout); File nuxeoXml = new File(tomcat, tomcatConfigurator.getTomcatConfig()); String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/"; zipFile(zipWebappsNuxeo + "META-INF/context.xml", nuxeoXml, zout, NuxeoXmlProcessor.INSTANCE); zipTree(zipWebappsNuxeo, new File(nxserver, "nuxeo.war"), false, zout); zipTree(zipWebappsNuxeo + ZIP_WEBINF, new File(nxserver, "config"), false, zout); File nuxeoBundles = listNuxeoBundles(); zipFile(zipWebappsNuxeo + ZIP_WEBINF + NuxeoStarter.NUXEO_BUNDLES_LIST, nuxeoBundles, zout, null); nuxeoBundles.delete(); zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "bundles"), false, zout); zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "lib"), false, zout); zipLibs(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(tomcat, "lib"), MISSING_WEBINF_LIBS, zout); zipLibs(ZIP_LIB, new File(tomcat, "lib"), MISSING_LIBS, zout); zipFile(ZIP_LIB + "log4j.xml", newFile(tomcat, "lib/log4j.xml"), zout, null); } finally { zout.finish(); zout.close(); } } /** * @throws IOException * @since 5.9.3 */ private File listNuxeoBundles() throws IOException { File nuxeoBundles = Framework.createTempFile(NuxeoStarter.NUXEO_BUNDLES_LIST, ""); File[] bundles = new File(nxserver, "bundles").listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }); try (BufferedWriter writer = new BufferedWriter(new FileWriter(nuxeoBundles))) { for (File bundle : bundles) { writer.write(bundle.getName()); writer.newLine(); } } return nuxeoBundles; } protected static File newFile(File base, String path) { return new File(base, path.replace("/", File.separator)); } protected void zipLibs(String prefix, File dir, List<String> patterns, ZipOutputStream zout) throws IOException { for (String name : dir.list()) { for (String pat : patterns) { if ((name.startsWith(pat + '-') && name.endsWith(".jar")) || name.equals(pat + ".jar")) { zipFile(prefix + name, new File(dir, name), zout, null); break; } } } } protected void zipDirectory(String entryName, ZipOutputStream zout) throws IOException { ZipEntry zentry = new ZipEntry(entryName); zout.putNextEntry(zentry); zout.closeEntry(); } protected void zipFile(String entryName, File file, ZipOutputStream zout, FileProcessor processor) throws IOException { ZipEntry zentry = new ZipEntry(entryName); if (processor == null) { processor = CopyProcessor.INSTANCE; zentry.setTime(file.lastModified()); } zout.putNextEntry(zentry); processor.process(file, zout); zout.closeEntry(); } protected void zipBytes(String entryName, byte[] bytes, ZipOutputStream zout) throws IOException { ZipEntry zentry = new ZipEntry(entryName); zout.putNextEntry(zentry); zout.write(bytes); zout.closeEntry(); } /** prefix ends with '/' */ protected void zipTree(String prefix, File root, boolean includeRoot, ZipOutputStream zout) throws IOException { if (includeRoot) { prefix += root.getName() + '/'; zipDirectory(prefix, zout); } String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/"; for (String name : root.list()) { File file = new File(root, name); if (file.isDirectory()) { zipTree(prefix, file, true, zout); } else { if (name.endsWith("~") // || name.endsWith("#") // || name.endsWith(".bak") // || name.equals("README.txt")) { continue; } name = prefix + name; FileProcessor processor; if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "web.xml")) { processor = WebXmlProcessor.INSTANCE; } else if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "opensocial.properties")) { processor = new PropertiesFileProcessor("res://config/", zipWebappsNuxeo + ZIP_WEBINF); } else { processor = null; } zipFile(name, file, zout, processor); } } } protected interface FileProcessor { void process(File file, OutputStream out) throws IOException; } protected static class CopyProcessor implements FileProcessor { public static final CopyProcessor INSTANCE = new CopyProcessor(); @Override public void process(File file, OutputStream out) throws IOException { FileInputStream in = new FileInputStream(file); try { IOUtils.copy(in, out); } finally { in.close(); } } } protected class PropertiesFileProcessor implements FileProcessor { protected String target; protected String replacement; public PropertiesFileProcessor(String target, String replacement) { this.target = target; this.replacement = replacement; } @Override public void process(File file, OutputStream out) throws IOException { FileInputStream in = new FileInputStream(file); try { List<String> lines = IOUtils.readLines(in, "UTF-8"); List<String> outLines = new ArrayList<>(); for (String line : lines) { outLines.add(line.replace(target, replacement)); } IOUtils.writeLines(outLines, null, out, "UTF-8"); } finally { in.close(); } } } protected static abstract class XmlProcessor implements FileProcessor { @Override public void process(File file, OutputStream out) throws IOException { DocumentBuilder parser; try { parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException e) { throw (IOException) new IOException().initCause(e); } InputStream in = new FileInputStream(file); try { Document doc = parser.parse(in); doc.setStrictErrorChecking(false); process(doc); Transformer trans = TransformerFactory.newInstance().newTransformer(); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); trans.setOutputProperty(OutputKeys.INDENT, "yes"); trans.transform(new DOMSource(doc), new StreamResult(out)); } catch (SAXException e) { throw (IOException) new IOException().initCause(e); } catch (TransformerException e) { throw (IOException) new IOException().initCause(e); } finally { in.close(); } } protected abstract void process(Document doc); } protected static class WebXmlProcessor extends XmlProcessor { public static WebXmlProcessor INSTANCE = new WebXmlProcessor(); private static final String LISTENER = "listener"; private static final String LISTENER_CLASS = "listener-class"; @Override protected void process(Document doc) { Node n = doc.getDocumentElement().getFirstChild(); while (n != null) { if (LISTENER.equals(n.getNodeName())) { // insert initial listener Element listener = doc.createElement(LISTENER); n.insertBefore(listener, n); listener.appendChild(doc.createElement(LISTENER_CLASS)).appendChild( doc.createTextNode(NuxeoStarter.class.getName())); break; } n = n.getNextSibling(); } } } protected static class NuxeoXmlProcessor extends XmlProcessor { public static NuxeoXmlProcessor INSTANCE = new NuxeoXmlProcessor(); private static final String DOCBASE = "docBase"; private static final String LOADER = "Loader"; private static final String LISTENER = "Listener"; @Override protected void process(Document doc) { Element root = doc.getDocumentElement(); root.removeAttribute(DOCBASE); Node n = root.getFirstChild(); while (n != null) { Node next = n.getNextSibling(); String name = n.getNodeName(); if (LOADER.equals(name) || LISTENER.equals(name)) { root.removeChild(n); } n = next; } } } protected static class ServerXmlProcessor implements FileProcessor { public static ServerXmlProcessor INSTANCE = new ServerXmlProcessor(); private static final String GLOBAL_NAMING_RESOURCES = "GlobalNamingResources"; private static final String RESOURCE = "Resource"; private static final String NAME = "name"; private static final String JDBC_NUXEO = "jdbc/nuxeo"; public String resource; @Override public void process(File file, OutputStream out) throws IOException { DocumentBuilder parser; try { parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException e) { throw (IOException) new IOException().initCause(e); } InputStream in = new FileInputStream(file); try { Document doc = parser.parse(in); doc.setStrictErrorChecking(false); Element root = doc.getDocumentElement(); Node n = root.getFirstChild(); Element resourceElement = null; while (n != null) { Node next = n.getNextSibling(); String name = n.getNodeName(); if (GLOBAL_NAMING_RESOURCES.equals(name)) { next = n.getFirstChild(); } if (RESOURCE.equals(name)) { if (((Element) n).getAttribute(NAME).equals(JDBC_NUXEO)) { resourceElement = (Element) n; break; } } n = next; } Transformer trans = TransformerFactory.newInstance().newTransformer(); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); trans.setOutputProperty(OutputKeys.INDENT, "no"); trans.transform(new DOMSource(resourceElement), // only resource new StreamResult(out)); } catch (SAXException e) { throw (IOException) new IOException().initCause(e); } catch (TransformerException e) { throw (IOException) new IOException().initCause(e); } finally { in.close(); } } } public static void fail(String message) { fail(message, null); } public static void fail(String message, Throwable t) { log.error(message, t); System.exit(1); } public static void main(String[] args) { if (args.length < 2 || args.length > 3 || (args.length == 3 && !Arrays.asList(COMMAND_PREPROCESSING, COMMAND_PACKAGING).contains(args[2]))) { fail(String.format("Usage: %s <nxserver_dir> <target_zip> [command]\n" + " command may be empty or '%s' or '%s'", PackWar.class.getSimpleName(), COMMAND_PREPROCESSING, COMMAND_PACKAGING)); } File nxserver = new File(args[0]).getAbsoluteFile(); File zip = new File(args[1]).getAbsoluteFile(); String command = args.length == 3 ? args[2] : null; log.info("Packing nuxeo WAR at " + nxserver + " into " + zip); try { new PackWar(nxserver, zip).execute(command); } catch (ConfigurationException | IOException e) { fail("Pack failed", e); } } }