001/*
002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Bogdan Stefanescu
018 *     Julien Carsique
019 *     Florent Guillaume
020 *     Kevin Leturc <kleturc@nuxeo.com>
021 */
022package org.nuxeo.runtime.deployment.preprocessor;
023
024import java.io.BufferedWriter;
025import java.io.ByteArrayOutputStream;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileOutputStream;
029import java.io.FileWriter;
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.OutputStream;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.List;
036import java.util.zip.ZipEntry;
037import java.util.zip.ZipOutputStream;
038
039import javax.xml.parsers.DocumentBuilder;
040import javax.xml.parsers.DocumentBuilderFactory;
041import javax.xml.parsers.ParserConfigurationException;
042import javax.xml.transform.OutputKeys;
043import javax.xml.transform.Transformer;
044import javax.xml.transform.TransformerException;
045import javax.xml.transform.TransformerFactory;
046import javax.xml.transform.dom.DOMSource;
047import javax.xml.transform.stream.StreamResult;
048
049import org.apache.commons.io.IOUtils;
050import org.apache.commons.lang3.StringUtils;
051import org.apache.commons.logging.Log;
052import org.apache.commons.logging.LogFactory;
053import org.nuxeo.common.Environment;
054import org.nuxeo.launcher.config.ConfigurationException;
055import org.nuxeo.launcher.config.ConfigurationGenerator;
056import org.nuxeo.launcher.config.TomcatConfigurator;
057import org.nuxeo.runtime.api.Framework;
058import org.nuxeo.runtime.deployment.NuxeoStarter;
059import org.w3c.dom.Document;
060import org.w3c.dom.Element;
061import org.w3c.dom.Node;
062import org.xml.sax.SAXException;
063
064/**
065 * Packs a Nuxeo Tomcat instance into a WAR file inside a ZIP.
066 */
067public class PackWar {
068
069    private static Log log = LogFactory.getLog(PackWar.class);
070
071    private static final List<String> MISSING_WEBINF_LIBS = Arrays.asList( //
072            "mail", //
073            "freemarker");
074
075    private static final List<String> MISSING_LIBS = Arrays.asList( //
076            // WSS
077            "nuxeo-wss-front", //
078            // Commons and logging
079            "log4j", //
080            "commons-logging", //
081            "commons-lang", //
082            "commons-lang3", //
083            "jcl-over-slf4j", //
084            "slf4j-api", //
085            "slf4j-log4j12", //
086            "tomcat-juli-adapters", //
087            // JDBC
088            "derby", // Derby
089            "h2", // H2
090            "ojdbc", // Oracle
091            "postgresql", // PostgreSQL
092            "mysql-connector-java", // MySQL
093            "nuxeo-core-storage-sql-extensions", // for Derby/H2
094            "lucene", // for H2
095            "xercesImpl", "xml-apis", "elasticsearch");
096
097    private static final String ZIP_LIB = "lib/";
098
099    private static final String ZIP_WEBAPPS = "webapps/";
100
101    private static final String ZIP_WEBINF = "WEB-INF/";
102
103    private static final String ZIP_WEBINF_LIB = ZIP_WEBINF + "lib/";
104
105    private static final String ZIP_README = "README-NUXEO.txt";
106
107    private static final String README_BEGIN = //
108    "This ZIP must be uncompressed at the root of your Tomcat instance.\n" //
109            + "\n" //
110            + "In order for Nuxeo to run, the following Resource defining your JDBC datasource configuration\n" //
111            + "must be added inside the <GlobalNamingResources> section of the file conf/server.xml\n" //
112            + "\n  ";
113
114    private static final String README_END = "\n\n" //
115            + "Make sure that the 'url' attribute above is correct.\n" //
116            + "Note that the following file can also contains database configuration:\n" //
117            + "\n" //
118            + "  webapps/nuxeo/WEB-INF/default-repository-config.xml\n" //
119            + "\n" //
120            + "Also note that you should start Tomcat with more memory than its default, for instance:\n" //
121            + "\n" //
122            + "  JAVA_OPTS=\"-Xms512m -Xmx1024m -Dnuxeo.log.dir=logs\" bin/catalina.sh start\n" //
123            + "\n" //
124            + "";
125
126    private static final String COMMAND_PREPROCESSING = "preprocessing";
127
128    private static final String COMMAND_PACKAGING = "packaging";
129
130    protected File nxserver;
131
132    protected File tomcat;
133
134    protected File zip;
135
136    private TomcatConfigurator tomcatConfigurator;
137
138    public PackWar(File nxserver, File zip) {
139        if (!nxserver.isDirectory() || !nxserver.getName().equals("nxserver")) {
140            fail("No nxserver found at " + nxserver);
141        }
142        if (zip.exists()) {
143            fail("Target ZIP file " + zip + " already exists");
144        }
145        this.nxserver = nxserver;
146        tomcat = nxserver.getParentFile();
147        this.zip = zip;
148    }
149
150    public void execute(String command) throws ConfigurationException, IOException {
151        boolean preprocessing = COMMAND_PREPROCESSING.equals(command) || StringUtils.isBlank(command);
152        boolean packaging = COMMAND_PACKAGING.equals(command) || StringUtils.isBlank(command);
153        if (!preprocessing && !packaging) {
154            fail("Command parameter should be empty or " + COMMAND_PREPROCESSING + " or " + COMMAND_PACKAGING);
155        }
156        if (preprocessing) {
157            executePreprocessing();
158        }
159        if (packaging) {
160            executePackaging();
161        }
162    }
163
164    protected void executePreprocessing() throws ConfigurationException, IOException {
165        runTemplatePreprocessor();
166        runDeploymentPreprocessor();
167    }
168
169    protected void runTemplatePreprocessor() throws ConfigurationException {
170        if (System.getProperty(Environment.NUXEO_HOME) == null) {
171            System.setProperty(Environment.NUXEO_HOME, tomcat.getAbsolutePath());
172        }
173        if (System.getProperty(ConfigurationGenerator.NUXEO_CONF) == null) {
174            System.setProperty(ConfigurationGenerator.NUXEO_CONF, new File(tomcat, "bin/nuxeo.conf").getPath());
175        }
176        ConfigurationGenerator cg = new ConfigurationGenerator();
177        cg.run();
178        tomcatConfigurator = ((TomcatConfigurator) cg.getServerConfigurator());
179    }
180
181    protected void runDeploymentPreprocessor() throws IOException {
182        DeploymentPreprocessor processor = new DeploymentPreprocessor(nxserver);
183        processor.init();
184        processor.predeploy();
185    }
186
187    protected void executePackaging() throws IOException {
188        try (OutputStream out = new FileOutputStream(zip); //
189                ZipOutputStream zout = new ZipOutputStream(out)) {
190            // extract jdbc datasource from server.xml into README
191            ByteArrayOutputStream bout = new ByteArrayOutputStream();
192            bout.write(README_BEGIN.getBytes("UTF-8"));
193            ServerXmlProcessor.INSTANCE.process(newFile(tomcat, "conf/server.xml"), bout);
194            bout.write(README_END.replace("webapps/nuxeo", "webapps/" + tomcatConfigurator.getContextName())
195                                 .getBytes("UTF-8"));
196            zipBytes(ZIP_README, bout.toByteArray(), zout);
197
198            File nuxeoXml = new File(tomcat, tomcatConfigurator.getTomcatConfig());
199            String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/";
200            zipFile(zipWebappsNuxeo + "META-INF/context.xml", nuxeoXml, zout, NuxeoXmlProcessor.INSTANCE);
201            zipTree(zipWebappsNuxeo, new File(nxserver, "nuxeo.war"), false, zout);
202            zipTree(zipWebappsNuxeo + ZIP_WEBINF, new File(nxserver, "config"), false, zout);
203            File nuxeoBundles = listNuxeoBundles();
204            zipFile(zipWebappsNuxeo + ZIP_WEBINF + NuxeoStarter.NUXEO_BUNDLES_LIST, nuxeoBundles, zout, null);
205            nuxeoBundles.delete();
206            zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "bundles"), false, zout);
207            zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "lib"), false, zout);
208            zipLibs(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(tomcat, "lib"), MISSING_WEBINF_LIBS, zout);
209            zipLibs(ZIP_LIB, new File(tomcat, "lib"), MISSING_LIBS, zout);
210            zipFile(ZIP_LIB + "log4j.xml", newFile(tomcat, "lib/log4j.xml"), zout, null);
211            zout.finish();
212        }
213    }
214
215    /**
216     * @throws IOException
217     * @since 5.9.3
218     */
219    private File listNuxeoBundles() throws IOException {
220        File nuxeoBundles = Framework.createTempFile(NuxeoStarter.NUXEO_BUNDLES_LIST, "");
221        File[] bundles = new File(nxserver, "bundles").listFiles((dir, name) -> name.endsWith(".jar"));
222        try (BufferedWriter writer = new BufferedWriter(new FileWriter(nuxeoBundles))) {
223            for (File bundle : bundles) {
224                writer.write(bundle.getName());
225                writer.newLine();
226            }
227        }
228        return nuxeoBundles;
229    }
230
231    protected static File newFile(File base, String path) {
232        return new File(base, path.replace("/", File.separator));
233    }
234
235    protected void zipLibs(String prefix, File dir, List<String> patterns, ZipOutputStream zout) throws IOException {
236        for (String name : dir.list()) {
237            for (String pat : patterns) {
238                if ((name.startsWith(pat + '-') && name.endsWith(".jar")) || name.equals(pat + ".jar")) {
239                    zipFile(prefix + name, new File(dir, name), zout, null);
240                    break;
241                }
242            }
243        }
244    }
245
246    protected void zipDirectory(String entryName, ZipOutputStream zout) throws IOException {
247        ZipEntry zentry = new ZipEntry(entryName);
248        zout.putNextEntry(zentry);
249        zout.closeEntry();
250    }
251
252    protected void zipFile(String entryName, File file, ZipOutputStream zout, FileProcessor processor)
253            throws IOException {
254        ZipEntry zentry = new ZipEntry(entryName);
255        if (processor == null) {
256            processor = CopyProcessor.INSTANCE;
257            zentry.setTime(file.lastModified());
258        }
259        zout.putNextEntry(zentry);
260        processor.process(file, zout);
261        zout.closeEntry();
262    }
263
264    protected void zipBytes(String entryName, byte[] bytes, ZipOutputStream zout) throws IOException {
265        ZipEntry zentry = new ZipEntry(entryName);
266        zout.putNextEntry(zentry);
267        zout.write(bytes);
268        zout.closeEntry();
269    }
270
271    /** prefix ends with '/' */
272    protected void zipTree(String prefix, File root, boolean includeRoot, ZipOutputStream zout) throws IOException {
273        if (includeRoot) {
274            prefix += root.getName() + '/';
275            zipDirectory(prefix, zout);
276        }
277        String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/";
278        for (String name : root.list()) {
279            File file = new File(root, name);
280            if (file.isDirectory()) {
281                zipTree(prefix, file, true, zout);
282            } else {
283                if (name.endsWith("~") //
284                        || name.endsWith("#") //
285                        || name.endsWith(".bak") //
286                        || name.equals("README.txt")) {
287                    continue;
288                }
289                name = prefix + name;
290                FileProcessor processor;
291                if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "web.xml")) {
292                    processor = WebXmlProcessor.INSTANCE;
293                } else if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "opensocial.properties")) {
294                    processor = new PropertiesFileProcessor("res://config/", zipWebappsNuxeo + ZIP_WEBINF);
295                } else {
296                    processor = null;
297                }
298                zipFile(name, file, zout, processor);
299            }
300        }
301    }
302
303    protected interface FileProcessor {
304        void process(File file, OutputStream out) throws IOException;
305    }
306
307    protected static class CopyProcessor implements FileProcessor {
308
309        public static final CopyProcessor INSTANCE = new CopyProcessor();
310
311        @Override
312        public void process(File file, OutputStream out) throws IOException {
313            try (FileInputStream in = new FileInputStream(file)) {
314                IOUtils.copy(in, out);
315            }
316        }
317    }
318
319    protected class PropertiesFileProcessor implements FileProcessor {
320
321        protected String target;
322
323        protected String replacement;
324
325        public PropertiesFileProcessor(String target, String replacement) {
326            this.target = target;
327            this.replacement = replacement;
328        }
329
330        @Override
331        public void process(File file, OutputStream out) throws IOException {
332            try (FileInputStream in = new FileInputStream(file)) {
333                List<String> lines = IOUtils.readLines(in, "UTF-8");
334                List<String> outLines = new ArrayList<>();
335                for (String line : lines) {
336                    outLines.add(line.replace(target, replacement));
337                }
338                IOUtils.writeLines(outLines, null, out, "UTF-8");
339            }
340        }
341    }
342
343    protected static abstract class XmlProcessor implements FileProcessor {
344
345        @Override
346        public void process(File file, OutputStream out) throws IOException {
347            DocumentBuilder parser;
348            try {
349                parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
350            } catch (ParserConfigurationException e) {
351                throw (IOException) new IOException().initCause(e);
352            }
353            try (InputStream in = new FileInputStream(file)) {
354                Document doc = parser.parse(in);
355                doc.setStrictErrorChecking(false);
356                process(doc);
357                Transformer trans = TransformerFactory.newInstance().newTransformer();
358                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
359                trans.setOutputProperty(OutputKeys.INDENT, "yes");
360                trans.transform(new DOMSource(doc), new StreamResult(out));
361            } catch (SAXException | TransformerException e) {
362                throw (IOException) new IOException().initCause(e);
363            }
364        }
365
366        protected abstract void process(Document doc);
367    }
368
369    protected static class WebXmlProcessor extends XmlProcessor {
370
371        public static final WebXmlProcessor INSTANCE = new WebXmlProcessor();
372
373        private static final String LISTENER = "listener";
374
375        private static final String LISTENER_CLASS = "listener-class";
376
377        @Override
378        protected void process(Document doc) {
379            Node n = doc.getDocumentElement().getFirstChild();
380            while (n != null) {
381                if (LISTENER.equals(n.getNodeName())) {
382                    // insert initial listener
383                    Element listener = doc.createElement(LISTENER);
384                    n.insertBefore(listener, n);
385                    listener.appendChild(doc.createElement(LISTENER_CLASS))
386                            .appendChild(doc.createTextNode(NuxeoStarter.class.getName()));
387                    break;
388                }
389                n = n.getNextSibling();
390            }
391        }
392    }
393
394    protected static class NuxeoXmlProcessor extends XmlProcessor {
395
396        public static final NuxeoXmlProcessor INSTANCE = new NuxeoXmlProcessor();
397
398        private static final String DOCBASE = "docBase";
399
400        private static final String LOADER = "Loader";
401
402        private static final String LISTENER = "Listener";
403
404        @Override
405        protected void process(Document doc) {
406            Element root = doc.getDocumentElement();
407            root.removeAttribute(DOCBASE);
408            Node n = root.getFirstChild();
409            while (n != null) {
410                Node next = n.getNextSibling();
411                String name = n.getNodeName();
412                if (LOADER.equals(name) || LISTENER.equals(name)) {
413                    root.removeChild(n);
414                }
415                n = next;
416            }
417        }
418    }
419
420    protected static class ServerXmlProcessor implements FileProcessor {
421
422        public static final ServerXmlProcessor INSTANCE = new ServerXmlProcessor();
423
424        private static final String GLOBAL_NAMING_RESOURCES = "GlobalNamingResources";
425
426        private static final String RESOURCE = "Resource";
427
428        private static final String NAME = "name";
429
430        private static final String JDBC_NUXEO = "jdbc/nuxeo";
431
432        public String resource;
433
434        @Override
435        public void process(File file, OutputStream out) throws IOException {
436            DocumentBuilder parser;
437            try {
438                parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
439            } catch (ParserConfigurationException e) {
440                throw (IOException) new IOException().initCause(e);
441            }
442            try (InputStream in = new FileInputStream(file)) {
443                Document doc = parser.parse(in);
444                doc.setStrictErrorChecking(false);
445                Element root = doc.getDocumentElement();
446                Node n = root.getFirstChild();
447                Element resourceElement = null;
448                while (n != null) {
449                    Node next = n.getNextSibling();
450                    String name = n.getNodeName();
451                    if (GLOBAL_NAMING_RESOURCES.equals(name)) {
452                        next = n.getFirstChild();
453                    }
454                    if (RESOURCE.equals(name)) {
455                        if (((Element) n).getAttribute(NAME).equals(JDBC_NUXEO)) {
456                            resourceElement = (Element) n;
457                            break;
458                        }
459                    }
460                    n = next;
461                }
462                Transformer trans = TransformerFactory.newInstance().newTransformer();
463                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
464                trans.setOutputProperty(OutputKeys.INDENT, "no");
465                trans.transform(new DOMSource(resourceElement), // only resource
466                        new StreamResult(out));
467            } catch (SAXException | TransformerException e) {
468                throw (IOException) new IOException().initCause(e);
469            }
470        }
471
472    }
473
474    public static void fail(String message) {
475        fail(message, null);
476    }
477
478    public static void fail(String message, Throwable t) {
479        log.error(message, t);
480        System.exit(1);
481    }
482
483    public static void main(String[] args) {
484        if (args.length < 2 || args.length > 3
485                || (args.length == 3 && !Arrays.asList(COMMAND_PREPROCESSING, COMMAND_PACKAGING).contains(args[2]))) {
486            fail(String.format(
487                    "Usage: %s <nxserver_dir> <target_zip> [command]\n" + "    command may be empty or '%s' or '%s'",
488                    PackWar.class.getSimpleName(), COMMAND_PREPROCESSING, COMMAND_PACKAGING));
489        }
490
491        File nxserver = new File(args[0]).getAbsoluteFile();
492        File zip = new File(args[1]).getAbsoluteFile();
493        String command = args.length == 3 ? args[2] : null;
494
495        log.info("Packing nuxeo WAR at " + nxserver + " into " + zip);
496        try {
497            new PackWar(nxserver, zip).execute(command);
498        } catch (ConfigurationException | IOException e) {
499            fail("Pack failed", e);
500        }
501    }
502
503}