001/*
002 * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Bogdan Stefanescu
016 *     Julien Carsique
017 *     Florent Guillaume
018 */
019package org.nuxeo.runtime.deployment.preprocessor;
020
021import java.io.BufferedWriter;
022import java.io.ByteArrayOutputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileOutputStream;
026import java.io.FileWriter;
027import java.io.FilenameFilter;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.OutputStream;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.List;
034import java.util.zip.ZipEntry;
035import java.util.zip.ZipOutputStream;
036
037import javax.xml.parsers.DocumentBuilder;
038import javax.xml.parsers.DocumentBuilderFactory;
039import javax.xml.parsers.ParserConfigurationException;
040import javax.xml.transform.OutputKeys;
041import javax.xml.transform.Transformer;
042import javax.xml.transform.TransformerException;
043import javax.xml.transform.TransformerFactory;
044import javax.xml.transform.dom.DOMSource;
045import javax.xml.transform.stream.StreamResult;
046
047import org.apache.commons.io.IOUtils;
048import org.apache.commons.lang.StringUtils;
049import org.apache.commons.logging.Log;
050import org.apache.commons.logging.LogFactory;
051import org.nuxeo.common.Environment;
052import org.nuxeo.launcher.config.ConfigurationException;
053import org.nuxeo.launcher.config.ConfigurationGenerator;
054import org.nuxeo.launcher.config.TomcatConfigurator;
055import org.nuxeo.runtime.deployment.NuxeoStarter;
056import org.w3c.dom.Document;
057import org.w3c.dom.Element;
058import org.w3c.dom.Node;
059import org.xml.sax.SAXException;
060
061/**
062 * Packs a Nuxeo Tomcat instance into a WAR file inside a ZIP.
063 */
064public class PackWar {
065
066    private static Log log = LogFactory.getLog(PackWar.class);
067
068    private static final List<String> ENDORSED_LIBS = Arrays.asList("xercesImpl");
069
070    private static final List<String> MISSING_WEBINF_LIBS = Arrays.asList( //
071            "mail", //
072            "freemarker");
073
074    private static final List<String> MISSING_LIBS = Arrays.asList( //
075            // WSS
076            "nuxeo-wss-front", //
077            // Commons and logging
078            "log4j", //
079            "commons-logging", //
080            "commons-lang", //
081            "jcl-over-slf4j", //
082            "slf4j-api", //
083            "slf4j-log4j12", //
084            "tomcat-juli-adapters", //
085            // JDBC
086            "derby", // Derby
087            "h2", // H2
088            "ojdbc", // Oracle
089            "postgresql", // PostgreSQL
090            "mysql-connector-java", // MySQL
091            "nuxeo-core-storage-sql-extensions", // for Derby/H2
092            "lucene", // for H2
093            "elasticsearch");
094
095    private static final String ENDORSED_LIB = "endorsed/";
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 ConfigurationGenerator cg;
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        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        OutputStream out = new FileOutputStream(zip);
191        ZipOutputStream zout = new ZipOutputStream(out);
192        try {
193
194            // extract jdbc datasource from server.xml into README
195            ByteArrayOutputStream bout = new ByteArrayOutputStream();
196            bout.write(README_BEGIN.getBytes("UTF-8"));
197            ServerXmlProcessor.INSTANCE.process(newFile(tomcat, "conf/server.xml"), bout);
198            bout.write(README_END.replace("webapps/nuxeo", "webapps/" + tomcatConfigurator.getContextName()).getBytes(
199                    "UTF-8"));
200            zipBytes(ZIP_README, bout.toByteArray(), zout);
201
202            File nuxeoXml = new File(tomcat, tomcatConfigurator.getTomcatConfig());
203            String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/";
204            zipFile(zipWebappsNuxeo + "META-INF/context.xml", nuxeoXml, zout, NuxeoXmlProcessor.INSTANCE);
205            zipTree(zipWebappsNuxeo, new File(nxserver, "nuxeo.war"), false, zout);
206            zipTree(zipWebappsNuxeo + ZIP_WEBINF, new File(nxserver, "config"), false, zout);
207            File nuxeoBundles = listNuxeoBundles();
208            zipFile(zipWebappsNuxeo + ZIP_WEBINF + NuxeoStarter.NUXEO_BUNDLES_LIST, nuxeoBundles, zout, null);
209            nuxeoBundles.delete();
210            zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "bundles"), false, zout);
211            zipTree(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(nxserver, "lib"), false, zout);
212            zipLibs(zipWebappsNuxeo + ZIP_WEBINF_LIB, new File(tomcat, "lib"), MISSING_WEBINF_LIBS, zout);
213            zipLibs(ZIP_LIB, new File(tomcat, "lib"), MISSING_LIBS, zout);
214            zipFile(ZIP_LIB + "log4j.xml", newFile(tomcat, "lib/log4j.xml"), zout, null);
215            zipTree(ENDORSED_LIB, new File(tomcat, "endorsed"), false, zout);
216            zipLibs(ENDORSED_LIB, new File(tomcat, "lib"), ENDORSED_LIBS, zout);
217        } finally {
218            zout.finish();
219            zout.close();
220        }
221    }
222
223    /**
224     * @throws IOException
225     * @since 5.9.3
226     */
227    private File listNuxeoBundles() throws IOException {
228        File nuxeoBundles = File.createTempFile(NuxeoStarter.NUXEO_BUNDLES_LIST, "");
229        File[] bundles = new File(nxserver, "bundles").listFiles(new FilenameFilter() {
230            @Override
231            public boolean accept(File dir, String name) {
232                return name.endsWith(".jar");
233            }
234        });
235        try (BufferedWriter writer = new BufferedWriter(new FileWriter(nuxeoBundles))) {
236            for (File bundle : bundles) {
237                writer.write(bundle.getName());
238                writer.newLine();
239            }
240        }
241        return nuxeoBundles;
242    }
243
244    protected static File newFile(File base, String path) {
245        return new File(base, path.replace("/", File.separator));
246    }
247
248    protected void zipLibs(String prefix, File dir, List<String> patterns, ZipOutputStream zout) throws IOException {
249        for (String name : dir.list()) {
250            for (String pat : patterns) {
251                if ((name.startsWith(pat + '-') && name.endsWith(".jar")) || name.equals(pat + ".jar")) {
252                    zipFile(prefix + name, new File(dir, name), zout, null);
253                    break;
254                }
255            }
256        }
257    }
258
259    protected void zipDirectory(String entryName, ZipOutputStream zout) throws IOException {
260        ZipEntry zentry = new ZipEntry(entryName);
261        zout.putNextEntry(zentry);
262        zout.closeEntry();
263    }
264
265    protected void zipFile(String entryName, File file, ZipOutputStream zout, FileProcessor processor)
266            throws IOException {
267        ZipEntry zentry = new ZipEntry(entryName);
268        if (processor == null) {
269            processor = CopyProcessor.INSTANCE;
270            zentry.setTime(file.lastModified());
271        }
272        zout.putNextEntry(zentry);
273        processor.process(file, zout);
274        zout.closeEntry();
275    }
276
277    protected void zipBytes(String entryName, byte[] bytes, ZipOutputStream zout) throws IOException {
278        ZipEntry zentry = new ZipEntry(entryName);
279        zout.putNextEntry(zentry);
280        zout.write(bytes);
281        zout.closeEntry();
282    }
283
284    /** prefix ends with '/' */
285    protected void zipTree(String prefix, File root, boolean includeRoot, ZipOutputStream zout) throws IOException {
286        if (includeRoot) {
287            prefix += root.getName() + '/';
288            zipDirectory(prefix, zout);
289        }
290        String zipWebappsNuxeo = ZIP_WEBAPPS + tomcatConfigurator.getContextName() + "/";
291        for (String name : root.list()) {
292            File file = new File(root, name);
293            if (file.isDirectory()) {
294                zipTree(prefix, file, true, zout);
295            } else {
296                if (name.endsWith("~") //
297                        || name.endsWith("#") //
298                        || name.endsWith(".bak") //
299                        || name.equals("README.txt")) {
300                    continue;
301                }
302                name = prefix + name;
303                FileProcessor processor;
304                if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "web.xml")) {
305                    processor = WebXmlProcessor.INSTANCE;
306                } else if (name.equals(zipWebappsNuxeo + ZIP_WEBINF + "opensocial.properties")) {
307                    processor = new PropertiesFileProcessor("res://config/", zipWebappsNuxeo + ZIP_WEBINF);
308                } else {
309                    processor = null;
310                }
311                zipFile(name, file, zout, processor);
312            }
313        }
314    }
315
316    protected interface FileProcessor {
317        void process(File file, OutputStream out) throws IOException;
318    }
319
320    protected static class CopyProcessor implements FileProcessor {
321
322        public static final CopyProcessor INSTANCE = new CopyProcessor();
323
324        @Override
325        public void process(File file, OutputStream out) throws IOException {
326            FileInputStream in = new FileInputStream(file);
327            try {
328                IOUtils.copy(in, out);
329            } finally {
330                in.close();
331            }
332        }
333    }
334
335    protected class PropertiesFileProcessor implements FileProcessor {
336
337        protected String target;
338
339        protected String replacement;
340
341        public PropertiesFileProcessor(String target, String replacement) {
342            this.target = target;
343            this.replacement = replacement;
344        }
345
346        @Override
347        public void process(File file, OutputStream out) throws IOException {
348            FileInputStream in = new FileInputStream(file);
349            try {
350                List<String> lines = IOUtils.readLines(in, "UTF-8");
351                List<String> outLines = new ArrayList<>();
352                for (String line : lines) {
353                    outLines.add(line.replace(target, replacement));
354                }
355                IOUtils.writeLines(outLines, null, out, "UTF-8");
356            } finally {
357                in.close();
358            }
359        }
360    }
361
362    protected static abstract class XmlProcessor implements FileProcessor {
363
364        @Override
365        public void process(File file, OutputStream out) throws IOException {
366            DocumentBuilder parser;
367            try {
368                parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
369            } catch (ParserConfigurationException e) {
370                throw (IOException) new IOException().initCause(e);
371            }
372            InputStream in = new FileInputStream(file);
373            try {
374                Document doc = parser.parse(in);
375                doc.setStrictErrorChecking(false);
376                process(doc);
377                Transformer trans = TransformerFactory.newInstance().newTransformer();
378                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
379                trans.setOutputProperty(OutputKeys.INDENT, "yes");
380                trans.transform(new DOMSource(doc), new StreamResult(out));
381            } catch (SAXException e) {
382                throw (IOException) new IOException().initCause(e);
383            } catch (TransformerException e) {
384                throw (IOException) new IOException().initCause(e);
385            } finally {
386                in.close();
387            }
388        }
389
390        protected abstract void process(Document doc);
391    }
392
393    protected static class WebXmlProcessor extends XmlProcessor {
394
395        public static WebXmlProcessor INSTANCE = new WebXmlProcessor();
396
397        private static final String LISTENER = "listener";
398
399        private static final String LISTENER_CLASS = "listener-class";
400
401        @Override
402        protected void process(Document doc) {
403            Node n = doc.getDocumentElement().getFirstChild();
404            while (n != null) {
405                if (LISTENER.equals(n.getNodeName())) {
406                    // insert initial listener
407                    Element listener = doc.createElement(LISTENER);
408                    n.insertBefore(listener, n);
409                    listener.appendChild(doc.createElement(LISTENER_CLASS)).appendChild(
410                            doc.createTextNode(NuxeoStarter.class.getName()));
411                    break;
412                }
413                n = n.getNextSibling();
414            }
415        }
416    }
417
418    protected static class NuxeoXmlProcessor extends XmlProcessor {
419
420        public static NuxeoXmlProcessor INSTANCE = new NuxeoXmlProcessor();
421
422        private static final String DOCBASE = "docBase";
423
424        private static final String LOADER = "Loader";
425
426        private static final String LISTENER = "Listener";
427
428        @Override
429        protected void process(Document doc) {
430            Element root = doc.getDocumentElement();
431            root.removeAttribute(DOCBASE);
432            Node n = root.getFirstChild();
433            while (n != null) {
434                Node next = n.getNextSibling();
435                String name = n.getNodeName();
436                if (LOADER.equals(name) || LISTENER.equals(name)) {
437                    root.removeChild(n);
438                }
439                n = next;
440            }
441        }
442    }
443
444    protected static class ServerXmlProcessor implements FileProcessor {
445
446        public static ServerXmlProcessor INSTANCE = new ServerXmlProcessor();
447
448        private static final String GLOBAL_NAMING_RESOURCES = "GlobalNamingResources";
449
450        private static final String RESOURCE = "Resource";
451
452        private static final String NAME = "name";
453
454        private static final String JDBC_NUXEO = "jdbc/nuxeo";
455
456        public String resource;
457
458        @Override
459        public void process(File file, OutputStream out) throws IOException {
460            DocumentBuilder parser;
461            try {
462                parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
463            } catch (ParserConfigurationException e) {
464                throw (IOException) new IOException().initCause(e);
465            }
466            InputStream in = new FileInputStream(file);
467            try {
468                Document doc = parser.parse(in);
469                doc.setStrictErrorChecking(false);
470                Element root = doc.getDocumentElement();
471                Node n = root.getFirstChild();
472                Element resourceElement = null;
473                while (n != null) {
474                    Node next = n.getNextSibling();
475                    String name = n.getNodeName();
476                    if (GLOBAL_NAMING_RESOURCES.equals(name)) {
477                        next = n.getFirstChild();
478                    }
479                    if (RESOURCE.equals(name)) {
480                        if (((Element) n).getAttribute(NAME).equals(JDBC_NUXEO)) {
481                            resourceElement = (Element) n;
482                            break;
483                        }
484                    }
485                    n = next;
486                }
487                Transformer trans = TransformerFactory.newInstance().newTransformer();
488                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
489                trans.setOutputProperty(OutputKeys.INDENT, "no");
490                trans.transform(new DOMSource(resourceElement), // only resource
491                        new StreamResult(out));
492            } catch (SAXException e) {
493                throw (IOException) new IOException().initCause(e);
494            } catch (TransformerException e) {
495                throw (IOException) new IOException().initCause(e);
496            } finally {
497                in.close();
498            }
499        }
500
501    }
502
503    public static void fail(String message) {
504        fail(message, null);
505    }
506
507    public static void fail(String message, Throwable t) {
508        log.error(message, t);
509        System.exit(1);
510    }
511
512    public static void main(String[] args) {
513        if (args.length < 2 || args.length > 3
514                || (args.length == 3 && !Arrays.asList(COMMAND_PREPROCESSING, COMMAND_PACKAGING).contains(args[2]))) {
515            fail(String.format("Usage: %s <nxserver_dir> <target_zip> [command]\n"
516                    + "    command may be empty or '%s' or '%s'", PackWar.class.getSimpleName(), COMMAND_PREPROCESSING,
517                    COMMAND_PACKAGING));
518        }
519
520        File nxserver = new File(args[0]).getAbsoluteFile();
521        File zip = new File(args[1]).getAbsoluteFile();
522        String command = args.length == 3 ? args[2] : null;
523
524        log.info("Packing nuxeo WAR at " + nxserver + " into " + zip);
525        try {
526            new PackWar(nxserver, zip).execute(command);
527        } catch (ConfigurationException | IOException e) {
528            fail("Pack failed", e);
529        }
530    }
531
532}