1.9.30 sync.
[gae.git] / java / src / main / com / google / appengine / tools / admin / Application.java
blob3aa13bca7f91c16435c83fa0cd590060303e34d4
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.admin;
5 import static java.nio.charset.StandardCharsets.UTF_8;
7 import com.google.appengine.tools.admin.AppAdminFactory.ApplicationProcessingOptions;
8 import com.google.appengine.tools.admin.RepoInfo.SourceContext;
9 import com.google.appengine.tools.info.SdkImplInfo;
10 import com.google.appengine.tools.info.SdkInfo;
11 import com.google.appengine.tools.info.Version;
12 import com.google.appengine.tools.util.ApiVersionFinder;
13 import com.google.appengine.tools.util.FileIterator;
14 import com.google.appengine.tools.util.JarSplitter;
15 import com.google.appengine.tools.util.JarTool;
16 import com.google.apphosting.utils.config.AppEngineConfigException;
17 import com.google.apphosting.utils.config.AppEngineWebXml;
18 import com.google.apphosting.utils.config.AppEngineWebXmlReader;
19 import com.google.apphosting.utils.config.AppYamlProcessor;
20 import com.google.apphosting.utils.config.BackendsXml;
21 import com.google.apphosting.utils.config.BackendsXmlReader;
22 import com.google.apphosting.utils.config.BackendsYamlReader;
23 import com.google.apphosting.utils.config.CronXml;
24 import com.google.apphosting.utils.config.CronXmlReader;
25 import com.google.apphosting.utils.config.CronYamlReader;
26 import com.google.apphosting.utils.config.DispatchXml;
27 import com.google.apphosting.utils.config.DispatchXmlReader;
28 import com.google.apphosting.utils.config.DispatchYamlReader;
29 import com.google.apphosting.utils.config.DosXml;
30 import com.google.apphosting.utils.config.DosXmlReader;
31 import com.google.apphosting.utils.config.DosYamlReader;
32 import com.google.apphosting.utils.config.GenerationDirectory;
33 import com.google.apphosting.utils.config.IndexesXml;
34 import com.google.apphosting.utils.config.IndexesXmlReader;
35 import com.google.apphosting.utils.config.QueueXml;
36 import com.google.apphosting.utils.config.QueueXmlReader;
37 import com.google.apphosting.utils.config.QueueYamlReader;
38 import com.google.apphosting.utils.config.WebXml;
39 import com.google.apphosting.utils.config.WebXmlReader;
40 import com.google.apphosting.utils.config.XmlUtils;
41 import com.google.common.collect.ImmutableSet;
42 import com.google.common.collect.Lists;
43 import com.google.common.collect.Sets;
44 import com.google.common.io.Files;
46 import org.mortbay.io.Buffer;
47 import org.mortbay.jetty.MimeTypes;
48 import org.w3c.dom.Document;
49 import org.w3c.dom.Node;
50 import org.w3c.dom.NodeList;
51 import org.xml.sax.SAXException;
53 import java.io.DataInputStream;
54 import java.io.File;
55 import java.io.FileInputStream;
56 import java.io.FileNotFoundException;
57 import java.io.FileOutputStream;
58 import java.io.FileWriter;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.PrintWriter;
62 import java.net.URL;
63 import java.net.URLConnection;
64 import java.util.ArrayList;
65 import java.util.Arrays;
66 import java.util.HashSet;
67 import java.util.List;
68 import java.util.Set;
69 import java.util.jar.JarEntry;
70 import java.util.jar.JarInputStream;
71 import java.util.logging.Level;
72 import java.util.logging.Logger;
73 import java.util.regex.Pattern;
75 import javax.activation.FileTypeMap;
76 import javax.tools.JavaCompiler;
77 import javax.tools.JavaFileObject;
78 import javax.tools.StandardJavaFileManager;
79 import javax.tools.ToolProvider;
80 import javax.xml.parsers.DocumentBuilder;
81 import javax.xml.parsers.DocumentBuilderFactory;
82 import javax.xml.parsers.ParserConfigurationException;
83 import javax.xml.transform.OutputKeys;
84 import javax.xml.transform.Transformer;
85 import javax.xml.transform.TransformerException;
86 import javax.xml.transform.TransformerFactory;
87 import javax.xml.transform.dom.DOMSource;
88 import javax.xml.transform.stream.StreamResult;
90 /**
91 * An App Engine application. You can {@link #readApplication read} an
92 * {@code Application} from a path, and
93 * {@link com.google.appengine.tools.admin.AppAdminFactory#createAppAdmin create}
94 * an {@link com.google.appengine.tools.admin.AppAdmin} to upload, create
95 * indexes, or otherwise manage it.
98 public class Application implements GenericApplication {
100 private static final int MAX_COMPILED_JSP_JAR_SIZE = 1024 * 1024 * 5;
101 private static final String COMPILED_JSP_JAR_NAME_PREFIX = "_ah_compiled_jsps";
103 private static final int MAX_CLASSES_JAR_SIZE = 1024 * 1024 * 5;
104 private static final String CLASSES_JAR_NAME_PREFIX = "_ah_webinf_classes";
106 private static final String JAVA_7_RUNTIME_ID = "java7";
107 private static final ImmutableSet<String> ALLOWED_RUNTIME_IDS = ImmutableSet.of(
108 JAVA_7_RUNTIME_ID);
110 private static final String BETA_SOURCE_REFERENCE_KEY = "source_reference";
112 private static final Pattern JSP_REGEX = Pattern.compile(".*\\.jspx?");
114 static final String DEFAULT_WEB_XML_CONTENT =
115 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
116 + "<web-app version=\"3.1\" "
117 + "xmlns=\"http://xmlns.jcp.org/xml/ns/javaee\" "
118 + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
119 + "xsi:schemaLocation=\"http://xmlns.jcp.org/xml/ns/javaee "
120 + "http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd\">"
121 + "</web-app>";
123 /** If available, this is set to a program to make symlinks, e.g. /bin/ln */
124 private static File ln = Utility.findLink();
125 private static File sdkDocsDir;
126 public static synchronized File getSdkDocsDir(){
127 if (null == sdkDocsDir){
128 sdkDocsDir = new File(SdkInfo.getSdkRoot(), "docs");
130 return sdkDocsDir;
133 private static Version sdkVersion;
134 public static synchronized Version getSdkVersion() {
135 if (null == sdkVersion) {
136 sdkVersion = SdkInfo.getLocalVersion();
138 return sdkVersion;
141 private static final String STAGEDIR_PREFIX = "appcfg";
143 private static final Logger logger = Logger.getLogger(Application.class.getName());
145 private static final MimeTypes mimeTypes = new MimeTypes();
147 private AppEngineWebXml appEngineWebXml;
148 private WebXml webXml;
149 private CronXml cronXml;
150 private DispatchXml dispatchXml;
151 private DosXml dosXml;
152 private String pagespeedYaml;
153 private QueueXml queueXml;
154 private IndexesXml indexesXml;
155 private BackendsXml backendsXml;
156 private File baseDir;
157 private File stageDir;
158 private File externalResourceDir;
159 private String apiVersion;
160 private String appYaml;
161 private SourceContext sourceContext;
163 private UpdateListener listener;
164 private PrintWriter detailsWriter;
165 private int updateProgress = 0;
166 private int progressAmount = 0;
168 protected Application(){
172 * Builds a normalized path for the given directory in which
173 * forward slashes are used as the file separator on all platforms.
174 * @param dir A directory
175 * @return The normalized path
177 private static String buildNormalizedPath(File dir) {
178 String normalizedPath = dir.getPath();
179 if (File.separatorChar == '\\') {
180 normalizedPath = normalizedPath.replace('\\', '/');
182 return normalizedPath;
185 private Application(String explodedPath, String appId, String module, String appVersion,
186 RepoInfo.SourceContext sourceContext) {
187 this.baseDir = new File(explodedPath);
188 explodedPath = buildNormalizedPath(baseDir);
189 File webinf = new File(baseDir, "WEB-INF");
190 if (!webinf.getName().equals("WEB-INF")) {
191 throw new AppEngineConfigException("WEB-INF directory must be capitalized.");
194 String webinfPath = webinf.getPath();
195 AppEngineWebXmlReader aewebReader = new AppEngineWebXmlReader(explodedPath);
196 WebXmlReader webXmlReader = new WebXmlReader(explodedPath);
197 AppYamlProcessor.convert(webinf, aewebReader.getFilename(), webXmlReader.getFilename());
199 File webXmlFile = new File(webinfPath, "web.xml");
200 if (!webXmlFile.exists()) {
201 writeDefaultWebXml(webXmlFile);
204 if (new File(aewebReader.getFilename()).exists()) {
205 XmlUtils.validateXml(
206 aewebReader.getFilename(), new File(getSdkDocsDir(), "appengine-web.xsd"));
208 appEngineWebXml = aewebReader.readAppEngineWebXml();
209 appEngineWebXml.setSourcePrefix(explodedPath);
210 if (appId == null) {
211 if (appEngineWebXml.getAppId() == null) {
212 throw new AppEngineConfigException(
213 "No app id supplied and XML files have no <application> element");
215 } else {
216 appEngineWebXml.setAppId(appId);
218 if (module != null) {
219 appEngineWebXml.setModule(module);
221 if (appVersion != null) {
222 appEngineWebXml.setMajorVersionId(appVersion);
225 if (sourceContext == null) {
226 sourceContext = new RepoInfo(baseDir).getSourceContext();
227 if (sourceContext != null) {
228 String sourceRef = sourceContext.getRevisionId();
229 if (sourceContext.getRepositoryUrl() != null) {
230 sourceRef = sourceContext.getRepositoryUrl() + "#" + sourceRef;
232 appEngineWebXml.addBetaSetting(BETA_SOURCE_REFERENCE_KEY, sourceRef);
235 this.sourceContext = sourceContext;
237 webXml = webXmlReader.readWebXml();
238 webXml.validate();
240 CronXmlReader cronReader = new CronXmlReader(explodedPath);
241 if (new File(cronReader.getFilename()).exists()) {
242 XmlUtils.validateXml(cronReader.getFilename(), new File(getSdkDocsDir(), "cron.xsd"));
244 cronXml = cronReader.readCronXml();
245 if (cronXml == null) {
246 CronYamlReader cronYaml = new CronYamlReader(webinfPath);
247 cronXml = cronYaml.parse();
250 QueueXmlReader queueReader = new QueueXmlReader(explodedPath);
251 if (new File(queueReader.getFilename()).exists()) {
252 XmlUtils.validateXml(queueReader.getFilename(), new File(getSdkDocsDir(), "queue.xsd"));
254 queueXml = queueReader.readQueueXml();
255 if (queueXml == null) {
256 QueueYamlReader queueYaml = new QueueYamlReader(webinfPath);
257 queueXml = queueYaml.parse();
260 DispatchXmlReader dispatchXmlReader = new DispatchXmlReader(explodedPath,
261 DispatchXmlReader.DEFAULT_RELATIVE_FILENAME);
262 if (new File(dispatchXmlReader.getFilename()).exists()) {
263 XmlUtils.validateXml(
264 dispatchXmlReader.getFilename(), new File(getSdkDocsDir(), "dispatch.xsd"));
266 dispatchXml = dispatchXmlReader.readDispatchXml();
267 if (dispatchXml == null) {
268 DispatchYamlReader dispatchYamlReader = new DispatchYamlReader(webinfPath);
269 dispatchXml = dispatchYamlReader.parse();
272 DosXmlReader dosReader = new DosXmlReader(explodedPath);
273 if (new File(dosReader.getFilename()).exists()) {
274 XmlUtils.validateXml(dosReader.getFilename(), new File(getSdkDocsDir(), "dos.xsd"));
276 dosXml = dosReader.readDosXml();
277 if (dosXml == null) {
278 DosYamlReader dosYaml = new DosYamlReader(webinfPath);
279 dosXml = dosYaml.parse();
282 if (getAppEngineWebXml().getPagespeed() != null) {
283 StringBuilder pagespeedYamlBuilder = new StringBuilder();
284 AppYamlTranslator.appendPagespeed(
285 getAppEngineWebXml().getPagespeed(), pagespeedYamlBuilder, 0);
286 pagespeedYaml = pagespeedYamlBuilder.toString();
289 IndexesXmlReader indexReader = new IndexesXmlReader(explodedPath);
290 File datastoreSchema = new File(getSdkDocsDir(), "datastore-indexes.xsd");
291 if (new File(indexReader.getFilename()).exists()) {
292 XmlUtils.validateXml(indexReader.getFilename(), datastoreSchema);
294 indexesXml = indexReader.readIndexesXml();
296 BackendsXmlReader backendsReader = new BackendsXmlReader(explodedPath);
297 if (new File(backendsReader.getFilename()).exists()) {
298 XmlUtils.validateXml(backendsReader.getFilename(), new File(getSdkDocsDir(), "backends.xsd"));
300 backendsXml = backendsReader.readBackendsXml();
301 if (backendsXml == null) {
302 BackendsYamlReader backendsYaml = new BackendsYamlReader(webinfPath);
303 backendsXml = backendsYaml.parse();
308 * Reads the App Engine application from {@code path}. The path may either
309 * be a WAR file or the root of an exploded WAR directory.
311 * @param path a not {@code null} path.
313 * @throws IOException if an error occurs while trying to read the
314 * {@code Application}.
315 * @throws com.google.apphosting.utils.config.AppEngineConfigException if the
316 * {@code Application's} appengine-web.xml file is malformed.
318 public static Application readApplication(String path)
319 throws IOException {
320 return readApplication(path, null);
324 * Reads the App Engine application from {@code path}. The path may either
325 * be a WAR file or the root of an exploded WAR directory.
327 * @param path a not {@code null} path.
328 * @param sourceContext an explicit RepoInfo.SourceContext. If {@code null}, the source
329 * context will be inferred from the current directory.
331 * @throws IOException if an error occurs while trying to read the
332 * {@code Application}.
333 * @throws com.google.apphosting.utils.config.AppEngineConfigException if the
334 * {@code Application's} appengine-web.xml file is malformed.
336 public static Application readApplication(String path, SourceContext sourceContext)
337 throws IOException {
338 return new Application(path, null, null, null, sourceContext);
342 * Sets the external resource directory. Call this method before invoking
343 * {@link #createStagingDirectory(ApplicationProcessingOptions, ResourceLimits)}.
344 * <p>
345 * The external resource directory is a directory outside of the war directory where additional
346 * files live. These files will be copied into the staging directory during an upload, after the
347 * war directory is copied there. Consequently if there are any name collisions the files in the
348 * external resource directory will win.
350 * @param path a not {@code null} path to an existing directory.
352 * @throws IllegalArgumentException If {@code path} does not refer to an existing
353 * directory.
355 public void setExternalResourceDir(String path) {
356 if (path == null) {
357 throw new NullPointerException("path is null");
359 if (stageDir != null) {
360 throw new IllegalStateException(
361 "This method must be invoked prior to createStagingDirectory()");
363 File dir = new File(path);
364 if (!dir.exists()) {
365 throw new IllegalArgumentException("path does not exist: " + path);
367 if (!dir.isDirectory()) {
368 throw new IllegalArgumentException(path + " is not a directory.");
370 this.externalResourceDir = dir;
374 * Reads the App Engine application from {@code path}. The path may either
375 * be a WAR file or the root of an exploded WAR directory.
377 * @param path a not {@code null} path.
378 * @param appId if non-null, use this as an application id override.
379 * @param module if non-null, use this as a module id override.
380 * @param appVersion if non-null, use this as an application version override.
382 * @throws IOException if an error occurs while trying to read the
383 * {@code Application}.
384 * @throws com.google.apphosting.utils.config.AppEngineConfigException if the
385 * {@code Application's} appengine-web.xml file is malformed.
387 public static Application readApplication(String path,
388 String appId,
389 String module,
390 String appVersion) throws IOException {
391 return new Application(path, appId, module, appVersion, null);
395 * Returns the application identifier, from the AppEngineWebXml config
396 * @return application identifier
398 @Override
399 public String getAppId() {
400 return appEngineWebXml.getAppId();
404 * Returns the application version, from the AppEngineWebXml config
405 * @return application version
407 @Override
408 public String getVersion() {
409 return appEngineWebXml.getMajorVersionId();
412 @Override
413 public String getSourceLanguage() {
414 return appEngineWebXml.getSourceLanguage();
417 @Override
418 public String getModule() {
419 if (appEngineWebXml.getModule() != null) {
420 return appEngineWebXml.getModule();
422 } else {
423 return appEngineWebXml.getService();
427 @Override
428 public String getInstanceClass() {
429 return appEngineWebXml.getInstanceClass();
432 @Override
433 public boolean isPrecompilationEnabled() {
434 return appEngineWebXml.getPrecompilationEnabled();
437 @Override
438 public List<ErrorHandler> getErrorHandlers() {
439 class ErrorHandlerImpl implements ErrorHandler {
440 private final AppEngineWebXml.ErrorHandler errorHandler;
441 public ErrorHandlerImpl(AppEngineWebXml.ErrorHandler errorHandler) {
442 this.errorHandler = errorHandler;
444 @Override
445 public String getFile() {
446 return "__static__/" + errorHandler.getFile();
448 @Override
449 public String getErrorCode() {
450 return errorHandler.getErrorCode();
452 @Override
453 public String getMimeType() {
454 return getMimeTypeIfStatic(getFile());
457 List<ErrorHandler> errorHandlers = new ArrayList<ErrorHandler>();
458 for (AppEngineWebXml.ErrorHandler errorHandler : appEngineWebXml.getErrorHandlers()) {
459 errorHandlers.add(new ErrorHandlerImpl(errorHandler));
461 return errorHandlers;
464 @Override
465 public String getMimeTypeIfStatic(String path) {
466 if (!path.contains("__static__/")) {
467 return null;
469 String mimeType = webXml.getMimeTypeForPath(path);
470 if (mimeType != null) {
471 return mimeType;
473 return guessContentTypeFromName(path);
477 * @param fileName path of a file with extension
478 * @return the mimetype of the file (or application/octect-stream if not recognized)
480 public static String guessContentTypeFromName(String fileName) {
481 String defaultValue = "application/octet-stream";
482 try {
483 Buffer buffer = mimeTypes.getMimeByExtension(fileName);
484 if (buffer != null) {
485 return new String(buffer.asArray());
487 String lowerName = fileName.toLowerCase();
488 if (lowerName.endsWith(".json")) {
489 return "application/json";
491 FileTypeMap typeMap = FileTypeMap.getDefaultFileTypeMap();
492 String ret = typeMap.getContentType(fileName);
493 if (ret != null) {
494 return ret;
496 ret = URLConnection.guessContentTypeFromName(fileName);
497 if (ret != null) {
498 return ret;
500 return defaultValue;
501 } catch (Throwable t) {
502 logger.log(Level.WARNING, "Error identify mimetype for " + fileName, t);
503 return defaultValue;
507 * Returns the AppEngineWebXml describing the application.
509 * @return a not {@code null} deployment descriptor
511 public AppEngineWebXml getAppEngineWebXml() {
512 return appEngineWebXml;
516 * Returns the AppEngineWebXml with application and version removed
518 * @return a not {@code null} deployment descriptor
520 public AppEngineWebXml getScrubbedAppEngineWebXml() {
521 AppEngineWebXml scrubbedAppEngineWebXml = appEngineWebXml.clone();
522 scrubbedAppEngineWebXml.setAppId(null);
523 scrubbedAppEngineWebXml.setMajorVersionId(null);
524 return scrubbedAppEngineWebXml;
528 * Returns the CronXml describing the applications' cron jobs.
529 * @return a cron descriptor, possibly empty or {@code null}
531 @Override
532 public CronXml getCronXml() {
533 return cronXml;
537 * Returns the QueueXml describing the applications' task queues.
538 * @return a queue descriptor, possibly empty or {@code null}
540 @Override
541 public QueueXml getQueueXml() {
542 return queueXml;
545 @Override
546 public DispatchXml getDispatchXml() {
547 return dispatchXml;
551 * Returns the DosXml describing the applications' DoS entries.
552 * @return a dos descriptor, possibly empty or {@code null}
554 @Override
555 public DosXml getDosXml() {
556 return dosXml;
560 * Returns the pagespeed.yaml describing the applications' PageSpeed configuration.
561 * @return a pagespeed.yaml string, possibly empty or {@code null}
563 @Override
564 public String getPagespeedYaml() {
565 return pagespeedYaml;
569 * Returns the IndexesXml describing the applications' indexes.
570 * @return a index descriptor, possibly empty or {@code null}
572 @Override
573 public IndexesXml getIndexesXml() {
574 return indexesXml;
578 * Returns the WebXml describing the applications' servlets and generic web
579 * application information.
581 * @return a WebXml descriptor, possibly empty but not {@code null}
583 public WebXml getWebXml() {
584 return webXml;
587 @Override
588 public BackendsXml getBackendsXml() {
589 return backendsXml;
593 * Returns the desired API version for the current application, or
594 * {@code "none"} if no API version was used.
596 * @throws IllegalStateException if createStagingDirectory has not been called.
598 @Override
599 public String getApiVersion() {
600 if (apiVersion == null) {
601 throw new IllegalStateException("Must call createStagingDirectory first.");
603 return apiVersion;
607 * Returns a path to an exploded WAR directory for the application.
608 * This may be a temporary directory.
610 * @return a not {@code null} path pointing to a directory
612 @Override
613 public String getPath() {
614 return baseDir.getAbsolutePath();
618 * Returns the staging directory, or {@code null} if none has been created.
620 @Override
621 public File getStagingDir() {
622 return stageDir;
625 @Override
626 public void resetProgress() {
627 updateProgress = 0;
628 progressAmount = 0;
632 * Creates a new staging directory, if needed, or returns the existing one
633 * if already created.
635 * @param opts User-specified options for processing the application.
636 * @return staging directory
637 * @throws IOException
639 @Override
640 public File createStagingDirectory(ApplicationProcessingOptions opts,
641 ResourceLimits resourceLimits) throws IOException {
642 if (stageDir != null) {
643 return stageDir;
646 int i = 0;
647 while (stageDir == null && i++ < 3) {
648 try {
649 stageDir = File.createTempFile(STAGEDIR_PREFIX, null);
650 } catch (IOException ex) {
651 continue;
653 stageDir.delete();
654 if (!stageDir.mkdir()) {
655 stageDir = null;
658 if (i == 3) {
659 throw new IOException("Couldn't create a temporary directory in 3 tries.");
661 statusUpdate("Created staging directory at: '" + stageDir.getPath() + "'", 20);
663 String runtime = getRuntime(opts);
664 return populateStagingDirectory(opts, resourceLimits, false, runtime);
668 * Populates and creates (if necessary) a user specified, staging directory
670 * @param opts User-specified options for processing the application.
671 * @param resourceLimits Various resource limits provided by the cloud.
672 * @param stagingDir User-specified staging directory (must be empty or not exist)
673 * @return staging directory
674 * @throws IOException if an error occurs trying to create or populate the staging directory
676 @Override
677 public File createStagingDirectory(ApplicationProcessingOptions opts,
678 ResourceLimits resourceLimits, File stagingDir) throws IOException {
679 if (!stagingDir.exists()) {
680 if (!stagingDir.mkdir()) {
681 throw new IOException("Could not create staging directory at " + stagingDir.getPath());
685 stageDir = stagingDir;
686 ln = null;
688 String runtime = getRuntime(opts);
689 populateStagingDirectory(opts, resourceLimits, true, runtime);
690 copyOrLinkDirectories(GenerationDirectory.getGenerationDirectory(stageDir), stageDir, runtime);
691 return stageDir;
694 private File populateStagingDirectory(ApplicationProcessingOptions opts,
695 ResourceLimits resourceLimits, boolean writeScrubbedAppYaml, String runtime)
696 throws IOException {
697 File staticDir = new File(stageDir, "__static__");
698 staticDir.mkdir();
699 copyOrLink(baseDir, stageDir, staticDir, false, opts, runtime);
700 if (externalResourceDir != null) {
701 String previousPrefix = appEngineWebXml.getSourcePrefix();
702 String newPrefix = buildNormalizedPath(externalResourceDir);
703 try {
704 appEngineWebXml.setSourcePrefix(newPrefix);
705 copyOrLink(externalResourceDir, stageDir, staticDir, false, opts, runtime);
706 } finally {
707 appEngineWebXml.setSourcePrefix(previousPrefix);
711 apiVersion = findApiVersion(stageDir, true);
713 if (opts.isCompileJspsSet()) {
714 compileJsps(stageDir, opts, runtime);
717 if (opts.isQuickstart()) {
718 try {
719 createQuickstartWebXml(opts);
720 webXml = new WebXmlReader(stageDir.getAbsolutePath(), "/WEB-INF/min-quickstart-web.xml")
721 .readWebXml();
722 } catch (SAXException | ParserConfigurationException | TransformerException e) {
723 throw new IOException(e);
727 appYaml = generateAppYaml(stageDir, runtime, appEngineWebXml);
729 if (GenerationDirectory.getGenerationDirectory(stageDir).mkdirs()) {
730 writePreparedYamlFile("app", writeScrubbedAppYaml
731 ? generateAppYaml(stageDir, runtime, getScrubbedAppEngineWebXml()) : appYaml);
732 writePreparedYamlFile("backends", backendsXml == null ? null : backendsXml.toYaml());
733 writePreparedYamlFile("index", indexesXml.size() == 0 ? null : indexesXml.toYaml());
734 writePreparedYamlFile("cron", cronXml == null ? null : cronXml.toYaml());
735 writePreparedYamlFile("queue", queueXml == null ? null : queueXml.toYaml());
736 writePreparedYamlFile("dos", dosXml == null ? null : dosXml.toYaml());
739 int maxJarSize = (int) resourceLimits.maxFileSize();
741 if (opts.isSplitJarsSet()) {
742 splitJars(new File(new File(stageDir, "WEB-INF"), "lib"),
743 maxJarSize, opts.getJarSplittingExcludes());
746 exportRepoInfoFile();
748 return stageDir;
751 @Override
752 public void exportRepoInfoFile() {
753 File target = new File(stageDir, "WEB-INF/classes/source-context.json");
754 if (target.exists()) {
755 return;
758 if (sourceContext == null || sourceContext.getJson() == null) {
759 return;
762 try {
763 target.getParentFile().mkdirs();
764 Files.write(sourceContext.getJson(), target, UTF_8);
765 } catch (IOException ex) {
766 logger.log(Level.FINE, "Failed to write git repository information file.", ex);
767 return;
770 statusUpdate("Generated git repository information file.");
774 * Write yaml file to generation subdirectory within stage directory.
776 private void writePreparedYamlFile(String yamlName, String yamlString) throws IOException {
777 File f = new File(GenerationDirectory.getGenerationDirectory(stageDir), yamlName + ".yaml");
778 if (yamlString != null && f.createNewFile()) {
779 FileWriter fw = new FileWriter(f);
780 fw.write(yamlString);
781 fw.close();
785 private static String findApiVersion(File baseDir, boolean deleteApiJars) {
786 ApiVersionFinder finder = new ApiVersionFinder();
788 String foundApiVersion = null;
789 File webInf = new File(baseDir, "WEB-INF");
790 File libDir = new File(webInf, "lib");
791 for (File file : new FileIterator(libDir)) {
792 if (file.getPath().endsWith(".jar")) {
793 try {
794 String apiVersion = finder.findApiVersion(file);
795 if (apiVersion != null) {
796 if (foundApiVersion == null) {
797 foundApiVersion = apiVersion;
798 } else if (!foundApiVersion.equals(apiVersion)) {
799 logger.warning("Warning: found duplicate API version: " + foundApiVersion +
800 ", using " + apiVersion);
802 if (deleteApiJars) {
803 if (!file.delete()) {
804 logger.log(Level.SEVERE, "Could not delete API jar: " + file);
808 } catch (IOException ex) {
809 logger.log(Level.WARNING, "Could not identify API version in " + file, ex);
814 if (foundApiVersion == null) {
815 foundApiVersion = "none";
817 return foundApiVersion;
821 * Returns the runtime id to use in the generated app.yaml.
823 * This method returns {@code "java7"}, unless an explicit runtime id was specified
824 * using the {@code -r} option.
826 * Before accepting an explicit runtime id, this method validates it against the list of
827 * supported Java runtimes (currently only {@code "java7"}), unless validation was turned
828 * off using the {@code --allowAnyRuntimes} option.
830 private String getRuntime(ApplicationProcessingOptions opts) {
831 boolean vm = appEngineWebXml.getUseVm() || appEngineWebXml.getEnv().equals("2");
832 if (vm && new File(baseDir, "Dockerfile").exists()) {
833 return "custom";
835 String runtime = opts.getRuntime();
836 if (runtime != null) {
837 if (!opts.isAllowAnyRuntime() && !ALLOWED_RUNTIME_IDS.contains(runtime)) {
838 throw new AppEngineConfigException("Invalid runtime id: " + runtime + ". Valid " +
839 "runtime id: java7.");
841 return runtime;
843 return JAVA_7_RUNTIME_ID;
846 private static final String JSPC_MAIN = "com.google.appengine.tools.development.LocalJspC";
848 private void compileJsps(File stage, ApplicationProcessingOptions opts, String runtime)
849 throws IOException {
850 statusUpdate("Scanning for jsp files.");
852 if (matchingFileExists(new File(stage.getPath()), JSP_REGEX)) {
853 statusUpdate("Compiling jsp files.");
855 File webInf = new File(stage, "WEB-INF");
857 for (File file : SdkImplInfo.getUserJspLibFiles()) {
858 copyOrLinkFile(file, new File(new File(webInf, "lib"), file.getName()), runtime);
860 for (File file : SdkImplInfo.getSharedJspLibFiles()) {
861 copyOrLinkFile(file, new File(new File(webInf, "lib"), file.getName()), runtime);
864 File classes = new File(webInf, "classes");
865 File generatedWebXml = new File(webInf, "generated_web.xml");
866 File tempDir = Files.createTempDir();
867 String classpath = getJspClasspath(classes, tempDir);
869 String javaCmd = opts.getJavaExecutable().getPath();
870 String[] args = new String[] {
871 javaCmd,
872 "-classpath", classpath,
873 JSPC_MAIN,
874 "-uriroot", stage.getPath(),
875 "-p", "org.apache.jsp",
876 "-l", "-v",
877 "-webinc", generatedWebXml.getPath(),
878 "-d", tempDir.getPath(),
879 "-javaEncoding", opts.getCompileEncoding(),
881 Process jspc = startProcess(args);
883 int status = 1;
884 try {
885 status = jspc.waitFor();
886 } catch (InterruptedException ex) { }
888 if (status != 0) {
889 detailsWriter.println("Error while executing: " + formatCommand(Arrays.asList(args)));
890 throw new JspCompilationException("Failed to compile jsp files.",
891 JspCompilationException.Source.JASPER);
894 compileJavaFiles(classpath, webInf, tempDir, opts, runtime);
896 webXml = new WebXmlReader(stage.getPath()).readWebXml();
900 private void compileJavaFiles(String classpath, File webInf, File jspClassDir,
901 ApplicationProcessingOptions opts, String runtime) throws IOException {
903 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
904 if (compiler == null) {
905 throw new RuntimeException(
906 "Cannot get the System Java Compiler. Please use a JDK, not a JRE.");
908 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
910 ArrayList<File> files = new ArrayList<File>();
911 for (File f : new FileIterator(jspClassDir)) {
912 if (f.getPath().toLowerCase().endsWith(".java")) {
913 files.add(f);
916 if (files.isEmpty()) {
917 return;
919 List<String> optionList = new ArrayList<String>();
920 optionList.addAll(Arrays.asList("-classpath", classpath.toString()));
921 optionList.addAll(Arrays.asList("-d", jspClassDir.getPath()));
922 optionList.addAll(Arrays.asList("-encoding", opts.getCompileEncoding()));
923 if (runtime.equals(JAVA_7_RUNTIME_ID)) {
924 optionList.addAll(Arrays.asList("-source", "7"));
925 optionList.addAll(Arrays.asList("-target", "7"));
928 Iterable<? extends JavaFileObject> compilationUnits =
929 fileManager.getJavaFileObjectsFromFiles(files);
930 boolean success = compiler.getTask(
931 null, fileManager, null, optionList, null, compilationUnits).call();
932 fileManager.close();
934 if (!success) {
935 throw new JspCompilationException("Failed to compile the generated JSP java files.",
936 JspCompilationException.Source.JSPC);
938 if (opts.isJarJSPsSet()) {
939 zipJasperGeneratedFiles(webInf, jspClassDir);
940 } else {
941 copyOrLinkDirectories(jspClassDir, new File(webInf, "classes"), runtime);
943 if (opts.isDeleteJSPs()) {
944 for (File f : new FileIterator(webInf.getParentFile())) {
945 if (f.getPath().toLowerCase().endsWith(".jsp")) {
946 f.delete();
950 if (opts.isJarClassesSet()) {
951 zipWebInfClassesFiles(webInf);
956 private void zipJasperGeneratedFiles(File webInfDir, File jspClassDir) throws IOException {
957 Set<String> fileTypesToExclude = ImmutableSet.of(".java");
958 File libDir = new File(webInfDir, "lib");
959 JarTool jarTool = new JarTool(
960 COMPILED_JSP_JAR_NAME_PREFIX, jspClassDir, libDir, MAX_COMPILED_JSP_JAR_SIZE,
961 fileTypesToExclude);
962 jarTool.run();
963 recursiveDelete(jspClassDir);
966 private void zipWebInfClassesFiles(File webInfDir) throws IOException {
967 File libDir = new File(webInfDir, "lib");
968 File classesDir = new File(webInfDir, "classes");
969 JarTool jarTool = new JarTool(
970 CLASSES_JAR_NAME_PREFIX, classesDir, libDir, MAX_CLASSES_JAR_SIZE,
971 null);
972 jarTool.run();
973 recursiveDelete(classesDir);
974 classesDir.mkdir();
977 private String getJspClasspath(File classDir, File genDir) {
978 StringBuilder classpath = new StringBuilder();
979 for (URL lib : SdkImplInfo.getImplLibs()) {
980 classpath.append(lib.getPath());
981 classpath.append(File.pathSeparatorChar);
983 for (File lib : SdkInfo.getSharedLibFiles()) {
984 classpath.append(lib.getPath());
985 classpath.append(File.pathSeparatorChar);
988 classpath.append(classDir.getPath());
989 classpath.append(File.pathSeparatorChar);
990 classpath.append(genDir.getPath());
991 classpath.append(File.pathSeparatorChar);
993 for (File f : new FileIterator(new File(classDir.getParentFile(), "lib"))) {
994 String filename = f.getPath().toLowerCase();
995 if (filename.endsWith(".jar") || filename.endsWith(".zip")) {
996 classpath.append(f.getPath());
997 classpath.append(File.pathSeparatorChar);
1001 return classpath.toString();
1004 private Process startProcess(String... args) throws IOException {
1005 ProcessBuilder builder = new ProcessBuilder(args);
1006 Process proc = builder.redirectErrorStream(true).start();
1007 logger.fine(formatCommand(builder.command()));
1008 new Thread(new OutputPump(proc.getInputStream(), detailsWriter)).start();
1009 return proc;
1012 private String formatCommand(Iterable<String> args) {
1013 StringBuilder command = new StringBuilder();
1014 for (String chunk : args) {
1015 command.append(chunk);
1016 command.append(" ");
1018 return command.toString();
1022 * Scans a given directory tree, testing whether any file matches a given
1023 * pattern.
1025 * @param dir the directory under which to scan
1026 * @param regex the pattern to look for
1027 * @returns Returns {@code true} on the first instance of such a file,
1028 * {@code false} otherwise.
1030 private static boolean matchingFileExists(File dir, Pattern regex) {
1031 for (File file : dir.listFiles()) {
1032 if (file.isDirectory()) {
1033 if (matchingFileExists(file, regex)) {
1034 return true;
1036 } else {
1037 if (regex.matcher(file.getName()).matches()) {
1038 return true;
1042 return false;
1046 * Invokes the JarSplitter code on any jar files found in {@code dir}. Any
1047 * jars larger than {@code max} will be split into fragments of at most that
1048 * size.
1049 * @param dir the directory to search, recursively
1050 * @param max the maximum allowed size
1051 * @param excludes a set of suffixes to exclude.
1052 * @throws IOException on filesystem errors.
1054 private static void splitJars(File dir, int max, Set<String> excludes) throws IOException {
1055 String[] children = dir.list();
1056 if (children == null) {
1057 return;
1059 for (String name : children) {
1060 File subfile = new File(dir, name);
1061 if (subfile.isDirectory()) {
1062 splitJars(subfile, max, excludes);
1063 } else if (name.endsWith(".jar")) {
1064 if (subfile.length() > max) {
1065 new JarSplitter(subfile, dir, max, false, 4, excludes).run();
1066 subfile.delete();
1072 private static final Pattern SKIP_FILES = Pattern.compile(
1073 "^(.*/)?((#.*#)|(.*~)|(.*/RCS/.*)|)$");
1076 * Copies files from the app to the upload staging directory, or makes
1077 * symlinks instead if supported. Puts the files into the correct places for
1078 * static vs. resource files, recursively.
1080 * @param sourceDir application war dir, or on recursion a subdirectory of it
1081 * @param resDir staging resource dir, or on recursion a subdirectory matching
1082 * the subdirectory in {@code sourceDir}
1083 * @param staticDir staging {@code __static__} dir, or an appropriate recursive
1084 * subdirectory
1085 * @param forceResource if all files should be considered resource files
1086 * @param opts processing options, used primarily for handling of *.jsp files
1087 * @throws FileNotFoundException
1088 * @throws IOException
1090 private void copyOrLink(File sourceDir, File resDir, File staticDir, boolean forceResource,
1091 ApplicationProcessingOptions opts, String runtime)
1092 throws FileNotFoundException, IOException {
1094 for (String name : sourceDir.list()) {
1095 File file = new File(sourceDir, name);
1097 String path = file.getPath();
1098 if (File.separatorChar == '\\') {
1099 path = path.replace('\\', '/');
1102 if (file.getName().startsWith(".") ||
1103 file.equals(GenerationDirectory.getGenerationDirectory(baseDir))) {
1104 continue;
1107 if (file.isDirectory()) {
1108 if (file.getName().equals("WEB-INF")) {
1109 copyOrLink(file, new File(resDir, name), new File(staticDir, name), true, opts, runtime);
1110 } else {
1111 copyOrLink(file, new File(resDir, name), new File(staticDir, name), forceResource,
1112 opts, runtime);
1114 } else {
1115 if (SKIP_FILES.matcher(path).matches()) {
1116 continue;
1119 if (forceResource || appEngineWebXml.includesResource(path) ||
1120 (opts.isCompileJspsSet() && name.toLowerCase().endsWith(".jsp"))) {
1121 copyOrLinkFile(file, new File(resDir, name), runtime);
1123 if (!forceResource && appEngineWebXml.includesStatic(path)) {
1124 copyOrLinkFile(file, new File(staticDir, name), runtime);
1131 * Attempts to symlink a single file, or copies it if symlinking is either
1132 * unsupported or fails.
1134 * @param source source file
1135 * @param dest destination file
1136 * @throws FileNotFoundException
1137 * @throws IOException
1139 private void copyOrLinkFile(File source, File dest, String runtime)
1140 throws FileNotFoundException, IOException {
1141 if (runtime.equals(JAVA_7_RUNTIME_ID)) {
1142 checkJavaVersion(source, 7);
1144 dest.getParentFile().mkdirs();
1145 if (ln != null && !source.getName().endsWith("web.xml")) {
1147 try {
1148 dest.delete();
1149 } catch (Exception e) {
1150 System.err.println("Warning: We tried to delete " + dest.getPath());
1151 System.err.println("in order to create a symlink from " + source.getPath());
1152 System.err.println("but the delete failed with message: " + e.getMessage());
1155 Process link = startProcess(ln.getAbsolutePath(), "-s",
1156 source.getAbsolutePath(),
1157 dest.getAbsolutePath());
1158 try {
1159 int stat = link.waitFor();
1160 if (stat == 0) {
1161 return;
1163 System.err.println(ln.getAbsolutePath() + " returned status " + stat
1164 + ", copying instead...");
1165 } catch (InterruptedException ex) {
1166 System.err.println(ln.getAbsolutePath() + " was interrupted, copying instead...");
1168 if (dest.delete()) {
1169 System.err.println("ln failed but symlink was created, removed: " + dest.getAbsolutePath());
1172 try (FileInputStream inStream = new FileInputStream(source);
1173 FileOutputStream outStream = new FileOutputStream(dest)) {
1174 byte[] buffer = new byte[1024];
1175 int readlen = inStream.read(buffer);
1176 while (readlen > 0) {
1177 outStream.write(buffer, 0, readlen);
1178 readlen = inStream.read(buffer);
1183 /** Copy (or link) one directory into another one.
1185 private void copyOrLinkDirectories(File sourceDir, File destination, String runtime)
1186 throws IOException {
1188 for (String name : sourceDir.list()) {
1189 File file = new File(sourceDir, name);
1190 if (file.isDirectory()) {
1191 copyOrLinkDirectories(file, new File(destination, name), runtime);
1192 } else {
1193 copyOrLinkFile(file, new File(destination, name), runtime);
1198 private void checkJavaVersion(File file, int maxVersion) {
1199 String name = file.getName();
1200 try {
1201 if (name.endsWith(".class")) {
1202 checkJavaClassVersion(file, maxVersion);
1203 } else if (name.endsWith(".jar")) {
1204 checkJavaJarVersion(file, maxVersion);
1206 } catch (IOException e) {
1210 private void checkJavaClassVersion(File classFile, int maxVersion) throws IOException {
1211 try (InputStream inputStream = new FileInputStream(classFile)) {
1212 checkJavaVersion(inputStream, maxVersion, classFile.getPath());
1216 private void checkJavaJarVersion(File jarFile, int maxVersion) throws IOException {
1217 try (JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jarFile))) {
1218 while (true) {
1219 JarEntry jarEntry = jarInputStream.getNextJarEntry();
1220 if (jarEntry == null) {
1221 return;
1223 if (jarEntry.getName().endsWith(".class")) {
1224 checkJavaVersion(jarInputStream, maxVersion, jarEntry.getName() + " in " + jarFile);
1225 return;
1231 private void checkJavaVersion(
1232 InputStream inputStream, int maxVersion, String what) throws IOException {
1233 DataInputStream in = new DataInputStream(inputStream);
1234 if (in.readInt() == 0xcafebabe) {
1235 in.readShort();
1236 int majorVersion = in.readShort();
1237 int actualVersion = majorVersion - 44;
1238 if (actualVersion > maxVersion) {
1239 throw new IllegalArgumentException(
1240 String.format("Class file is Java %d but max supported is Java %d: %s",
1241 actualVersion, maxVersion, what));
1246 /** deletes the staging directory, if one was created. */
1247 @Override
1248 public void cleanStagingDirectory() {
1249 if (stageDir != null) {
1250 recursiveDelete(stageDir);
1254 /** Recursive directory deletion. */
1255 public static void recursiveDelete(File dead) {
1256 String[] files = dead.list();
1257 if (files != null) {
1258 for (String name : files) {
1259 recursiveDelete(new File(dead, name));
1262 dead.delete();
1265 @Override
1266 public void setListener(UpdateListener l) {
1267 listener = l;
1270 @Override
1271 public void setDetailsWriter(PrintWriter detailsWriter) {
1272 this.detailsWriter = detailsWriter;
1275 @Override
1276 public void statusUpdate(String message, int amount) {
1277 updateProgress += progressAmount;
1278 if (updateProgress > 99) {
1279 updateProgress = 99;
1281 progressAmount = amount;
1282 if (listener != null) {
1283 listener.onProgress(new UpdateProgressEvent(
1284 Thread.currentThread(), message, updateProgress));
1288 @Override
1289 public void statusUpdate(String message) {
1290 int amount = progressAmount / 4;
1291 updateProgress += amount;
1292 if (updateProgress > 99) {
1293 updateProgress = 99;
1295 progressAmount -= amount;
1296 if (listener != null) {
1297 listener.onProgress(new UpdateProgressEvent(
1298 Thread.currentThread(), message, updateProgress));
1302 private String generateAppYaml(File stageDir, String runtime, AppEngineWebXml aeWebXml) {
1303 Set<String> staticFiles = new HashSet<String>();
1304 for (File f : new FileIterator(new File(stageDir, "__static__"))) {
1305 staticFiles.add(Utility.calculatePath(f, stageDir));
1308 AppYamlTranslator translator =
1309 new AppYamlTranslator(aeWebXml, getWebXml(), getBackendsXml(),
1310 getApiVersion(), staticFiles, null, runtime, getSdkVersion());
1311 String yaml = translator.getYaml();
1312 logger.fine("Generated app.yaml file:\n" + yaml);
1313 return yaml;
1317 * Returns the app.yaml string.
1319 * @throws IllegalStateException if createStagingDirectory has not been called.
1321 @Override
1322 public String getAppYaml() {
1323 if (appYaml == null) {
1324 throw new IllegalStateException("Must call createStagingDirectory first.");
1326 return appYaml;
1329 private void writeDefaultWebXml(File webXmlFile) {
1330 try {
1331 Files.write(DEFAULT_WEB_XML_CONTENT, webXmlFile, UTF_8);
1332 } catch (IOException e) {
1333 throw new AppEngineConfigException("Error: "
1334 + "Could not autogenerate file " + webXmlFile.getAbsolutePath());
1339 * Generates a quickstart-web.xml. Minimizes and saves in min-quickstart-web.xml
1340 * @return Relative path to min-quickstart-web.xml
1342 private void createQuickstartWebXml(ApplicationProcessingOptions opts)
1343 throws IOException, SAXException, ParserConfigurationException, TransformerException {
1344 String javaCmd = opts.getJavaExecutable().getPath();
1346 String quickstartJar = new File(SdkInfo.getSdkRoot(),
1347 "lib/java-managed-vm/appengine-java-vmruntime/quickstartgenerator.jar").getAbsolutePath();
1349 String[] args = {
1350 javaCmd,
1351 "-jar", quickstartJar,
1352 stageDir.getAbsolutePath()
1354 Process quickstartProcess = startProcess(args);
1356 int status;
1357 try {
1358 status = quickstartProcess.waitFor();
1359 } catch (InterruptedException ex) {
1360 status = 1;
1363 if (status != 0) {
1364 detailsWriter.println("Error while executing: " + formatCommand(Arrays.asList(args)));
1365 throw new RuntimeException("Failed to generate quickstart-web.xml.");
1368 File webDefaultXml = new File(SdkInfo.getSdkRoot() + "/lib/jetty-base-sdk/etc/webdefault.xml");
1369 File quickstartXml = new File(stageDir, "/WEB-INF/quickstart-web.xml");
1370 File minimizedQuickstartXml = new File(stageDir, "/WEB-INF/min-quickstart-web.xml");
1372 Document quickstartDoc = getFilteredQuickstartDoc(quickstartXml, webDefaultXml);
1374 Transformer transformer = TransformerFactory.newInstance().newTransformer();
1375 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1376 StreamResult result = new StreamResult(new FileWriter(minimizedQuickstartXml));
1377 DOMSource source = new DOMSource(quickstartDoc);
1378 transformer.transform(source, result);
1382 * Removes mappings from quickstart-web.xml that come from webdefault.xml.
1384 * The quickstart-web.xml generated by the quickstartgenerator process includes
1385 * the contents of the user's web.xml, entries derived from Java annotations,
1386 * and entries derived from the contents of webdefault.xml. All of those are
1387 * appropriate for the Java web server. But when generating an app.yaml for
1388 * appcfg or dev_appserver, the webdefault.xml entries are not appropriate, since
1389 * app.yaml should only reflect what is specific to the user's app. So this
1390 * method returns a modified min-quickstart-web Document from which the webdefault.xml
1391 * entries have been removed. Specifically, we look at the <url-pattern> inside
1392 * every <servlet-mapping> or <filter-mapping> element in webdefault.xml to
1393 * determine default patterns; then we look at those elements inside
1394 * quickstart-web.xml and remove any whose <url-pattern> is one of the default
1395 * patterns.
1397 * @return a filtered quickstart Document object appropriate for translation to app.yaml
1399 static Document getFilteredQuickstartDoc(File quickstartXml, File webDefaultXml)
1400 throws ParserConfigurationException, IOException, SAXException {
1402 DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
1403 DocumentBuilder webDefaultDocBuilder = docBuilderFactory.newDocumentBuilder();
1404 Document webDefaultDoc = webDefaultDocBuilder.parse(webDefaultXml);
1405 DocumentBuilder quickstartDocBuilder = docBuilderFactory.newDocumentBuilder();
1406 Document quickstartDoc = quickstartDocBuilder.parse(quickstartXml);
1407 final Set<String> tagsToExamine = ImmutableSet.of("filter-mapping", "servlet-mapping");
1408 final String urlPatternTag = "url-pattern";
1410 Set<String> defaultRoots = Sets.newHashSet();
1411 List<Node> nodesToRemove = Lists.newArrayList();
1413 webDefaultDoc.getDocumentElement().normalize();
1414 NodeList webDefaultChildren = webDefaultDoc.getDocumentElement()
1415 .getElementsByTagName(urlPatternTag);
1416 for (int i = 0; i < webDefaultChildren.getLength(); i++) {
1417 Node child = webDefaultChildren.item(i);
1418 if (tagsToExamine.contains(child.getParentNode().getNodeName())) {
1419 String url = child.getTextContent().trim();
1420 if (url.startsWith("/")) {
1421 defaultRoots.add(url);
1426 quickstartDoc.getDocumentElement().normalize();
1427 NodeList quickstartChildren = quickstartDoc.getDocumentElement()
1428 .getElementsByTagName(urlPatternTag);
1429 for (int i = 0; i < quickstartChildren.getLength(); i++) {
1430 Node child = quickstartChildren.item(i);
1431 if (tagsToExamine.contains(child.getParentNode().getNodeName())) {
1432 String url = child.getTextContent().trim();
1433 if (defaultRoots.contains(url)) {
1434 nodesToRemove.add(child.getParentNode());
1438 for (Node node : nodesToRemove) {
1439 quickstartDoc.getDocumentElement().removeChild(node);
1442 return quickstartDoc;