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}