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 static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING;
025
026import java.io.BufferedWriter;
027import java.io.ByteArrayOutputStream;
028import java.io.File;
029import java.io.FileInputStream;
030import java.io.FileOutputStream;
031import java.io.FileWriter;
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.OutputStream;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.List;
038import java.util.zip.ZipEntry;
039import java.util.zip.ZipOutputStream;
040
041import javax.xml.parsers.DocumentBuilder;
042import javax.xml.parsers.DocumentBuilderFactory;
043import javax.xml.parsers.ParserConfigurationException;
044import javax.xml.transform.OutputKeys;
045import javax.xml.transform.Transformer;
046import javax.xml.transform.TransformerException;
047import javax.xml.transform.TransformerFactory;
048import javax.xml.transform.dom.DOMSource;
049import javax.xml.transform.stream.StreamResult;
050
051import org.apache.commons.io.IOUtils;
052import org.apache.commons.lang3.StringUtils;
053import org.apache.commons.logging.Log;
054import org.apache.commons.logging.LogFactory;
055import org.nuxeo.common.Environment;
056import org.nuxeo.launcher.config.ConfigurationException;
057import org.nuxeo.launcher.config.ConfigurationGenerator;
058import org.nuxeo.launcher.config.TomcatConfigurator;
059import org.nuxeo.runtime.api.Framework;
060import org.nuxeo.runtime.deployment.NuxeoStarter;
061import org.w3c.dom.Document;
062import org.w3c.dom.Element;
063import org.w3c.dom.Node;
064import org.xml.sax.SAXException;
065
066/**
067 * Packs a Nuxeo Tomcat instance into a WAR file inside a ZIP.
068 */
069public class PackWar {
070
071    private static Log log = LogFactory.getLog(PackWar.class);
072
073    private static final List<String> MISSING_WEBINF_LIBS = Arrays.asList( //
074            "mail", //
075            "freemarker");
076
077    private static final List<String> MISSING_LIBS = Arrays.asList( //
078            // WSS
079            "nuxeo-wss-front", //
080            // Commons and logging
081            // TODO need to update it ?
082            "log4j", //
083            "commons-logging", //
084            "commons-lang", //
085            "commons-lang3", //
086            "jcl-over-slf4j", //
087            "slf4j-api", //
088            "tomcat-juli-adapters", //
089            // JDBC
090            "derby", // Derby
091            "h2", // H2
092            "ojdbc", // Oracle
093            "postgresql", // PostgreSQL
094            "mysql-connector-java", // MySQL
095            "nuxeo-core-storage-sql-extensions", // for Derby/H2
096            "lucene", // for H2
097            "xercesImpl", "xml-apis", "elasticsearch");
098
099    private static final String ZIP_LIB = "lib/";
100
101    private static final String ZIP_WEBAPPS = "webapps/";
102
103    private static final String ZIP_WEBINF = "WEB-INF/";
104
105    private static final String ZIP_WEBINF_LIB = ZIP_WEBINF + "lib/";
106
107    private static final String ZIP_README = "README-NUXEO.txt";
108
109    private static final String README_BEGIN = //
110    "This ZIP must be uncompressed at the root of your Tomcat instance.\n" //
111            + "\n" //
112            + "In order for Nuxeo to run, the following Resource defining your JDBC datasource configuration\n" //
113            + "must be added inside the <GlobalNamingResources> section of the file conf/server.xml\n" //
114            + "\n  ";
115
116    private static final String README_END = "\n\n" //
117            + "Make sure that the 'url' attribute above is correct.\n" //
118            + "Note that the following file can also contains database configuration:\n" //
119            + "\n" //
120            + "  webapps/nuxeo/WEB-INF/default-repository-config.xml\n" //
121            + "\n" //
122            + "Also note that you should start Tomcat with more memory than its default, for instance:\n" //
123            + "\n" //
124            + "  JAVA_OPTS=\"-Xms512m -Xmx1024m -Dnuxeo.log.dir=logs\" bin/catalina.sh start\n" //
125            + "\n" //
126            + "";
127
128    private static final String COMMAND_PREPROCESSING = "preprocessing";
129
130    private static final String COMMAND_PACKAGING = "packaging";
131
132    protected File nxserver;
133
134    protected File tomcat;
135
136    protected File zip;
137
138    private TomcatConfigurator tomcatConfigurator;
139
140    public PackWar(File nxserver, File zip) {
141        if (!nxserver.isDirectory() || !nxserver.getName().equals("nxserver")) {
142            fail("No nxserver found at " + nxserver);
143        }
144        if (zip.exists()) {
145            fail("Target ZIP file " + zip + " already exists");
146        }
147        this.nxserver = nxserver;
148        tomcat = nxserver.getParentFile();
149        this.zip = zip;
150    }
151
152    public void execute(String command) throws ConfigurationException, IOException {
153        boolean preprocessing = COMMAND_PREPROCESSING.equals(command) || StringUtils.isBlank(command);
154        boolean packaging = COMMAND_PACKAGING.equals(command) || StringUtils.isBlank(command);
155        if (!preprocessing && !packaging) {
156            fail("Command parameter should be empty or " + COMMAND_PREPROCESSING + " or " + COMMAND_PACKAGING);
157        }
158        if (preprocessing) {
159            executePreprocessing();
160        }
161        if (packaging) {
162            executePackaging();
163        }
164    }
165
166    protected void executePreprocessing() throws ConfigurationException, IOException {
167        runTemplatePreprocessor();
168        runDeploymentPreprocessor();
169    }
170
171    protected void runTemplatePreprocessor() throws ConfigurationException {
172        if (System.getProperty(Environment.NUXEO_HOME) == null) {
173            System.setProperty(Environment.NUXEO_HOME, tomcat.getAbsolutePath());
174        }
175        if (System.getProperty(ConfigurationGenerator.NUXEO_CONF) == null) {
176            System.setProperty(ConfigurationGenerator.NUXEO_CONF, new File(tomcat, "bin/nuxeo.conf").getPath());
177        }
178        ConfigurationGenerator cg = new ConfigurationGenerator();
179        cg.run();
180        tomcatConfigurator = ((TomcatConfigurator) cg.getServerConfigurator());
181    }
182
183    protected void runDeploymentPreprocessor() throws IOException {
184        DeploymentPreprocessor processor = new DeploymentPreprocessor(nxserver);
185        processor.init();
186        processor.predeploy();
187    }
188
189    protected void executePackaging() throws IOException {
190        try (OutputStream out = new FileOutputStream(zip); //
191                ZipOutputStream zout = new ZipOutputStream(out)) {
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 + "log4j2.xml", newFile(tomcat, "lib/log4j2.xml"), zout, null);
213            zout.finish();
214        }
215    }
216
217    /**
218     * @throws IOException
219     * @since 5.9.3
220     */
221    private File listNuxeoBundles() throws IOException {
222        File nuxeoBundles = Framework.createTempFile(NuxeoStarter.NUXEO_BUNDLES_LIST, "");
223        File[] bundles = new File(nxserver, "bundles").listFiles((dir, name) -> name.endsWith(".jar"));
224        try (BufferedWriter writer = new BufferedWriter(new FileWriter(nuxeoBundles))) {
225            for (File bundle : bundles) {
226                writer.write(bundle.getName());
227                writer.newLine();
228            }
229        }
230        return nuxeoBundles;
231    }
232
233    protected static File newFile(File base, String path) {
234        return new File(base, path.replace("/", File.separator));
235    }
236
237    protected void zipLibs(String prefix, File dir, List<String> patterns, ZipOutputStream zout) throws IOException {
238        for (String name : dir.list()) {
239            for (String pat : patterns) {
240                if ((name.startsWith(pat + '-') && name.endsWith(".jar")) || name.equals(pat + ".jar")) {
241                    zipFile(prefix + name, new File(dir, name), zout, null);
242                    break;
243                }
244            }
245        }
246    }
247
248    protected void zipDirectory(String entryName, ZipOutputStream zout) throws IOException {
249        ZipEntry zentry = new ZipEntry(entryName);
250        zout.putNextEntry(zentry);
251        zout.closeEntry();
252    }
253
254    protected void zipFile(String entryName, File file, ZipOutputStream zout, FileProcessor processor)
255            throws IOException {
256        ZipEntry zentry = new ZipEntry(entryName);
257        if (processor == null) {
258            processor = CopyProcessor.INSTANCE;
259            zentry.setTime(file.lastModified());
260        }
261        zout.putNextEntry(zentry);
262        processor.process(file, zout);
263        zout.closeEntry();
264    }
265
266    protected void zipBytes(String entryName, byte[] bytes, ZipOutputStream zout) throws IOException {
267        ZipEntry zentry = new ZipEntry(entryName);
268        zout.putNextEntry(zentry);
269        zout.write(bytes);
270        zout.closeEntry();
271    }
272
273    /** prefix ends with '/' */
274    protected void zipTree(String prefix, File root, boolean includeRoot, ZipOutputStream zout) throws IOException {
275        if (includeRoot) {
276            prefix += root.getName() + '/';
277            zipDirectory(prefix, zout);
278        }
279        String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/";
280        for (String name : root.list()) {
281            File file = new File(root, name);
282            if (file.isDirectory()) {
283                zipTree(prefix, file, true, zout);
284            } else {
285                if (name.endsWith("~") //
286                        || name.endsWith("#") //
287                        || name.endsWith(".bak") //
288                        || name.equals("README.txt")) {
289                    continue;
290                }
291                name = prefix + name;
292                FileProcessor processor;
293                if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "web.xml")) {
294                    processor = WebXmlProcessor.INSTANCE;
295                } else if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "opensocial.properties")) {
296                    processor = new PropertiesFileProcessor("res://config/", zipWebappsNuxeo + ZIP_WEBINF);
297                } else {
298                    processor = null;
299                }
300                zipFile(name, file, zout, processor);
301            }
302        }
303    }
304
305    protected interface FileProcessor {
306        void process(File file, OutputStream out) throws IOException;
307    }
308
309    protected static class CopyProcessor implements FileProcessor {
310
311        public static final CopyProcessor INSTANCE = new CopyProcessor();
312
313        @Override
314        public void process(File file, OutputStream out) throws IOException {
315            try (FileInputStream in = new FileInputStream(file)) {
316                IOUtils.copy(in, out);
317            }
318        }
319    }
320
321    protected class PropertiesFileProcessor implements FileProcessor {
322
323        protected String target;
324
325        protected String replacement;
326
327        public PropertiesFileProcessor(String target, String replacement) {
328            this.target = target;
329            this.replacement = replacement;
330        }
331
332        @Override
333        public void process(File file, OutputStream out) throws IOException {
334            try (FileInputStream in = new FileInputStream(file)) {
335                List<String> lines = IOUtils.readLines(in, "UTF-8");
336                List<String> outLines = new ArrayList<>();
337                for (String line : lines) {
338                    outLines.add(line.replace(target, replacement));
339                }
340                IOUtils.writeLines(outLines, null, out, "UTF-8");
341            }
342        }
343    }
344
345    protected static abstract class XmlProcessor implements FileProcessor {
346
347        @Override
348        public void process(File file, OutputStream out) throws IOException {
349            DocumentBuilder parser;
350            try {
351                parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
352            } catch (ParserConfigurationException e) {
353                throw (IOException) new IOException().initCause(e);
354            }
355            try (InputStream in = new FileInputStream(file)) {
356                Document doc = parser.parse(in);
357                doc.setStrictErrorChecking(false);
358                process(doc);
359                TransformerFactory factory = TransformerFactory.newInstance();
360                factory.setFeature(FEATURE_SECURE_PROCESSING, true);
361                Transformer trans = factory.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 final 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 final 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 final 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                TransformerFactory factory = TransformerFactory.newInstance();
467                factory.setFeature(FEATURE_SECURE_PROCESSING, true);
468                Transformer trans = factory.newTransformer();
469                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
470                trans.setOutputProperty(OutputKeys.INDENT, "no");
471                trans.transform(new DOMSource(resourceElement), // only resource
472                        new StreamResult(out));
473            } catch (SAXException | TransformerException e) {
474                throw (IOException) new IOException().initCause(e);
475            }
476        }
477
478    }
479
480    public static void fail(String message) {
481        fail(message, null);
482    }
483
484    public static void fail(String message, Throwable t) {
485        log.error(message, t);
486        System.exit(1);
487    }
488
489    public static void main(String[] args) {
490        if (args.length < 2 || args.length > 3
491                || (args.length == 3 && !Arrays.asList(COMMAND_PREPROCESSING, COMMAND_PACKAGING).contains(args[2]))) {
492            fail(String.format(
493                    "Usage: %s <nxserver_dir> <target_zip> [command]\n" + "    command may be empty or '%s' or '%s'",
494                    PackWar.class.getSimpleName(), COMMAND_PREPROCESSING, COMMAND_PACKAGING));
495        }
496
497        File nxserver = new File(args[0]).getAbsoluteFile();
498        File zip = new File(args[1]).getAbsoluteFile();
499        String command = args.length == 3 ? args[2] : null;
500
501        log.info("Packing nuxeo WAR at " + nxserver + " into " + zip);
502        try {
503            new PackWar(nxserver, zip).execute(command);
504        } catch (ConfigurationException | IOException e) {
505            fail("Pack failed", e);
506        }
507    }
508
509}