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