Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / Application.java
blob362ef8b8edd2272d41b920a994f5a6cd7599e532
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.admin;
5 import com.google.appengine.tools.admin.AppAdminFactory.ApplicationProcessingOptions;
6 import com.google.appengine.tools.info.SdkImplInfo;
7 import com.google.appengine.tools.info.SdkInfo;
8 import com.google.appengine.tools.info.Version;
9 import com.google.appengine.tools.util.ApiVersionFinder;
10 import com.google.appengine.tools.util.FileIterator;
11 import com.google.appengine.tools.util.JarSplitter;
12 import com.google.appengine.tools.util.JarTool;
13 import com.google.apphosting.utils.config.AppEngineConfigException;
14 import com.google.apphosting.utils.config.AppEngineWebXml;
15 import com.google.apphosting.utils.config.AppEngineWebXmlReader;
16 import com.google.apphosting.utils.config.AppYamlProcessor;
17 import com.google.apphosting.utils.config.BackendsXml;
18 import com.google.apphosting.utils.config.BackendsXmlReader;
19 import com.google.apphosting.utils.config.BackendsYamlReader;
20 import com.google.apphosting.utils.config.CronXml;
21 import com.google.apphosting.utils.config.CronXmlReader;
22 import com.google.apphosting.utils.config.CronYamlReader;
23 import com.google.apphosting.utils.config.DispatchXml;
24 import com.google.apphosting.utils.config.DispatchXmlReader;
25 import com.google.apphosting.utils.config.DispatchYamlReader;
26 import com.google.apphosting.utils.config.DosXml;
27 import com.google.apphosting.utils.config.DosXmlReader;
28 import com.google.apphosting.utils.config.DosYamlReader;
29 import com.google.apphosting.utils.config.GenerationDirectory;
30 import com.google.apphosting.utils.config.IndexesXml;
31 import com.google.apphosting.utils.config.IndexesXmlReader;
32 import com.google.apphosting.utils.config.QueueXml;
33 import com.google.apphosting.utils.config.QueueXmlReader;
34 import com.google.apphosting.utils.config.QueueYamlReader;
35 import com.google.apphosting.utils.config.WebXml;
36 import com.google.apphosting.utils.config.WebXmlReader;
37 import com.google.common.collect.ImmutableSet;
38 import com.google.common.collect.Lists;
39 import com.google.common.collect.Sets;
40 import com.google.common.io.Files;
42 import org.mortbay.io.Buffer;
43 import org.mortbay.jetty.MimeTypes;
44 import org.w3c.dom.Document;
45 import org.w3c.dom.Node;
46 import org.w3c.dom.NodeList;
47 import org.xml.sax.SAXException;
49 import java.io.File;
50 import java.io.FileInputStream;
51 import java.io.FileNotFoundException;
52 import java.io.FileOutputStream;
53 import java.io.FileWriter;
54 import java.io.IOException;
55 import java.io.PrintWriter;
56 import java.net.URL;
57 import java.net.URLConnection;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.HashSet;
61 import java.util.List;
62 import java.util.Set;
63 import java.util.logging.Level;
64 import java.util.logging.Logger;
65 import java.util.regex.Pattern;
67 import javax.activation.FileTypeMap;
68 import javax.tools.JavaCompiler;
69 import javax.tools.JavaFileObject;
70 import javax.tools.StandardJavaFileManager;
71 import javax.tools.ToolProvider;
72 import javax.xml.XMLConstants;
73 import javax.xml.parsers.DocumentBuilder;
74 import javax.xml.parsers.DocumentBuilderFactory;
75 import javax.xml.parsers.ParserConfigurationException;
76 import javax.xml.transform.OutputKeys;
77 import javax.xml.transform.Transformer;
78 import javax.xml.transform.TransformerException;
79 import javax.xml.transform.TransformerFactory;
80 import javax.xml.transform.dom.DOMSource;
81 import javax.xml.transform.stream.StreamResult;
82 import javax.xml.transform.stream.StreamSource;
83 import javax.xml.validation.SchemaFactory;
85 /**
86 * An App Engine application. You can {@link #readApplication read} an
87 * {@code Application} from a path, and
88 * {@link com.google.appengine.tools.admin.AppAdminFactory#createAppAdmin create}
89 * an {@link com.google.appengine.tools.admin.AppAdmin} to upload, create
90 * indexes, or otherwise manage it.
93 public class Application implements GenericApplication {
95 private static final int MAX_COMPILED_JSP_JAR_SIZE = 1024 * 1024 * 5;
96 private static final String COMPILED_JSP_JAR_NAME_PREFIX = "_ah_compiled_jsps";
98 private static final int MAX_CLASSES_JAR_SIZE = 1024 * 1024 * 5;
99 private static final String CLASSES_JAR_NAME_PREFIX = "_ah_webinf_classes";
101 private static final String JAVA_7_RUNTIME_ID = "java7";
102 private static final ImmutableSet<String> ALLOWED_RUNTIME_IDS = ImmutableSet.of(
103 JAVA_7_RUNTIME_ID);
105 private static Pattern JSP_REGEX = Pattern.compile(".*\\.jspx?");
107 /** If available, this is set to a program to make symlinks, e.g. /bin/ln */
108 private static File ln = Utility.findLink();
109 private static File sdkDocsDir;
110 public static synchronized File getSdkDocsDir(){
111 if (null == sdkDocsDir){
112 sdkDocsDir = new File(SdkInfo.getSdkRoot(), "docs");
114 return sdkDocsDir;
117 private static Version sdkVersion;
118 public static synchronized Version getSdkVersion() {
119 if (null == sdkVersion) {
120 sdkVersion = SdkInfo.getLocalVersion();
122 return sdkVersion;
125 private static final String STAGEDIR_PREFIX = "appcfg";
127 private static final Logger logger = Logger.getLogger(Application.class.getName());
129 private static final MimeTypes mimeTypes = new MimeTypes();
131 private AppEngineWebXml appEngineWebXml;
132 private WebXml webXml;
133 private CronXml cronXml;
134 private DispatchXml dispatchXml;
135 private DosXml dosXml;
136 private String pagespeedYaml;
137 private QueueXml queueXml;
138 private IndexesXml indexesXml;
139 private BackendsXml backendsXml;
140 private File baseDir;
141 private File stageDir;
142 private File externalResourceDir;
143 private String apiVersion;
144 private String appYaml;
146 private UpdateListener listener;
147 private PrintWriter detailsWriter;
148 private int updateProgress = 0;
149 private int progressAmount = 0;
151 protected Application(){
155 * Builds a normalized path for the given directory in which
156 * forward slashes are used as the file separator on all platforms.
157 * @param dir A directory
158 * @return The normalized path
160 private static String buildNormalizedPath(File dir) {
161 String normalizedPath = dir.getPath();
162 if (File.separatorChar == '\\') {
163 normalizedPath = normalizedPath.replace('\\', '/');
165 return normalizedPath;
168 private Application(String explodedPath, String appId, String module, String appVersion) {
169 this.baseDir = new File(explodedPath);
170 explodedPath = buildNormalizedPath(baseDir);
171 File webinf = new File(baseDir, "WEB-INF");
172 if (!webinf.getName().equals("WEB-INF")) {
173 throw new AppEngineConfigException("WEB-INF directory must be capitalized.");
176 String webinfPath = webinf.getPath();
177 AppEngineWebXmlReader aewebReader = new AppEngineWebXmlReader(explodedPath);
178 WebXmlReader webXmlReader = new WebXmlReader(explodedPath);
179 AppYamlProcessor.convert(webinf, aewebReader.getFilename(), webXmlReader.getFilename());
181 validateXml(aewebReader.getFilename(), new File(getSdkDocsDir(), "appengine-web.xsd"));
182 appEngineWebXml = aewebReader.readAppEngineWebXml();
183 appEngineWebXml.setSourcePrefix(explodedPath);
184 if (appId == null) {
185 if (appEngineWebXml.getAppId() == null) {
186 throw new AppEngineConfigException(
187 "No app id supplied and XML files have no <application> element");
189 } else {
190 appEngineWebXml.setAppId(appId);
192 if (module != null) {
193 appEngineWebXml.setModule(module);
195 if (appVersion != null) {
196 appEngineWebXml.setMajorVersionId(appVersion);
199 webXml = webXmlReader.readWebXml();
200 webXml.validate();
202 CronXmlReader cronReader = new CronXmlReader(explodedPath);
203 validateXml(cronReader.getFilename(), new File(getSdkDocsDir(), "cron.xsd"));
204 cronXml = cronReader.readCronXml();
205 if (cronXml == null) {
206 CronYamlReader cronYaml = new CronYamlReader(webinfPath);
207 cronXml = cronYaml.parse();
210 QueueXmlReader queueReader = new QueueXmlReader(explodedPath);
211 validateXml(queueReader.getFilename(), new File(getSdkDocsDir(), "queue.xsd"));
212 queueXml = queueReader.readQueueXml();
213 if (queueXml == null) {
214 QueueYamlReader queueYaml = new QueueYamlReader(webinfPath);
215 queueXml = queueYaml.parse();
218 DispatchXmlReader dispatchXmlReader = new DispatchXmlReader(explodedPath,
219 DispatchXmlReader.DEFAULT_RELATIVE_FILENAME);
220 validateXml(dispatchXmlReader.getFilename(), new File(getSdkDocsDir(), "dispatch.xsd"));
221 dispatchXml = dispatchXmlReader.readDispatchXml();
222 if (dispatchXml == null) {
223 DispatchYamlReader dispatchYamlReader = new DispatchYamlReader(webinfPath);
224 dispatchXml = dispatchYamlReader.parse();
227 DosXmlReader dosReader = new DosXmlReader(explodedPath);
228 validateXml(dosReader.getFilename(), new File(getSdkDocsDir(), "dos.xsd"));
229 dosXml = dosReader.readDosXml();
230 if (dosXml == null) {
231 DosYamlReader dosYaml = new DosYamlReader(webinfPath);
232 dosXml = dosYaml.parse();
235 if (getAppEngineWebXml().getPagespeed() != null) {
236 StringBuilder pagespeedYamlBuilder = new StringBuilder();
237 AppYamlTranslator.appendPagespeed(
238 getAppEngineWebXml().getPagespeed(), pagespeedYamlBuilder, 0);
239 pagespeedYaml = pagespeedYamlBuilder.toString();
242 IndexesXmlReader indexReader = new IndexesXmlReader(explodedPath);
243 File datastoreSchema = new File(getSdkDocsDir(), "datastore-indexes.xsd");
244 validateXml(indexReader.getFilename(), datastoreSchema);
245 indexesXml = indexReader.readIndexesXml();
247 BackendsXmlReader backendsReader = new BackendsXmlReader(explodedPath);
248 validateXml(backendsReader.getFilename(), new File(getSdkDocsDir(), "backends.xsd"));
249 backendsXml = backendsReader.readBackendsXml();
250 if (backendsXml == null) {
251 BackendsYamlReader backendsYaml = new BackendsYamlReader(webinfPath);
252 backendsXml = backendsYaml.parse();
257 * Reads the App Engine application from {@code path}. The path may either
258 * be a WAR file or the root of an exploded WAR directory.
260 * @param path a not {@code null} path.
262 * @throws IOException if an error occurs while trying to read the
263 * {@code Application}.
264 * @throws com.google.apphosting.utils.config.AppEngineConfigException if the
265 * {@code Application's} appengine-web.xml file is malformed.
267 public static Application readApplication(String path)
268 throws IOException {
269 return new Application(path, null, null, null);
273 * Sets the external resource directory. Call this method before invoking
274 * {@link #createStagingDirectory(ApplicationProcessingOptions, ResourceLimits)}.
275 * <p>
276 * The external resource directory is a directory outside of the war directory where additional
277 * files live. These files will be copied into the staging directory during an upload, after the
278 * war directory is copied there. Consequently if there are any name collisions the files in the
279 * external resource directory will win.
281 * @param path a not {@code null} path to an existing directory.
283 * @throws IllegalArgumentException If {@code path} does not refer to an existing
284 * directory.
286 public void setExternalResourceDir(String path) {
287 if (path == null) {
288 throw new NullPointerException("path is null");
290 if (stageDir != null) {
291 throw new IllegalStateException(
292 "This method must be invoked prior to createStagingDirectory()");
294 File dir = new File(path);
295 if (!dir.exists()) {
296 throw new IllegalArgumentException("path does not exist: " + path);
298 if (!dir.isDirectory()) {
299 throw new IllegalArgumentException(path + " is not a directory.");
301 this.externalResourceDir = dir;
305 * Reads the App Engine application from {@code path}. The path may either
306 * be a WAR file or the root of an exploded WAR directory.
308 * @param path a not {@code null} path.
309 * @param appId if non-null, use this as an application id override.
310 * @param module if non-null, use this as a module id override.
311 * @param appVersion if non-null, use this as an application version override.
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 String appId,
320 String module,
321 String appVersion) throws IOException {
322 return new Application(path, appId, module, appVersion);
326 * Returns the application identifier, from the AppEngineWebXml config
327 * @return application identifier
329 @Override
330 public String getAppId() {
331 return appEngineWebXml.getAppId();
335 * Returns the application version, from the AppEngineWebXml config
336 * @return application version
338 @Override
339 public String getVersion() {
340 return appEngineWebXml.getMajorVersionId();
343 @Override
344 public String getSourceLanguage() {
345 return appEngineWebXml.getSourceLanguage();
348 @Override
349 public String getModule() {
350 return appEngineWebXml.getModule();
353 @Override
354 public String getInstanceClass() {
355 return appEngineWebXml.getInstanceClass();
358 @Override
359 public boolean isPrecompilationEnabled() {
360 return appEngineWebXml.getPrecompilationEnabled();
363 @Override
364 public List<ErrorHandler> getErrorHandlers() {
365 class ErrorHandlerImpl implements ErrorHandler {
366 private final AppEngineWebXml.ErrorHandler errorHandler;
367 public ErrorHandlerImpl(AppEngineWebXml.ErrorHandler errorHandler) {
368 this.errorHandler = errorHandler;
370 @Override
371 public String getFile() {
372 return "__static__/" + errorHandler.getFile();
374 @Override
375 public String getErrorCode() {
376 return errorHandler.getErrorCode();
378 @Override
379 public String getMimeType() {
380 return getMimeTypeIfStatic(getFile());
383 List<ErrorHandler> errorHandlers = new ArrayList<ErrorHandler>();
384 for (AppEngineWebXml.ErrorHandler errorHandler: appEngineWebXml.getErrorHandlers()) {
385 errorHandlers.add(new ErrorHandlerImpl(errorHandler));
387 return errorHandlers;
390 @Override
391 public String getMimeTypeIfStatic(String path) {
392 if (!path.contains("__static__/")) {
393 return null;
395 String mimeType = webXml.getMimeTypeForPath(path);
396 if (mimeType != null) {
397 return mimeType;
399 return guessContentTypeFromName(path);
403 * @param fileName path of a file with extension
404 * @return the mimetype of the file (or application/octect-stream if not recognized)
406 public static String guessContentTypeFromName(String fileName) {
407 String defaultValue = "application/octet-stream";
408 try {
409 Buffer buffer = mimeTypes.getMimeByExtension(fileName);
410 if (buffer != null) {
411 return new String(buffer.asArray());
413 String lowerName = fileName.toLowerCase();
414 if (lowerName.endsWith(".json")) {
415 return "application/json";
417 FileTypeMap typeMap = FileTypeMap.getDefaultFileTypeMap();
418 String ret = typeMap.getContentType(fileName);
419 if (ret != null) {
420 return ret;
422 ret = URLConnection.guessContentTypeFromName(fileName);
423 if (ret != null) {
424 return ret;
426 return defaultValue;
427 } catch (Throwable t) {
428 logger.log(Level.WARNING, "Error identify mimetype for " + fileName, t);
429 return defaultValue;
433 * Returns the AppEngineWebXml describing the application.
435 * @return a not {@code null} deployment descriptor
437 public AppEngineWebXml getAppEngineWebXml() {
438 return appEngineWebXml;
442 * Returns the AppEngineWebXml with application and version removed
444 * @return a not {@code null} deployment descriptor
446 public AppEngineWebXml getScrubbedAppEngineWebXml() {
447 AppEngineWebXml scrubbedAppEngineWebXml = appEngineWebXml.clone();
448 scrubbedAppEngineWebXml.setAppId(null);
449 scrubbedAppEngineWebXml.setMajorVersionId(null);
450 return scrubbedAppEngineWebXml;
454 * Returns the CronXml describing the applications' cron jobs.
455 * @return a cron descriptor, possibly empty or {@code null}
457 @Override
458 public CronXml getCronXml() {
459 return cronXml;
463 * Returns the QueueXml describing the applications' task queues.
464 * @return a queue descriptor, possibly empty or {@code null}
466 @Override
467 public QueueXml getQueueXml() {
468 return queueXml;
471 @Override
472 public DispatchXml getDispatchXml() {
473 return dispatchXml;
477 * Returns the DosXml describing the applications' DoS entries.
478 * @return a dos descriptor, possibly empty or {@code null}
480 @Override
481 public DosXml getDosXml() {
482 return dosXml;
486 * Returns the pagespeed.yaml describing the applications' PageSpeed configuration.
487 * @return a pagespeed.yaml string, possibly empty or {@code null}
489 @Override
490 public String getPagespeedYaml() {
491 return pagespeedYaml;
495 * Returns the IndexesXml describing the applications' indexes.
496 * @return a index descriptor, possibly empty or {@code null}
498 @Override
499 public IndexesXml getIndexesXml() {
500 return indexesXml;
504 * Returns the WebXml describing the applications' servlets and generic web
505 * application information.
507 * @return a WebXml descriptor, possibly empty but not {@code null}
509 public WebXml getWebXml() {
510 return webXml;
513 @Override
514 public BackendsXml getBackendsXml() {
515 return backendsXml;
519 * Returns the desired API version for the current application, or
520 * {@code "none"} if no API version was used.
522 * @throws IllegalStateException if createStagingDirectory has not been called.
524 @Override
525 public String getApiVersion() {
526 if (apiVersion == null) {
527 throw new IllegalStateException("Must call createStagingDirectory first.");
529 return apiVersion;
533 * Returns a path to an exploded WAR directory for the application.
534 * This may be a temporary directory.
536 * @return a not {@code null} path pointing to a directory
538 @Override
539 public String getPath() {
540 return baseDir.getAbsolutePath();
544 * Returns the staging directory, or {@code null} if none has been created.
546 @Override
547 public File getStagingDir() {
548 return stageDir;
551 @Override
552 public void resetProgress() {
553 updateProgress = 0;
554 progressAmount = 0;
558 * Creates a new staging directory, if needed, or returns the existing one
559 * if already created.
561 * @param opts User-specified options for processing the application.
562 * @return staging directory
563 * @throws IOException
565 @Override
566 public File createStagingDirectory(ApplicationProcessingOptions opts,
567 ResourceLimits resourceLimits) throws IOException {
568 if (stageDir != null) {
569 return stageDir;
572 int i = 0;
573 while (stageDir == null && i++ < 3) {
574 try {
575 stageDir = File.createTempFile(STAGEDIR_PREFIX, null);
576 } catch (IOException ex) {
577 continue;
579 stageDir.delete();
580 if (!stageDir.mkdir()) {
581 stageDir = null;
584 if (i == 3) {
585 throw new IOException("Couldn't create a temporary directory in 3 tries.");
587 statusUpdate("Created staging directory at: '" + stageDir.getPath() + "'", 20);
589 return populateStagingDirectory(opts, resourceLimits, false);
593 * Populates and creates (if necessary) a user specified, staging directory
595 * @param opts User-specified options for processing the application.
596 * @param resourceLimits Various resource limits provided by the cloud.
597 * @param stagingDir User-specified staging directory (must be empty or not exist)
598 * @return staging directory
599 * @throws IOException if an error occurs trying to create or populate the staging directory
601 @Override
602 public File createStagingDirectory(ApplicationProcessingOptions opts,
603 ResourceLimits resourceLimits, File stagingDir) throws IOException {
604 if (!stagingDir.exists()) {
605 if (!stagingDir.mkdir()) {
606 throw new IOException("Could not create staging directory at " + stagingDir.getPath());
610 stageDir = stagingDir;
611 ln = null;
613 populateStagingDirectory(opts, resourceLimits, true);
614 copyOrLinkDirectories(GenerationDirectory.getGenerationDirectory(stageDir), stageDir);
615 return stageDir;
618 private File populateStagingDirectory(ApplicationProcessingOptions opts,
619 ResourceLimits resourceLimits, boolean writeScrubbedAppYaml) throws IOException {
620 File staticDir = new File(stageDir, "__static__");
621 staticDir.mkdir();
622 copyOrLink(baseDir, stageDir, staticDir, false, opts);
623 if (externalResourceDir != null) {
624 String previousPrefix = appEngineWebXml.getSourcePrefix();
625 String newPrefix = buildNormalizedPath(externalResourceDir);
626 try {
627 appEngineWebXml.setSourcePrefix(newPrefix);
628 copyOrLink(externalResourceDir, stageDir, staticDir, false, opts);
629 } finally {
630 appEngineWebXml.setSourcePrefix(previousPrefix);
634 apiVersion = findApiVersion(stageDir, true);
636 String runtime = getRuntime(opts);
638 if (opts.isCompileJspsSet()) {
639 compileJsps(stageDir, opts);
642 if (opts.isQuickstart()) {
643 try {
644 createQuickstartWebXml(opts);
645 webXml = new WebXmlReader(stageDir.getAbsolutePath(), "/WEB-INF/min-quickstart-web.xml")
646 .readWebXml();
647 } catch (SAXException | ParserConfigurationException | TransformerException e) {
648 throw new IOException(e);
652 appYaml = generateAppYaml(stageDir, runtime, appEngineWebXml);
654 if (GenerationDirectory.getGenerationDirectory(stageDir).mkdirs()) {
655 writePreparedYamlFile("app", writeScrubbedAppYaml
656 ? generateAppYaml(stageDir, runtime, getScrubbedAppEngineWebXml()) : appYaml);
657 writePreparedYamlFile("backends", backendsXml == null ? null : backendsXml.toYaml());
658 writePreparedYamlFile("index", indexesXml.size() == 0 ? null : indexesXml.toYaml());
659 writePreparedYamlFile("cron", cronXml == null ? null : cronXml.toYaml());
660 writePreparedYamlFile("queue", queueXml == null ? null : queueXml.toYaml());
661 writePreparedYamlFile("dos", dosXml == null ? null : dosXml.toYaml());
664 int maxJarSize = (int) resourceLimits.maxFileSize();
666 if (opts.isSplitJarsSet()) {
667 splitJars(new File(new File(stageDir, "WEB-INF"), "lib"),
668 maxJarSize, opts.getJarSplittingExcludes());
671 return stageDir;
674 @Override
675 public void exportRepoInfoFile() {
676 File target = new File(stageDir, "WEB-INF/classes/source-context.json");
677 if (target.exists()) {
678 return;
681 RepoInfo repoInfo = new RepoInfo(baseDir);
682 if (!repoInfo.generate(target)) {
683 return;
686 statusUpdate("Generated git repository information file.");
690 * Write yaml file to generation subdirectory within stage directory.
692 private void writePreparedYamlFile(String yamlName, String yamlString) throws IOException {
693 File f = new File(GenerationDirectory.getGenerationDirectory(stageDir), yamlName + ".yaml");
694 if (yamlString != null && f.createNewFile()) {
695 FileWriter fw = new FileWriter(f);
696 fw.write(yamlString);
697 fw.close();
701 private static String findApiVersion(File baseDir, boolean deleteApiJars) {
702 ApiVersionFinder finder = new ApiVersionFinder();
704 String foundApiVersion = null;
705 File webInf = new File(baseDir, "WEB-INF");
706 File libDir = new File(webInf, "lib");
707 for (File file : new FileIterator(libDir)) {
708 if (file.getPath().endsWith(".jar")) {
709 try {
710 String apiVersion = finder.findApiVersion(file);
711 if (apiVersion != null) {
712 if (foundApiVersion == null) {
713 foundApiVersion = apiVersion;
714 } else if (!foundApiVersion.equals(apiVersion)) {
715 logger.warning("Warning: found duplicate API version: " + foundApiVersion +
716 ", using " + apiVersion);
718 if (deleteApiJars) {
719 if (!file.delete()) {
720 logger.log(Level.SEVERE, "Could not delete API jar: " + file);
724 } catch (IOException ex) {
725 logger.log(Level.WARNING, "Could not identify API version in " + file, ex);
730 if (foundApiVersion == null) {
731 foundApiVersion = "none";
733 return foundApiVersion;
737 * Returns the runtime id to use in the generated app.yaml.
739 * This method returns {@code "java7"}, unless an explicit runtime id was specified
740 * using the {@code -r} option.
742 * Before accepting an explicit runtime id, this method validates it against the list of
743 * supported Java runtimes (currently only {@code "java7"}), unless validation was turned
744 * off using the {@code --allowAnyRuntimes} option.
746 private String getRuntime(ApplicationProcessingOptions opts) {
747 String runtime = opts.getRuntime();
748 if (runtime != null) {
749 if (!opts.isAllowAnyRuntime() && !ALLOWED_RUNTIME_IDS.contains(runtime)) {
750 throw new AppEngineConfigException("Invalid runtime id: " + runtime + ". Valid " +
751 "runtime id: java7.");
753 return runtime;
755 return JAVA_7_RUNTIME_ID;
759 * Validates a given XML document against a given schema.
761 * @param xmlFilename filename with XML document
762 * @param schema XSD schema to validate with
764 * @throws AppEngineConfigException for malformed XML, or IO errors
766 private static void validateXml(String xmlFilename, File schema) {
767 File xml = new File(xmlFilename);
768 if (!xml.exists()) {
769 return;
771 try {
772 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
773 try {
774 factory.newSchema(schema).newValidator().validate(
775 new StreamSource(new FileInputStream(xml)));
776 } catch (SAXException ex) {
777 throw new AppEngineConfigException("XML error validating " +
778 xml.getPath() + " against " + schema.getPath(), ex);
780 } catch (IOException ex) {
781 throw new AppEngineConfigException("IO error validating " +
782 xml.getPath() + " against " + schema.getPath(), ex);
786 private static final String JSPC_MAIN = "com.google.appengine.tools.development.LocalJspC";
788 private void compileJsps(File stage, ApplicationProcessingOptions opts)
789 throws IOException {
790 statusUpdate("Scanning for jsp files.");
792 if (matchingFileExists(new File(stage.getPath()), JSP_REGEX)) {
793 statusUpdate("Compiling jsp files.");
795 File webInf = new File(stage, "WEB-INF");
797 for (File file : SdkImplInfo.getUserJspLibFiles()) {
798 copyOrLinkFile(file, new File(new File(webInf, "lib"), file.getName()));
800 for (File file : SdkImplInfo.getSharedJspLibFiles()) {
801 copyOrLinkFile(file, new File(new File(webInf, "lib"), file.getName()));
804 File classes = new File(webInf, "classes");
805 File generatedWebXml = new File(webInf, "generated_web.xml");
806 File tempDir = Files.createTempDir();
807 String classpath = getJspClasspath(classes, tempDir);
809 String javaCmd = opts.getJavaExecutable().getPath();
810 String[] args = new String[] {
811 javaCmd,
812 "-classpath", classpath,
813 JSPC_MAIN,
814 "-uriroot", stage.getPath(),
815 "-p", "org.apache.jsp",
816 "-l", "-v",
817 "-webinc", generatedWebXml.getPath(),
818 "-d", tempDir.getPath(),
819 "-javaEncoding", opts.getCompileEncoding(),
821 Process jspc = startProcess(args);
823 int status = 1;
824 try {
825 status = jspc.waitFor();
826 } catch (InterruptedException ex) { }
828 if (status != 0) {
829 detailsWriter.println("Error while executing: " + formatCommand(Arrays.asList(args)));
830 throw new JspCompilationException("Failed to compile jsp files.",
831 JspCompilationException.Source.JASPER);
834 compileJavaFiles(classpath, webInf, tempDir, opts);
836 webXml = new WebXmlReader(stage.getPath()).readWebXml();
840 private void compileJavaFiles(String classpath, File webInf, File jspClassDir,
841 ApplicationProcessingOptions opts) throws IOException {
843 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
844 if (compiler == null) {
845 throw new RuntimeException(
846 "Cannot get the System Java Compiler. Please use a JDK, not a JRE.");
848 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
850 ArrayList<File> files = new ArrayList<File>();
851 for (File f : new FileIterator(jspClassDir)) {
852 if (f.getPath().toLowerCase().endsWith(".java")) {
853 files.add(f);
856 if (files.isEmpty()) {
857 return;
859 List<String> optionList = new ArrayList<String>();
860 optionList.addAll(Arrays.asList("-classpath", classpath.toString()));
861 optionList.addAll(Arrays.asList("-d", jspClassDir.getPath()));
862 optionList.addAll(Arrays.asList("-encoding", opts.getCompileEncoding()));
863 optionList.addAll(Arrays.asList("-source", "7"));
864 optionList.addAll(Arrays.asList("-target", "7"));
866 Iterable<? extends JavaFileObject> compilationUnits =
867 fileManager.getJavaFileObjectsFromFiles(files);
868 boolean success = compiler.getTask(
869 null, fileManager, null, optionList, null, compilationUnits).call();
870 fileManager.close();
872 if (!success) {
873 throw new JspCompilationException("Failed to compile the generated JSP java files.",
874 JspCompilationException.Source.JSPC);
876 if (opts.isJarJSPsSet()) {
877 zipJasperGeneratedFiles(webInf, jspClassDir);
878 } else {
879 copyOrLinkDirectories(jspClassDir, new File(webInf, "classes"));
881 if (opts.isDeleteJSPs()) {
882 for (File f : new FileIterator(webInf.getParentFile())) {
883 if (f.getPath().toLowerCase().endsWith(".jsp")) {
884 f.delete();
888 if (opts.isJarClassesSet()) {
889 zipWebInfClassesFiles(webInf);
894 private void zipJasperGeneratedFiles(File webInfDir, File jspClassDir) throws IOException {
895 Set<String> fileTypesToExclude = ImmutableSet.of(".java");
896 File libDir = new File(webInfDir, "lib");
897 JarTool jarTool = new JarTool(
898 COMPILED_JSP_JAR_NAME_PREFIX, jspClassDir, libDir, MAX_COMPILED_JSP_JAR_SIZE,
899 fileTypesToExclude);
900 jarTool.run();
901 recursiveDelete(jspClassDir);
904 private void zipWebInfClassesFiles(File webInfDir) throws IOException {
905 File libDir = new File(webInfDir, "lib");
906 File classesDir = new File(webInfDir, "classes");
907 JarTool jarTool = new JarTool(
908 CLASSES_JAR_NAME_PREFIX, classesDir, libDir, MAX_CLASSES_JAR_SIZE,
909 null);
910 jarTool.run();
911 recursiveDelete(classesDir);
912 classesDir.mkdir();
915 private String getJspClasspath(File classDir, File genDir) {
916 StringBuilder classpath = new StringBuilder();
917 for (URL lib : SdkImplInfo.getImplLibs()) {
918 classpath.append(lib.getPath());
919 classpath.append(File.pathSeparatorChar);
921 for (File lib : SdkInfo.getSharedLibFiles()) {
922 classpath.append(lib.getPath());
923 classpath.append(File.pathSeparatorChar);
926 classpath.append(classDir.getPath());
927 classpath.append(File.pathSeparatorChar);
928 classpath.append(genDir.getPath());
929 classpath.append(File.pathSeparatorChar);
931 for (File f : new FileIterator(new File(classDir.getParentFile(), "lib"))) {
932 String filename = f.getPath().toLowerCase();
933 if (filename.endsWith(".jar") || filename.endsWith(".zip")) {
934 classpath.append(f.getPath());
935 classpath.append(File.pathSeparatorChar);
939 return classpath.toString();
942 private Process startProcess(String... args) throws IOException {
943 ProcessBuilder builder = new ProcessBuilder(args);
944 Process proc = builder.redirectErrorStream(true).start();
945 logger.fine(formatCommand(builder.command()));
946 new Thread(new OutputPump(proc.getInputStream(), detailsWriter)).start();
947 return proc;
950 private String formatCommand(Iterable<String> args) {
951 StringBuilder command = new StringBuilder();
952 for (String chunk : args) {
953 command.append(chunk);
954 command.append(" ");
956 return command.toString();
960 * Scans a given directory tree, testing whether any file matches a given
961 * pattern.
963 * @param dir the directory under which to scan
964 * @param regex the pattern to look for
965 * @returns Returns {@code true} on the first instance of such a file,
966 * {@code false} otherwise.
968 private static boolean matchingFileExists(File dir, Pattern regex) {
969 for (File file : dir.listFiles()) {
970 if (file.isDirectory()) {
971 if (matchingFileExists(file, regex)) {
972 return true;
974 } else {
975 if (regex.matcher(file.getName()).matches()) {
976 return true;
980 return false;
984 * Invokes the JarSplitter code on any jar files found in {@code dir}. Any
985 * jars larger than {@code max} will be split into fragments of at most that
986 * size.
987 * @param dir the directory to search, recursively
988 * @param max the maximum allowed size
989 * @param excludes a set of suffixes to exclude.
990 * @throws IOException on filesystem errors.
992 private static void splitJars(File dir, int max, Set<String> excludes) throws IOException {
993 String children[] = dir.list();
994 if (children == null) {
995 return;
997 for (String name : children) {
998 File subfile = new File(dir, name);
999 if (subfile.isDirectory()) {
1000 splitJars(subfile, max, excludes);
1001 } else if (name.endsWith(".jar")) {
1002 if (subfile.length() > max) {
1003 new JarSplitter(subfile, dir, max, false, 4, excludes).run();
1004 subfile.delete();
1010 private static final Pattern SKIP_FILES = Pattern.compile(
1011 "^(.*/)?((#.*#)|(.*~)|(.*/RCS/.*)|)$");
1014 * Copies files from the app to the upload staging directory, or makes
1015 * symlinks instead if supported. Puts the files into the correct places for
1016 * static vs. resource files, recursively.
1018 * @param sourceDir application war dir, or on recursion a subdirectory of it
1019 * @param resDir staging resource dir, or on recursion a subdirectory matching
1020 * the subdirectory in {@code sourceDir}
1021 * @param staticDir staging {@code __static__} dir, or an appropriate recursive
1022 * subdirectory
1023 * @param forceResource if all files should be considered resource files
1024 * @param opts processing options, used primarily for handling of *.jsp files
1025 * @throws FileNotFoundException
1026 * @throws IOException
1028 private void copyOrLink(File sourceDir, File resDir, File staticDir, boolean forceResource,
1029 ApplicationProcessingOptions opts)
1030 throws FileNotFoundException, IOException {
1032 for (String name : sourceDir.list()) {
1033 File file = new File(sourceDir, name);
1035 String path = file.getPath();
1036 if (File.separatorChar == '\\') {
1037 path = path.replace('\\', '/');
1040 if (file.getName().startsWith(".") ||
1041 file.equals(GenerationDirectory.getGenerationDirectory(baseDir))) {
1042 continue;
1045 if (file.isDirectory()) {
1046 if (file.getName().equals("WEB-INF")) {
1047 copyOrLink(file, new File(resDir, name), new File(staticDir, name), true, opts);
1048 } else {
1049 copyOrLink(file, new File(resDir, name), new File(staticDir, name), forceResource,
1050 opts);
1052 } else {
1053 if (SKIP_FILES.matcher(path).matches()) {
1054 continue;
1057 if (forceResource || appEngineWebXml.includesResource(path) ||
1058 (opts.isCompileJspsSet() && name.toLowerCase().endsWith(".jsp"))) {
1059 copyOrLinkFile(file, new File(resDir, name));
1061 if (!forceResource && appEngineWebXml.includesStatic(path)) {
1062 copyOrLinkFile(file, new File(staticDir, name));
1069 * Attempts to symlink a single file, or copies it if symlinking is either
1070 * unsupported or fails.
1072 * @param source source file
1073 * @param dest destination file
1074 * @throws FileNotFoundException
1075 * @throws IOException
1077 private void copyOrLinkFile(File source, File dest)
1078 throws FileNotFoundException, IOException {
1079 dest.getParentFile().mkdirs();
1080 if (ln != null && !source.getName().endsWith("web.xml")) {
1082 try {
1083 dest.delete();
1084 } catch (Exception e) {
1085 System.err.println("Warning: We tried to delete " + dest.getPath());
1086 System.err.println("in order to create a symlink from " + source.getPath());
1087 System.err.println("but the delete failed with message: " + e.getMessage());
1090 Process link = startProcess(ln.getAbsolutePath(), "-s",
1091 source.getAbsolutePath(),
1092 dest.getAbsolutePath());
1093 try {
1094 int stat = link.waitFor();
1095 if (stat == 0) {
1096 return;
1098 System.err.println(ln.getAbsolutePath() + " returned status " + stat
1099 + ", copying instead...");
1100 } catch (InterruptedException ex) {
1101 System.err.println(ln.getAbsolutePath() + " was interrupted, copying instead...");
1103 if (dest.delete()) {
1104 System.err.println("ln failed but symlink was created, removed: " + dest.getAbsolutePath());
1107 byte buffer[] = new byte[1024];
1108 int readlen;
1109 FileInputStream inStream = new FileInputStream(source);
1110 FileOutputStream outStream = new FileOutputStream(dest);
1111 try {
1112 readlen = inStream.read(buffer);
1113 while (readlen > 0) {
1114 outStream.write(buffer, 0, readlen);
1115 readlen = inStream.read(buffer);
1117 } finally {
1118 try {
1119 inStream.close();
1120 } catch (IOException ex) {
1122 try {
1123 outStream.close();
1124 } catch (IOException ex) {
1128 /** Copy (or link) one directory into another one.
1130 private void copyOrLinkDirectories(File sourceDir, File destination)
1131 throws IOException {
1133 for (String name : sourceDir.list()) {
1134 File file = new File(sourceDir, name);
1135 if (file.isDirectory()) {
1136 copyOrLinkDirectories(file, new File(destination, name));
1137 } else {
1138 copyOrLinkFile(file, new File(destination, name));
1143 /** deletes the staging directory, if one was created. */
1144 @Override
1145 public void cleanStagingDirectory() {
1146 if (stageDir != null) {
1147 recursiveDelete(stageDir);
1151 /** Recursive directory deletion. */
1152 public static void recursiveDelete(File dead) {
1153 String[] files = dead.list();
1154 if (files != null) {
1155 for (String name : files) {
1156 recursiveDelete(new File(dead, name));
1159 dead.delete();
1162 @Override
1163 public void setListener(UpdateListener l) {
1164 listener = l;
1167 @Override
1168 public void setDetailsWriter(PrintWriter detailsWriter) {
1169 this.detailsWriter = detailsWriter;
1172 @Override
1173 public void statusUpdate(String message, int amount) {
1174 updateProgress += progressAmount;
1175 if (updateProgress > 99) {
1176 updateProgress = 99;
1178 progressAmount = amount;
1179 if (listener != null) {
1180 listener.onProgress(new UpdateProgressEvent(
1181 Thread.currentThread(), message, updateProgress));
1185 @Override
1186 public void statusUpdate(String message) {
1187 int amount = progressAmount / 4;
1188 updateProgress += amount;
1189 if (updateProgress > 99) {
1190 updateProgress = 99;
1192 progressAmount -= amount;
1193 if (listener != null) {
1194 listener.onProgress(new UpdateProgressEvent(
1195 Thread.currentThread(), message, updateProgress));
1199 private String generateAppYaml(File stageDir, String runtime, AppEngineWebXml aeWebXml) {
1200 Set<String> staticFiles = new HashSet<String>();
1201 for (File f : new FileIterator(new File(stageDir, "__static__"))) {
1202 staticFiles.add(Utility.calculatePath(f, stageDir));
1205 AppYamlTranslator translator =
1206 new AppYamlTranslator(aeWebXml, getWebXml(), getBackendsXml(),
1207 getApiVersion(), staticFiles, null, runtime, getSdkVersion());
1208 String yaml = translator.getYaml();
1209 logger.fine("Generated app.yaml file:\n" + yaml);
1210 return yaml;
1214 * Returns the app.yaml string.
1216 * @throws IllegalStateException if createStagingDirectory has not been called.
1218 @Override
1219 public String getAppYaml() {
1220 if (appYaml == null) {
1221 throw new IllegalStateException("Must call createStagingDirectory first.");
1223 return appYaml;
1227 * Generates a quickstart-web.xml. Minimizes and saves in min-quickstart-web.xml
1228 * @return Relative path to min-quickstart-web.xml
1230 private void createQuickstartWebXml(ApplicationProcessingOptions opts)
1231 throws IOException, SAXException, ParserConfigurationException, TransformerException {
1232 String javaCmd = opts.getJavaExecutable().getPath();
1234 String quickstartJar = new File(SdkInfo.getSdkRoot(),
1235 "lib/java-managed-vm/appengine-java-vmruntime/quickstartgenerator.jar").getAbsolutePath();
1237 String[] args = {
1238 javaCmd,
1239 "-jar", quickstartJar,
1240 stageDir.getAbsolutePath()
1242 Process quickstartProcess = startProcess(args);
1244 int status;
1245 try {
1246 status = quickstartProcess.waitFor();
1247 } catch (InterruptedException ex) {
1248 status = 1;
1251 if (status != 0) {
1252 detailsWriter.println("Error while executing: " + formatCommand(Arrays.asList(args)));
1253 throw new RuntimeException("Failed to generate quickstart-web.xml.");
1256 File webDefaultXml = new File(SdkInfo.getSdkRoot() + "/lib/jetty-base-sdk/etc/webdefault.xml");
1257 File quickstartXml = new File(stageDir, "/WEB-INF/quickstart-web.xml");
1258 File minimizedQuickstartXml = new File(stageDir, "/WEB-INF/min-quickstart-web.xml");
1260 Document quickstartDoc = getFilteredQuickstartDoc(quickstartXml, webDefaultXml);
1262 Transformer transformer = TransformerFactory.newInstance().newTransformer();
1263 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1264 StreamResult result = new StreamResult(new FileWriter(minimizedQuickstartXml));
1265 DOMSource source = new DOMSource(quickstartDoc);
1266 transformer.transform(source, result);
1270 * Removes mappings from quickstart-web.xml that come from webdefault.xml.
1272 * The quickstart-web.xml generated by the quickstartgenerator process includes
1273 * the contents of the user's web.xml, entries derived from Java annotations,
1274 * and entries derived from the contents of webdefault.xml. All of those are
1275 * appropriate for the Java web server. But when generating an app.yaml for
1276 * appcfg or dev_appserver, the webdefault.xml entries are not appropriate, since
1277 * app.yaml should only reflect what is specific to the user's app. So this
1278 * method returns a modified min-quickstart-web Document from which the webdefault.xml
1279 * entries have been removed. Specifically, we look at the <url-pattern> inside
1280 * every <servlet-mapping> or <filter-mapping> element in webdefault.xml to
1281 * determine default patterns; then we look at those elements inside
1282 * quickstart-web.xml and remove any whose <url-pattern> is one of the default
1283 * patterns.
1285 * @return a filtered quickstart Document object appropriate for translation to app.yaml
1287 static Document getFilteredQuickstartDoc(File quickstartXml, File webDefaultXml)
1288 throws ParserConfigurationException, IOException, SAXException {
1290 DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
1291 DocumentBuilder webDefaultDocBuilder = docBuilderFactory.newDocumentBuilder();
1292 Document webDefaultDoc = webDefaultDocBuilder.parse(webDefaultXml);
1293 DocumentBuilder quickstartDocBuilder = docBuilderFactory.newDocumentBuilder();
1294 Document quickstartDoc = quickstartDocBuilder.parse(quickstartXml);
1295 final Set<String> tagsToExamine = ImmutableSet.of("filter-mapping", "servlet-mapping");
1296 final String urlPatternTag = "url-pattern";
1298 Set<String> defaultRoots = Sets.newHashSet();
1299 List<Node> nodesToRemove = Lists.newArrayList();
1301 webDefaultDoc.getDocumentElement().normalize();
1302 NodeList webDefaultChildren = webDefaultDoc.getDocumentElement()
1303 .getElementsByTagName(urlPatternTag);
1304 for (int i = 0; i < webDefaultChildren.getLength(); i++) {
1305 Node child = webDefaultChildren.item(i);
1306 if (tagsToExamine.contains(child.getParentNode().getNodeName())) {
1307 String url = child.getTextContent().trim();
1308 if (url.startsWith("/")) {
1309 defaultRoots.add(url);
1314 quickstartDoc.getDocumentElement().normalize();
1315 NodeList quickstartChildren = quickstartDoc.getDocumentElement()
1316 .getElementsByTagName(urlPatternTag);
1317 for (int i = 0; i < quickstartChildren.getLength(); i++) {
1318 Node child = quickstartChildren.item(i);
1319 if (tagsToExamine.contains(child.getParentNode().getNodeName())) {
1320 String url = child.getTextContent().trim();
1321 if (defaultRoots.contains(url)) {
1322 nodesToRemove.add(child.getParentNode());
1326 for (Node node : nodesToRemove) {
1327 quickstartDoc.getDocumentElement().removeChild(node);
1330 return quickstartDoc;