Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / AppYamlTranslator.java
blob59a7addbf16810dfa12697547a48da98a7b73620
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.admin;
5 import com.google.apphosting.utils.config.AppEngineConfigException;
6 import com.google.apphosting.utils.config.AppEngineWebXml;
7 import com.google.apphosting.utils.config.AppEngineWebXml.AdminConsolePage;
8 import com.google.apphosting.utils.config.AppEngineWebXml.ApiConfig;
9 import com.google.apphosting.utils.config.AppEngineWebXml.ErrorHandler;
10 import com.google.apphosting.utils.config.AppEngineWebXml.Pagespeed;
11 import com.google.apphosting.utils.config.BackendsXml;
12 import com.google.apphosting.utils.config.WebXml;
13 import com.google.apphosting.utils.config.WebXml.SecurityConstraint;
14 import com.google.apphosting.utils.glob.ConflictResolver;
15 import com.google.apphosting.utils.glob.Glob;
16 import com.google.apphosting.utils.glob.GlobFactory;
17 import com.google.apphosting.utils.glob.GlobIntersector;
18 import com.google.apphosting.utils.glob.LongestPatternConflictResolver;
19 import com.google.common.collect.Maps;
21 import net.sourceforge.yamlbeans.YamlConfig;
22 import net.sourceforge.yamlbeans.YamlException;
23 import net.sourceforge.yamlbeans.YamlWriter;
25 import java.io.StringWriter;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
34 /**
35 * Generates {@code app.yaml} files suitable for uploading as part of
36 * a Google App Engine application.
39 public class AppYamlTranslator {
40 private static final String NO_API_VERSION = "none";
42 private static final ConflictResolver RESOLVER =
43 new LongestPatternConflictResolver();
45 private static final String DYNAMIC_PROPERTY = "dynamic";
46 private static final String STATIC_PROPERTY = "static";
47 private static final String WELCOME_FILES = "welcome";
48 private static final String TRANSPORT_GUARANTEE_PROPERTY = "transportGuarantee";
49 private static final String REQUIRED_ROLE_PROPERTY = "requiredRole";
50 private static final String EXPIRATION_PROPERTY = "expiration";
51 private static final String HTTP_HEADERS_PROPERTY = "http_headers";
52 private static final String API_ENDPOINT_REGEX = "/_ah/spi/*";
54 private static final String[] PROPERTIES = new String[] {
55 DYNAMIC_PROPERTY,
56 STATIC_PROPERTY,
57 WELCOME_FILES,
58 TRANSPORT_GUARANTEE_PROPERTY,
59 REQUIRED_ROLE_PROPERTY,
60 EXPIRATION_PROPERTY,
63 private static final int MAX_HANDLERS = 100;
65 private final AppEngineWebXml appEngineWebXml;
66 private final WebXml webXml;
67 private final BackendsXml backendsXml;
68 private final String apiVersion;
69 private final Set<String> staticFiles;
70 private final ApiConfig apiConfig;
72 public AppYamlTranslator(AppEngineWebXml appEngineWebXml,
73 WebXml webXml,
74 BackendsXml backendsXml,
75 String apiVersion,
76 Set<String> staticFiles,
77 ApiConfig apiConfig) {
78 this.appEngineWebXml = appEngineWebXml;
79 this.webXml = webXml;
80 this.backendsXml = backendsXml;
81 this.apiVersion = apiVersion;
82 this.staticFiles = staticFiles;
83 this.apiConfig = apiConfig;
86 public String getYaml() {
87 StringBuilder builder = new StringBuilder();
88 translateAppEngineWebXml(builder);
89 translateApiVersion(builder);
90 translateWebXml(builder);
91 return builder.toString();
94 private void appendIfNotNull(StringBuilder builder, String tag, Object value) {
95 if (value != null) {
96 builder.append(tag);
97 builder.append(value);
98 builder.append("\n");
103 * Returns the String that is used to identify the App Engine runtime
104 * on which the uploaded app should run. Normally this String is
105 * "java". But during periods of transition we may need to support
106 * multiple runtime labels.
107 * <p>
108 * Currently we are in transition from Java 6 to Java 7 and
109 * we have two runtimes: "java" and "java7". This method
110 * will return "java" unless the System Property
111 * "com.google.apphosting.runtime.use_java7" is "true".
113 private String getRuntimeLabel() {
114 return (Boolean.getBoolean(AppCfg.USE_JAVA7_SYSTEM_PROP) ? "java7" : "java");
117 private void translateAppEngineWebXml(StringBuilder builder) {
118 builder.append("application: '" + appEngineWebXml.getAppId() + "'\n");
119 builder.append("runtime: " + getRuntimeLabel() + "\n");
120 if (appEngineWebXml.getSourceLanguage() != null) {
121 builder.append("source_language: '" + appEngineWebXml.getSourceLanguage() + "'\n");
124 if (appEngineWebXml.getMajorVersionId() != null) {
125 builder.append("version: '" + appEngineWebXml.getMajorVersionId() + "'\n");
128 if (appEngineWebXml.getServer() != null) {
129 builder.append("server: '" + appEngineWebXml.getServer() + "'\n");
132 if (!appEngineWebXml.getServerSettings().isEmpty()) {
133 builder.append("server_settings:\n");
134 AppEngineWebXml.ServerSettings settings = appEngineWebXml.getServerSettings();
135 appendIfNotNull(builder, " instances: ", settings.getInstances());
136 appendIfNotNull(builder, " class: ", settings.getInstanceClass());
137 appendIfNotNull(builder, " min_pending_latency: ", settings.getMinPendingLatency());
138 appendIfNotNull(builder, " max_pending_latency: ", settings.getMaxPendingLatency());
139 appendIfNotNull(builder, " min_idle_instances: ", settings.getMinIdleInstances());
140 appendIfNotNull(builder, " max_idle_instances: ", settings.getMaxIdleInstances());
143 Collection<String> services = appEngineWebXml.getInboundServices();
144 if (!services.isEmpty()) {
145 builder.append("inbound_services:\n");
146 for (String service : services) {
147 builder.append("- " + service + "\n");
151 if (appEngineWebXml.getPrecompilationEnabled()) {
152 builder.append("derived_file_type:\n");
153 builder.append("- java_precompiled\n");
156 if (appEngineWebXml.getThreadsafe()) {
157 builder.append("threadsafe: True\n");
160 if (appEngineWebXml.getCodeLock()) {
161 builder.append("code_lock: True\n");
164 List<AdminConsolePage> adminConsolePages = appEngineWebXml.getAdminConsolePages();
165 if (!adminConsolePages.isEmpty()) {
166 builder.append("admin_console:\n");
167 builder.append(" pages:\n");
168 for (AdminConsolePage page : adminConsolePages) {
169 builder.append(" - name: " + page.getName() + "\n");
170 builder.append(" url: " + page.getUrl() + "\n");
174 List<ErrorHandler> errorHandlers = appEngineWebXml.getErrorHandlers();
175 if (!errorHandlers.isEmpty()) {
176 builder.append("error_handlers:\n");
177 for (ErrorHandler handler : errorHandlers) {
178 String fileName = handler.getFile();
179 if (!fileName.startsWith("/")) {
180 fileName = "/" + fileName;
182 if (!staticFiles.contains("__static__" + fileName)) {
183 throw new AppEngineConfigException("No static file found for error handler: " +
184 fileName + ", out of " + staticFiles);
186 builder.append("- file: __static__" + fileName + "\n");
187 if (handler.getErrorCode() != null) {
188 builder.append(" error_code: " + handler.getErrorCode() + "\n");
190 String mimeType = webXml.getMimeTypeForPath(handler.getFile());
191 if (mimeType != null) {
192 builder.append(" mime_type: " + mimeType + "\n");
197 if (backendsXml != null) {
198 builder.append(backendsXml.toYaml());
201 ApiConfig apiConfig = appEngineWebXml.getApiConfig();
202 if (apiConfig != null) {
203 builder.append("api_config:\n");
204 builder.append(" url: " + apiConfig.getUrl() + "\n");
205 builder.append(" script: unused\n");
208 if (appEngineWebXml.getPagespeed() != null) {
209 builder.append("pagespeed:\n");
210 appendPagespeed(appEngineWebXml.getPagespeed(), builder, 2);
215 * Append the given Pagespeed node as YAML to the given StringBuilder.
216 * @param pagespeed The Pagespeed instance to append as YAML.
217 * @param builder The StringBuilder to append to.
218 * @param indent The number of spaces to indent the pagespeed YAML.
220 public static void appendPagespeed(Pagespeed pagespeed, StringBuilder builder, int indent) {
221 if (pagespeed != null && !pagespeed.isEmpty()) {
222 Map<String, List<String>> config = Maps.newTreeMap();
223 putListInMapIfNotEmpty(config, "url_blacklist", pagespeed.getUrlBlacklist());
224 putListInMapIfNotEmpty(config, "domains_to_rewrite", pagespeed.getDomainsToRewrite());
225 putListInMapIfNotEmpty(config, "enabled_rewriters", pagespeed.getEnabledRewriters());
226 putListInMapIfNotEmpty(config, "disabled_rewriters", pagespeed.getDisabledRewriters());
227 appendObjectAsYaml(builder, config, indent);
232 * Adds the list to the given map, using the specified name, if the list is non-null and not
233 * empty.
235 private static void putListInMapIfNotEmpty(Map<String, List<String>> map, String name,
236 List<String> values) {
237 if (values != null && !values.isEmpty()) {
238 map.put(name, values);
243 * Appends the given collection to the StringBuilder as YAML, indenting each emitted line by
244 * numIndentSpaces.
246 private static void appendObjectAsYaml(
247 StringBuilder builder, Object collection, int numIndentSpaces) {
248 StringBuilder prefixBuilder = new StringBuilder();
249 for (int i = 0; i < numIndentSpaces; ++i) {
250 prefixBuilder.append(' ');
252 final String indentPrefix = prefixBuilder.toString();
254 StringWriter stringWriter = new StringWriter();
255 YamlConfig yamlConfig = new YamlConfig();
256 yamlConfig.writeConfig.setIndentSize(2);
257 yamlConfig.writeConfig.setWriteRootTags(false);
259 YamlWriter writer = new YamlWriter(stringWriter, yamlConfig);
260 try {
261 writer.write(collection);
262 writer.close();
263 } catch (YamlException e) {
264 throw new AppEngineConfigException("Unable to generate YAML.", e);
267 for (String line : stringWriter.toString().split("\n")) {
268 builder.append(indentPrefix);
269 builder.append(line);
270 builder.append("\n");
274 private void translateApiVersion(StringBuilder builder) {
275 if (apiVersion == null) {
276 builder.append("api_version: '" + NO_API_VERSION + "'\n");
277 } else {
278 builder.append("api_version: '" + apiVersion + "'\n");
282 private void translateWebXml(StringBuilder builder) {
283 builder.append("handlers:\n");
285 AbstractHandlerGenerator staticGenerator = null;
286 if (staticFiles.isEmpty()) {
287 staticGenerator = new EmptyHandlerGenerator();
288 } else {
289 staticGenerator = new StaticHandlerGenerator(appEngineWebXml.getPublicRoot());
292 DynamicHandlerGenerator dynamicGenerator =
293 new DynamicHandlerGenerator(webXml.getFallThroughToRuntime());
294 if (staticGenerator.size() + dynamicGenerator.size() > MAX_HANDLERS) {
295 dynamicGenerator = new DynamicHandlerGenerator(true);
298 staticGenerator.translate(builder);
299 dynamicGenerator.translate(builder);
302 class StaticHandlerGenerator extends AbstractHandlerGenerator {
303 private final String root;
304 private final String rootRegex;
306 public StaticHandlerGenerator(String root) {
307 this.root = root;
308 this.rootRegex = root.replaceAll("([^A-Za-z0-9\\-_/])", "\\\\$1");
311 @Override
312 protected Map<String, Object> getWelcomeProperties() {
313 List<String> staticWelcomeFiles = new ArrayList<String>();
314 for (String welcomeFile : webXml.getWelcomeFiles()) {
315 for (String staticFile : staticFiles) {
316 if (staticFile.endsWith("/" + welcomeFile)) {
317 staticWelcomeFiles.add(welcomeFile);
318 break;
322 return Collections.<String,Object>singletonMap(WELCOME_FILES, staticWelcomeFiles);
325 @Override
326 protected void addPatterns(GlobIntersector intersector) {
327 List<AppEngineWebXml.StaticFileInclude> includes = appEngineWebXml.getStaticFileIncludes();
328 if (includes.isEmpty()) {
329 intersector.addGlob(GlobFactory.createGlob("/*", STATIC_PROPERTY, true));
330 } else {
331 for (AppEngineWebXml.StaticFileInclude include : includes) {
332 String pattern = include.getPattern().replaceAll("\\*\\*", "*");
333 if (!pattern.startsWith("/")) {
334 pattern = "/" + pattern;
336 Map<String, Object> props = new HashMap<String, Object>();
337 props.put(STATIC_PROPERTY, true);
338 if (include.getExpiration() != null) {
339 props.put(EXPIRATION_PROPERTY, include.getExpiration());
341 if (include.getHttpHeaders() != null) {
342 props.put(HTTP_HEADERS_PROPERTY, include.getHttpHeaders());
345 intersector.addGlob(GlobFactory.createGlob(pattern, props));
350 @Override
351 public void translateGlob(StringBuilder builder, Glob glob) {
352 String regex = glob.getRegularExpression().pattern();
353 if (!root.equals("")) {
354 if (regex.startsWith(root)){
355 regex = regex.substring(root.length(), regex.length());
358 @SuppressWarnings("unchecked")
359 List<String> welcomeFiles =
360 (List<String>) glob.getProperty(WELCOME_FILES, RESOLVER);
361 if (welcomeFiles != null) {
362 for (String welcomeFile : welcomeFiles) {
363 builder.append("- url: (" + regex + ")\n");
364 builder.append(" static_files: __static__" + root + "\\1" + welcomeFile + "\n");
365 builder.append(" upload: __NOT_USED__\n");
366 builder.append(" require_matching_file: True\n");
367 translateHandlerOptions(builder, glob);
368 translateAdditionalStaticOptions(builder, glob);
370 } else {
371 Boolean isStatic = (Boolean) glob.getProperty(STATIC_PROPERTY, RESOLVER);
372 if (isStatic != null && isStatic.booleanValue()) {
373 builder.append("- url: (" + regex + ")\n");
374 builder.append(" static_files: __static__" + root + "\\1\n");
375 builder.append(" upload: __NOT_USED__\n");
376 builder.append(" require_matching_file: True\n");
377 translateHandlerOptions(builder, glob);
378 translateAdditionalStaticOptions(builder, glob);
383 private void translateAdditionalStaticOptions(StringBuilder builder, Glob glob)
384 throws AppEngineConfigException {
385 String expiration = (String) glob.getProperty(EXPIRATION_PROPERTY, RESOLVER);
386 if (expiration != null) {
387 builder.append(" expiration: " + expiration + "\n");
390 @SuppressWarnings("unchecked")
391 Map<String, String> httpHeaders =
392 (Map<String, String>) glob.getProperty(HTTP_HEADERS_PROPERTY, RESOLVER);
393 if (httpHeaders != null && !httpHeaders.isEmpty()) {
394 builder.append(" http_headers:\n");
395 appendObjectAsYaml(builder, httpHeaders, 4);
400 class DynamicHandlerGenerator extends AbstractHandlerGenerator {
401 private final List<String> patterns;
402 private boolean fallthrough;
403 private boolean hasJsps;
405 DynamicHandlerGenerator(boolean alwaysFallthrough) {
406 fallthrough = alwaysFallthrough;
407 patterns = new ArrayList<String>();
408 for (String servletPattern : webXml.getServletPatterns()) {
409 if (servletPattern.equals("/") || servletPattern.equals("/*")) {
410 fallthrough = true;
411 } else if (servletPattern.equals(API_ENDPOINT_REGEX)) {
412 hasApiEndpoint = true;
413 } else if (servletPattern.endsWith(".jsp")) {
414 hasJsps = true;
415 } else {
416 patterns.add(servletPattern);
421 @Override
422 protected Map<String, Object> getWelcomeProperties() {
423 if (fallthrough) {
424 return null;
425 } else {
426 return Collections.<String,Object>singletonMap(DYNAMIC_PROPERTY, true);
430 @Override
431 protected void addPatterns(GlobIntersector intersector) {
432 if (fallthrough) {
433 intersector.addGlob(GlobFactory.createGlob(
434 "/*",
435 DYNAMIC_PROPERTY, true));
436 } else {
437 for (String servletPattern : patterns) {
438 intersector.addGlob(GlobFactory.createGlob(
439 servletPattern,
440 DYNAMIC_PROPERTY, true));
441 if (servletPattern.endsWith("/*")) {
442 intersector.addGlob(GlobFactory.createGlob(
443 servletPattern.substring(0, servletPattern.length() - 2),
444 DYNAMIC_PROPERTY, true));
447 if (hasJsps) {
448 intersector.addGlob(GlobFactory.createGlob(
449 "*.jsp",
450 DYNAMIC_PROPERTY, true));
452 intersector.addGlob(GlobFactory.createGlob(
453 "/_ah/*",
454 DYNAMIC_PROPERTY, true));
458 @Override
459 public void translateGlob(StringBuilder builder, Glob glob) {
460 String regex = glob.getRegularExpression().pattern();
462 Boolean isDynamic = (Boolean) glob.getProperty(DYNAMIC_PROPERTY, RESOLVER);
463 if (isDynamic != null && isDynamic.booleanValue()) {
464 builder.append("- url: " + regex + "\n");
465 builder.append(" script: unused\n");
466 translateHandlerOptions(builder, glob);
472 * An {@code AbstractHandlerGenerator} that returns no globs.
474 class EmptyHandlerGenerator extends AbstractHandlerGenerator {
475 @Override
476 protected void addPatterns(GlobIntersector intersector) {
479 @Override
480 protected void translateGlob(StringBuilder builder, Glob glob) {
483 @Override
484 protected Map<String, Object> getWelcomeProperties() {
485 return Collections.emptyMap();
489 abstract class AbstractHandlerGenerator {
490 private List<Glob> globs = null;
491 protected boolean hasApiEndpoint;
493 public int size() {
494 return getGlobPatterns().size();
497 public void translate(StringBuilder builder) {
498 for (Glob glob : getGlobPatterns()) {
499 translateGlob(builder, glob);
503 abstract protected void addPatterns(GlobIntersector intersector);
504 abstract protected void translateGlob(StringBuilder builder, Glob glob);
507 * @returns a map of welcome properties to apply to the welcome
508 * file entries, or {@code null} if no welcome file entries are
509 * necessary.
511 abstract protected Map<String, Object> getWelcomeProperties();
513 protected List<Glob> getGlobPatterns() {
514 if (globs == null) {
515 GlobIntersector intersector = new GlobIntersector();
516 addPatterns(intersector);
517 addSecurityConstraints(intersector);
518 addWelcomeFiles(intersector);
520 globs = intersector.getIntersection();
521 removeNearDuplicates(globs);
522 if (hasApiEndpoint) {
523 globs.add(GlobFactory.createGlob(API_ENDPOINT_REGEX, DYNAMIC_PROPERTY, true));
526 return globs;
529 protected void addWelcomeFiles(GlobIntersector intersector) {
530 Map<String, Object> welcomeProperties = getWelcomeProperties();
531 if (welcomeProperties != null) {
532 intersector.addGlob(GlobFactory.createGlob("/", welcomeProperties));
533 intersector.addGlob(GlobFactory.createGlob("/*/", welcomeProperties));
537 protected void addSecurityConstraints(GlobIntersector intersector) {
538 for (SecurityConstraint constraint : webXml.getSecurityConstraints()) {
539 for (String pattern : constraint.getUrlPatterns()) {
540 intersector.addGlob(GlobFactory.createGlob(
541 pattern,
542 TRANSPORT_GUARANTEE_PROPERTY,
543 constraint.getTransportGuarantee()));
544 intersector.addGlob(GlobFactory.createGlob(
545 pattern,
546 REQUIRED_ROLE_PROPERTY,
547 constraint.getRequiredRole()));
552 protected void translateHandlerOptions(StringBuilder builder, Glob glob) {
553 SecurityConstraint.RequiredRole requiredRole =
554 (SecurityConstraint.RequiredRole) glob.getProperty(REQUIRED_ROLE_PROPERTY, RESOLVER);
555 if (requiredRole == null) {
556 requiredRole = SecurityConstraint.RequiredRole.NONE;
558 switch (requiredRole) {
559 case NONE:
560 builder.append(" login: optional\n");
561 break;
562 case ANY_USER:
563 builder.append(" login: required\n");
564 break;
565 case ADMIN:
566 builder.append(" login: admin\n");
567 break;
570 SecurityConstraint.TransportGuarantee transportGuarantee =
571 (SecurityConstraint.TransportGuarantee) glob.getProperty(TRANSPORT_GUARANTEE_PROPERTY,
572 RESOLVER);
573 if (transportGuarantee == null) {
574 transportGuarantee = SecurityConstraint.TransportGuarantee.NONE;
576 switch (transportGuarantee) {
577 case NONE:
578 if (appEngineWebXml.getSslEnabled()) {
579 builder.append(" secure: optional\n");
580 } else {
581 builder.append(" secure: never\n");
583 break;
584 case INTEGRAL:
585 case CONFIDENTIAL:
586 if (!appEngineWebXml.getSslEnabled()) {
587 throw new AppEngineConfigException(
588 "SSL must be enabled in appengine-web.xml to use transport-guarantee");
590 builder.append(" secure: always\n");
591 break;
594 String pattern = glob.getRegularExpression().pattern();
595 String id = webXml.getHandlerIdForPattern(pattern);
596 if (id != null) {
597 if (appEngineWebXml.isApiEndpoint(id)) {
598 builder.append(" api_endpoint: True\n");
603 private void removeNearDuplicates(List<Glob> globs) {
604 for (int i = 0; i < globs.size(); i++) {
605 Glob topGlob = globs.get(i);
606 for (int j = i + 1; j < globs.size(); j++) {
607 Glob bottomGlob = globs.get(j);
608 if (bottomGlob.matchesAll(topGlob)) {
609 if (propertiesMatch(topGlob, bottomGlob)) {
610 globs.remove(i);
611 i--;
613 break;
619 private boolean propertiesMatch(Glob glob1, Glob glob2) {
620 for (String property : PROPERTIES) {
621 Object value1 = glob1.getProperty(property, RESOLVER);
622 Object value2 = glob2.getProperty(property, RESOLVER);
623 if (value1 != value2 && (value1 == null || !value1.equals(value2))) {
624 return false;
627 return true;