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