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        OutputStream out = new FileOutputStream(zip);
189        ZipOutputStream zout = new ZipOutputStream(out);
190        try {
191
192            // extract jdbc datasource from server.xml into README
193            ByteArrayOutputStream bout = new ByteArrayOutputStream();
194            bout.write(README_BEGIN.getBytes("UTF-8"));
195            ServerXmlProcessor.INSTANCE.process(newFile(tomcat, "conf/server.xml"), bout);
196            bout.write(README_END.replace("webapps/nuxeo", "webapps/" + tomcatConfigurator.getContextName())
197                                 .getBytes("UTF-8"));
198            zipBytes(ZIP_README, bout.toByteArray(), zout);
199
200            File nuxeoXml = new File(tomcat, tomcatConfigurator.getTomcatConfig());
201            String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/";
202            zipFile(zipWebappsNuxeo + "META-INF/context.xml", nuxeoXml, zout, NuxeoXmlProcessor.INSTANCE);
203            zipTree(zipWebappsNuxeo, new File(nxserver, "nuxeo.war"), false, zout);
204            zipTree(zipWebappsNuxeo + ZIP_WEBINF, new File(nxserver, "config"), false, zout);
205            File nuxeoBundles = listNuxeoBundles();
206            zipFile(zipWebappsNuxeo + ZIP_WEBINF + NuxeoStarter.NUXEO_BUNDLES_LIST, nuxeoBundles, zout, null);
207            nuxeoBundles.delete();
208            zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "bundles"), false, zout);
209            zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "lib"), false, zout);
210            zipLibs(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(tomcat, "lib"), MISSING_WEBINF_LIBS, zout);
211            zipLibs(ZIP_LIB, new File(tomcat, "lib"), MISSING_LIBS, zout);
212            zipFile(ZIP_LIB + "log4j.xml", newFile(tomcat, "lib/log4j.xml"), zout, null);
213        } finally {
214            zout.finish();
215            zout.close();
216        }
217    }
218
219    /**
220     * @throws IOException
221     * @since 5.9.3
222     */
223    private File listNuxeoBundles() throws IOException {
224        File nuxeoBundles = Framework.createTempFile(NuxeoStarter.NUXEO_BUNDLES_LIST, "");
225        File[] bundles = new File(nxserver, "bundles").listFiles((dir, name) -> name.endsWith(".jar"));
226        try (BufferedWriter writer = new BufferedWriter(new FileWriter(nuxeoBundles))) {
227            for (File bundle : bundles) {
228                writer.write(bundle.getName());
229                writer.newLine();
230            }
231        }
232        return nuxeoBundles;
233    }
234
235    protected static File newFile(File base, String path) {
236        return new File(base, path.replace("/", File.separator));
237    }
238
239    protected void zipLibs(String prefix, File dir, List<String> patterns, ZipOutputStream zout) throws IOException {
240        for (String name : dir.list()) {
241            for (String pat : patterns) {
242                if ((name.startsWith(pat + '-') && name.endsWith(".jar")) || name.equals(pat + ".jar")) {
243                    zipFile(prefix + name, new File(dir, name), zout, null);
244                    break;
245                }
246            }
247        }
248    }
249
250    protected void zipDirectory(String entryName, ZipOutputStream zout) throws IOException {
251        ZipEntry zentry = new ZipEntry(entryName);
252        zout.putNextEntry(zentry);
253        zout.closeEntry();
254    }
255
256    protected void zipFile(String entryName, File file, ZipOutputStream zout, FileProcessor processor)
257            throws IOException {
258        ZipEntry zentry = new ZipEntry(entryName);
259        if (processor == null) {
260            processor = CopyProcessor.INSTANCE;
261            zentry.setTime(file.lastModified());
262        }
263        zout.putNextEntry(zentry);
264        processor.process(file, zout);
265        zout.closeEntry();
266    }
267
268    protected void zipBytes(String entryName, byte[] bytes, ZipOutputStream zout) throws IOException {
269        ZipEntry zentry = new ZipEntry(entryName);
270        zout.putNextEntry(zentry);
271        zout.write(bytes);
272        zout.closeEntry();
273    }
274
275    /** prefix ends with '/' */
276    protected void zipTree(String prefix, File root, boolean includeRoot, ZipOutputStream zout) throws IOException {
277        if (includeRoot) {
278            prefix += root.getName() + '/';
279            zipDirectory(prefix, zout);
280        }
281        String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/";
282        for (String name : root.list()) {
283            File file = new File(root, name);
284            if (file.isDirectory()) {
285                zipTree(prefix, file, true, zout);
286            } else {
287                if (name.endsWith("~") //
288                        || name.endsWith("#") //
289                        || name.endsWith(".bak") //
290                        || name.equals("README.txt")) {
291                    continue;
292                }
293                name = prefix + name;
294                FileProcessor processor;
295                if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "web.xml")) {
296                    processor = WebXmlProcessor.INSTANCE;
297                } else if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "opensocial.properties")) {
298                    processor = new PropertiesFileProcessor("res://config/", zipWebappsNuxeo + ZIP_WEBINF);
299                } else {
300                    processor = null;
301                }
302                zipFile(name, file, zout, processor);
303            }
304        }
305    }
306
307    protected interface FileProcessor {
308        void process(File file, OutputStream out) throws IOException;
309    }
310
311    protected static class CopyProcessor implements FileProcessor {
312
313        public static final CopyProcessor INSTANCE = new CopyProcessor();
314
315        @Override
316        public void process(File file, OutputStream out) throws IOException {
317            try (FileInputStream in = new FileInputStream(file)) {
318                IOUtils.copy(in, out);
319            }
320        }
321    }
322
323    protected class PropertiesFileProcessor implements FileProcessor {
324
325        protected String target;
326
327        protected String replacement;
328
329        public PropertiesFileProcessor(String target, String replacement) {
330            this.target = target;
331            this.replacement = replacement;
332        }
333
334        @Override
335        public void process(File file, OutputStream out) throws IOException {
336            try (FileInputStream in = new FileInputStream(file)) {
337                List<String> lines = IOUtils.readLines(in, "UTF-8");
338                List<String> outLines = new ArrayList<>();
339                for (String line : lines) {
340                    outLines.add(line.replace(target, replacement));
341                }
342                IOUtils.writeLines(outLines, null, out, "UTF-8");
343            }
344        }
345    }
346
347    protected static abstract class XmlProcessor implements FileProcessor {
348
349        @Override
350        public void process(File file, OutputStream out) throws IOException {
351            DocumentBuilder parser;
352            try {
353                parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
354            } catch (ParserConfigurationException e) {
355                throw (IOException) new IOException().initCause(e);
356            }
357            try (InputStream in = new FileInputStream(file)) {
358                Document doc = parser.parse(in);
359                doc.setStrictErrorChecking(false);
360                process(doc);
361                Transformer trans = TransformerFactory.newInstance().newTransformer();
362                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
363                trans.setOutputProperty(OutputKeys.INDENT, "yes");
364                trans.transform(new DOMSource(doc), new StreamResult(out));
365            } catch (SAXException | TransformerException e) {
366                throw (IOException) new IOException().initCause(e);
367            }
368        }
369
370        protected abstract void process(Document doc);
371    }
372
373    protected static class WebXmlProcessor extends XmlProcessor {
374
375        public static WebXmlProcessor INSTANCE = new WebXmlProcessor();
376
377        private static final String LISTENER = "listener";
378
379        private static final String LISTENER_CLASS = "listener-class";
380
381        @Override
382        protected void process(Document doc) {
383            Node n = doc.getDocumentElement().getFirstChild();
384            while (n != null) {
385                if (LISTENER.equals(n.getNodeName())) {
386                    // insert initial listener
387                    Element listener = doc.createElement(LISTENER);
388                    n.insertBefore(listener, n);
389                    listener.appendChild(doc.createElement(LISTENER_CLASS))
390                            .appendChild(doc.createTextNode(NuxeoStarter.class.getName()));
391                    break;
392                }
393                n = n.getNextSibling();
394            }
395        }
396    }
397
398    protected static class NuxeoXmlProcessor extends XmlProcessor {
399
400        public static NuxeoXmlProcessor INSTANCE = new NuxeoXmlProcessor();
401
402        private static final String DOCBASE = "docBase";
403
404        private static final String LOADER = "Loader";
405
406        private static final String LISTENER = "Listener";
407
408        @Override
409        protected void process(Document doc) {
410            Element root = doc.getDocumentElement();
411            root.removeAttribute(DOCBASE);
412            Node n = root.getFirstChild();
413            while (n != null) {
414                Node next = n.getNextSibling();
415                String name = n.getNodeName();
416                if (LOADER.equals(name) || LISTENER.equals(name)) {
417                    root.removeChild(n);
418                }
419                n = next;
420            }
421        }
422    }
423
424    protected static class ServerXmlProcessor implements FileProcessor {
425
426        public static ServerXmlProcessor INSTANCE = new ServerXmlProcessor();
427
428        private static final String GLOBAL_NAMING_RESOURCES = "GlobalNamingResources";
429
430        private static final String RESOURCE = "Resource";
431
432        private static final String NAME = "name";
433
434        private static final String JDBC_NUXEO = "jdbc/nuxeo";
435
436        public String resource;
437
438        @Override
439        public void process(File file, OutputStream out) throws IOException {
440            DocumentBuilder parser;
441            try {
442                parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
443            } catch (ParserConfigurationException e) {
444                throw (IOException) new IOException().initCause(e);
445            }
446            try (InputStream in = new FileInputStream(file)) {
447                Document doc = parser.parse(in);
448                doc.setStrictErrorChecking(false);
449                Element root = doc.getDocumentElement();
450                Node n = root.getFirstChild();
451                Element resourceElement = null;
452                while (n != null) {
453                    Node next = n.getNextSibling();
454                    String name = n.getNodeName();
455                    if (GLOBAL_NAMING_RESOURCES.equals(name)) {
456                        next = n.getFirstChild();
457                    }
458                    if (RESOURCE.equals(name)) {
459                        if (((Element) n).getAttribute(NAME).equals(JDBC_NUXEO)) {
460                            resourceElement = (Element) n;
461                            break;
462                        }
463                    }
464                    n = next;
465                }
466                Transformer trans = TransformerFactory.newInstance().newTransformer();
467                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
468                trans.setOutputProperty(OutputKeys.INDENT, "no");
469                trans.transform(new DOMSource(resourceElement), // only resource
470                        new StreamResult(out));
471            } catch (SAXException | TransformerException e) {
472                throw (IOException) new IOException().initCause(e);
473            }
474        }
475
476    }
477
478    public static void fail(String message) {
479        fail(message, null);
480    }
481
482    public static void fail(String message, Throwable t) {
483        log.error(message, t);
484        System.exit(1);
485    }
486
487    public static void main(String[] args) {
488        if (args.length < 2 || args.length > 3
489                || (args.length == 3 && !Arrays.asList(COMMAND_PREPROCESSING, COMMAND_PACKAGING).contains(args[2]))) {
490            fail(String.format(
491                    "Usage: %s <nxserver_dir> <target_zip> [command]\n" + "    command may be empty or '%s' or '%s'",
492                    PackWar.class.getSimpleName(), COMMAND_PREPROCESSING, COMMAND_PACKAGING));
493        }
494
495        File nxserver = new File(args[0]).getAbsoluteFile();
496        File zip = new File(args[1]).getAbsoluteFile();
497        String command = args.length == 3 ? args[2] : null;
498
499        log.info("Packing nuxeo WAR at " + nxserver + " into " + zip);
500        try {
501            new PackWar(nxserver, zip).execute(command);
502        } catch (ConfigurationException | IOException e) {
503            fail("Pack failed", e);
504        }
505    }
506
507}