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