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}