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