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