Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.google.appengine.tools.admin.AppYamlTranslator Maven / Gradle / Ivy
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.appengine.tools.admin;
import com.esotericsoftware.yamlbeans.YamlConfig;
import com.esotericsoftware.yamlbeans.YamlException;
import com.esotericsoftware.yamlbeans.YamlWriter;
import com.google.apphosting.utils.config.AppEngineConfigException;
import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.apphosting.utils.config.AppEngineWebXml.AdminConsolePage;
import com.google.apphosting.utils.config.AppEngineWebXml.ApiConfig;
import com.google.apphosting.utils.config.AppEngineWebXml.CpuUtilization;
import com.google.apphosting.utils.config.AppEngineWebXml.CustomMetricUtilization;
import com.google.apphosting.utils.config.AppEngineWebXml.ErrorHandler;
import com.google.apphosting.utils.config.AppEngineWebXml.HealthCheck;
import com.google.apphosting.utils.config.AppEngineWebXml.LivenessCheck;
import com.google.apphosting.utils.config.AppEngineWebXml.Network;
import com.google.apphosting.utils.config.AppEngineWebXml.ReadinessCheck;
import com.google.apphosting.utils.config.AppEngineWebXml.Resources;
import com.google.apphosting.utils.config.AppEngineWebXml.VpcAccessConnector;
import com.google.apphosting.utils.config.BackendsXml;
import com.google.apphosting.utils.config.WebXml;
import com.google.apphosting.utils.config.WebXml.SecurityConstraint;
import com.google.apphosting.utils.glob.ConflictResolver;
import com.google.apphosting.utils.glob.Glob;
import com.google.apphosting.utils.glob.GlobFactory;
import com.google.apphosting.utils.glob.GlobIntersector;
import com.google.apphosting.utils.glob.LongestPatternConflictResolver;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Generates {@code app.yaml} files suitable for uploading as part of
* a Google App Engine application.
*
*/
public class AppYamlTranslator {
private static final String NO_API_VERSION = "none";
private static final ConflictResolver RESOLVER =
new LongestPatternConflictResolver();
private static final String DYNAMIC_PROPERTY = "dynamic";
private static final String STATIC_PROPERTY = "static";
private static final String WELCOME_FILES = "welcome";
private static final String TRANSPORT_GUARANTEE_PROPERTY = "transportGuarantee";
private static final String REQUIRED_ROLE_PROPERTY = "requiredRole";
private static final String EXPIRATION_PROPERTY = "expiration";
private static final String HTTP_HEADERS_PROPERTY = "http_headers";
private static final String API_ENDPOINT_REGEX = "/_ah/spi/*";
private static final String[] PROPERTIES = new String[] {
DYNAMIC_PROPERTY,
STATIC_PROPERTY,
WELCOME_FILES,
TRANSPORT_GUARANTEE_PROPERTY,
REQUIRED_ROLE_PROPERTY,
EXPIRATION_PROPERTY,
};
// This should be kept in sync with MAX_URL_MAPS in .
private static final int MAX_HANDLERS = 100;
private final AppEngineWebXml appEngineWebXml;
private final WebXml webXml;
private final BackendsXml backendsXml;
private final String apiVersion;
private final Set staticFiles;
private final String runtime;
public AppYamlTranslator(
AppEngineWebXml appEngineWebXml,
WebXml webXml,
BackendsXml backendsXml,
String apiVersion,
Set staticFiles,
ApiConfig apiConfig,
String runtime) {
this.appEngineWebXml = appEngineWebXml;
this.webXml = webXml;
this.backendsXml = backendsXml;
this.apiVersion = apiVersion;
this.staticFiles = staticFiles;
// apiConfig not used.
if (runtime != null) {
this.runtime = runtime;
} else {
this.runtime = appEngineWebXml.getRuntime();
}
if (appEngineWebXml.getUseVm() && appEngineWebXml.isFlexible()) {
throw new AppEngineConfigException("Cannot define both and entries.");
}
}
public String getYaml() {
StringBuilder builder = new StringBuilder();
translateAppEngineWebXml(builder);
// No need to process the api_version field for Java11 or above.
if (!appEngineWebXml.isJava11OrAbove()) {
translateApiVersion(builder);
}
translateWebXml(builder);
return builder.toString();
}
private void appendIfNotNull(StringBuilder builder, String tag, Object value) {
if (value != null) {
builder.append(tag);
builder.append(value);
builder.append("\n");
}
}
private void appendIfNotZero(StringBuilder builder, String tag, double value) {
if (value != 0) {
builder.append(tag);
builder.append(value);
builder.append("\n");
}
}
private void translateAppEngineWebXml(StringBuilder builder) {
if (appEngineWebXml.getAppId() != null) {
builder.append("application: '" + appEngineWebXml.getAppId() + "'\n");
}
builder.append("runtime: " + runtime + "\n");
if (appEngineWebXml.getUseVm()) {
builder.append("vm: True\n");
}
if (appEngineWebXml.isFlexible()) {
builder.append("env: " + appEngineWebXml.getEnv() + "\n");
}
if (appEngineWebXml.getEntrypoint() != null) {
builder.append("entrypoint: '" + appEngineWebXml.getEntrypoint() + "'\n");
}
if (appEngineWebXml.getRuntimeChannel() != null) {
builder.append("runtime_channel: " + appEngineWebXml.getRuntimeChannel() + "\n");
}
if (appEngineWebXml.getMajorVersionId() != null) {
builder.append("version: '" + appEngineWebXml.getMajorVersionId() + "'\n");
}
if (appEngineWebXml.getService() != null) {
builder.append("service: '" + appEngineWebXml.getService() + "'\n");
} else if (appEngineWebXml.getModule() != null) {
builder.append("module: '" + appEngineWebXml.getModule() + "'\n");
}
if (appEngineWebXml.getInstanceClass() != null) {
builder.append("instance_class: " + appEngineWebXml.getInstanceClass() + "\n");
}
if (!appEngineWebXml.getAutomaticScaling().isEmpty()) {
builder.append("automatic_scaling:\n");
AppEngineWebXml.AutomaticScaling settings = appEngineWebXml.getAutomaticScaling();
appendIfNotNull(builder, " min_pending_latency: ", settings.getMinPendingLatency());
appendIfNotNull(builder, " max_pending_latency: ", settings.getMaxPendingLatency());
appendIfNotNull(builder, " min_idle_instances: ", settings.getMinIdleInstances());
appendIfNotNull(builder, " max_idle_instances: ", settings.getMaxIdleInstances());
appendIfNotNull(builder, " max_concurrent_requests: ", settings.getMaxConcurrentRequests());
appendIfNotNull(builder, " min_num_instances: ", settings.getMinNumInstances());
appendIfNotNull(builder, " max_num_instances: ", settings.getMaxNumInstances());
appendIfNotNull(builder, " cool_down_period_sec: ", settings.getCoolDownPeriodSec());
// GAE Standard new clone scheduler:
// I know, having min_num_instances and min_instances is confusing. b/70626925.
appendIfNotNull(builder, " min_instances: ", settings.getMinInstances());
appendIfNotNull(builder, " max_instances: ", settings.getMaxInstances());
appendIfNotNull(builder, " target_cpu_utilization: ", settings.getTargetCpuUtilization());
appendIfNotNull(
builder, " target_throughput_utilization: ", settings.getTargetThroughputUtilization());
CpuUtilization cpuUtil = settings.getCpuUtilization();
if (cpuUtil != null
&& (cpuUtil.getTargetUtilization() != null
|| cpuUtil.getAggregationWindowLengthSec() != null)) {
builder.append(" cpu_utilization:\n");
appendIfNotNull(builder, " target_utilization: ", cpuUtil.getTargetUtilization());
appendIfNotNull(
builder, " aggregation_window_length_sec: ",
cpuUtil.getAggregationWindowLengthSec());
}
appendIfNotNull(
builder, " target_network_sent_bytes_per_sec: ",
settings.getTargetNetworkSentBytesPerSec());
appendIfNotNull(
builder, " target_network_sent_packets_per_sec: ",
settings.getTargetNetworkSentPacketsPerSec());
appendIfNotNull(
builder, " target_network_received_bytes_per_sec: ",
settings.getTargetNetworkReceivedBytesPerSec());
appendIfNotNull(
builder, " target_network_received_packets_per_sec: ",
settings.getTargetNetworkReceivedPacketsPerSec());
appendIfNotNull(
builder, " target_disk_write_bytes_per_sec: ",
settings.getTargetDiskWriteBytesPerSec());
appendIfNotNull(
builder, " target_disk_write_ops_per_sec: ",
settings.getTargetDiskWriteOpsPerSec());
appendIfNotNull(
builder, " target_disk_read_bytes_per_sec: ",
settings.getTargetDiskReadBytesPerSec());
appendIfNotNull(
builder, " target_disk_read_ops_per_sec: ",
settings.getTargetDiskReadOpsPerSec());
appendIfNotNull(
builder, " target_request_count_per_sec: ",
settings.getTargetRequestCountPerSec());
appendIfNotNull(
builder, " target_concurrent_requests: ",
settings.getTargetConcurrentRequests());
if (!settings.getCustomMetrics().isEmpty()) {
if (!appEngineWebXml.isFlexible()) {
throw new AppEngineConfigException("custom-metrics is only available in the AppEngine "
+ "Flexible environment.");
}
builder.append(" custom_metrics:\n");
for (CustomMetricUtilization metric : settings.getCustomMetrics()) {
builder.append(" - metric_name: '" + metric.getMetricName() + "'\n");
builder.append(" target_type: '" + metric.getTargetType() + "'\n");
appendIfNotNull(builder, " target_utilization: ", metric.getTargetUtilization());
appendIfNotNull(builder,
" single_instance_assignment: ",
metric.getSingleInstanceAssignment());
if (metric.getFilter() != null) {
builder.append(" filter: '" + metric.getFilter() + "'\n");
}
}
}
}
if (!appEngineWebXml.getManualScaling().isEmpty()) {
builder.append("manual_scaling:\n");
AppEngineWebXml.ManualScaling settings = appEngineWebXml.getManualScaling();
builder.append(" instances: " + settings.getInstances() + "\n");
}
if (!appEngineWebXml.getBasicScaling().isEmpty()) {
builder.append("basic_scaling:\n");
AppEngineWebXml.BasicScaling settings = appEngineWebXml.getBasicScaling();
builder.append(" max_instances: " + settings.getMaxInstances() + "\n");
appendIfNotNull(builder, " idle_timeout: ", settings.getIdleTimeout());
}
Collection services = appEngineWebXml.getInboundServices();
if (!services.isEmpty()) {
builder.append("inbound_services:\n");
for (String service : services) {
builder.append("- " + service + "\n");
}
}
// Precompilation is only for the Standard environment.
if (appEngineWebXml.getPrecompilationEnabled()
&& !appEngineWebXml.getUseVm()
&& !appEngineWebXml.isFlexible()
&& !appEngineWebXml.isJava11OrAbove()) {
builder.append("derived_file_type:\n");
builder.append("- java_precompiled\n");
}
if (appEngineWebXml.getThreadsafe() && !appEngineWebXml.isJava11OrAbove()) {
builder.append("threadsafe: True\n");
}
if (appEngineWebXml.getAppEngineApis() && appEngineWebXml.isJava11OrAbove()) {
builder.append("app_engine_apis: True\n");
}
if (appEngineWebXml.getThreadsafeValueProvided() && appEngineWebXml.isJava11OrAbove()) {
System.out.println(
"Warning: The "
+ appEngineWebXml.getRuntime()
+ " runtime does not use the element"
+ " in appengine-web.xml anymore");
System.out.println(
"Instead, you can use the element in .");
}
if (appEngineWebXml.getAutoIdPolicy() != null) {
builder.append("auto_id_policy: " + appEngineWebXml.getAutoIdPolicy() + "\n");
} else {
// NOTE: The YAML parsing and validation done in the admin console must
// set the value for unspecified auto_id_policy to 'legacy' in order to achieve
// the desired behavior for previous SDK versions. But the desired value for
// unspecified auto_id_policy in current and future SDK versions is 'default'.
// Therefore in new SDK versions we intercept unspecified auto_id_policy here.
builder.append("auto_id_policy: default\n");
}
if (appEngineWebXml.getCodeLock()) {
builder.append("code_lock: True\n");
}
if (appEngineWebXml.getVpcAccessConnector() != null) {
VpcAccessConnector connector = appEngineWebXml.getVpcAccessConnector();
builder.append("vpc_access_connector:\n");
builder.append(" name: " + connector.getName() + "\n");
if (connector.getEgressSetting().isPresent()) {
builder.append(" egress_setting: " + connector.getEgressSetting().get() + "\n");
}
}
if (appEngineWebXml.getServiceAccount() != null) {
builder.append("service_account: " + appEngineWebXml.getServiceAccount() + "\n");
}
List adminConsolePages = appEngineWebXml.getAdminConsolePages();
if (!adminConsolePages.isEmpty()) {
builder.append("admin_console:\n");
builder.append(" pages:\n");
for (AdminConsolePage page : adminConsolePages) {
builder.append(" - name: " + page.getName() + "\n");
builder.append(" url: " + page.getUrl() + "\n");
}
}
List errorHandlers = appEngineWebXml.getErrorHandlers();
if (!errorHandlers.isEmpty()) {
builder.append("error_handlers:\n");
for (ErrorHandler handler : errorHandlers) {
String fileName = handler.getFile();
if (!fileName.startsWith("/")) {
fileName = "/" + fileName;
}
// TODO: Consider whether we should be adding the
// public root to this path.
if (!staticFiles.contains("__static__" + fileName)) {
throw new AppEngineConfigException("No static file found for error handler: "
+ fileName + ", out of " + staticFiles);
}
// error_handlers doesn't want a leading slash here.
builder.append("- file: __static__" + fileName + "\n");
if (handler.getErrorCode() != null) {
builder.append(" error_code: " + handler.getErrorCode() + "\n");
}
String mimeType = webXml.getMimeTypeForPath(handler.getFile());
if (mimeType != null) {
builder.append(" mime_type: " + mimeType + "\n");
}
}
}
if (backendsXml != null) {
builder.append(backendsXml.toYaml());
}
// Only one api config, it is a singleton, and multiple APIs are served
// from subpaths within the namespace.
// TODO: Verify script: is required.
ApiConfig apiConfig = appEngineWebXml.getApiConfig();
if (apiConfig != null) {
builder.append("api_config:\n");
builder.append(" url: " + apiConfig.getUrl() + "\n");
builder.append(" script: unused\n");
}
// For beta-settings, we allow anything and defer to the later app.yaml processing
// to detect invalid values.
appendBetaSettings(appEngineWebXml.getBetaSettings(), builder);
appendEnvVariables(appEngineWebXml.getEnvironmentVariables(), builder);
appendBuildEnvVariables(appEngineWebXml.getBuildEnvironmentVariables(), builder);
if (appEngineWebXml.getUseVm() || appEngineWebXml.isFlexible()) {
if (appEngineWebXml.getHealthCheck() != null) {
appendHealthCheck(appEngineWebXml.getHealthCheck(), builder);
}
if (appEngineWebXml.getLivenessCheck() != null) {
appendLivenessCheck(appEngineWebXml.getLivenessCheck(), builder);
}
if (appEngineWebXml.getReadinessCheck() != null) {
appendReadinessCheck(appEngineWebXml.getReadinessCheck(), builder);
}
appendResources(appEngineWebXml.getResources(), builder);
appendNetwork(appEngineWebXml.getNetwork(), builder);
}
}
/**
* Appends the given environment variables as YAML to the given StringBuilder.
*
* @param envVariables The env variables map to append as YAML.
* @param builder The StringBuilder to append to.
*/
private void appendEnvVariables(Map envVariables, StringBuilder builder) {
if (envVariables.size() > 0) {
builder.append("env_variables:\n");
for (Map.Entry envVariable : envVariables.entrySet()) {
String k = envVariable.getKey();
String v = envVariable.getValue();
builder.append(" ").append(yamlQuote(k)).append(": ").append(yamlQuote(v)).append("\n");
}
}
}
/**
* Appends the given build environment variables as YAML to the given StringBuilder.
*
* @param buildEnvVariables The build env variables map to append as YAML.
* @param builder The StringBuilder to append to.
*/
private void appendBuildEnvVariables(
Map buildEnvVariables, StringBuilder builder) {
if (buildEnvVariables.size() > 0) {
builder.append("build_env_variables:\n");
for (Map.Entry buildEnvVariable : buildEnvVariables.entrySet()) {
String k = buildEnvVariable.getKey();
String v = buildEnvVariable.getValue();
builder.append(" ").append(yamlQuote(k)).append(": ").append(yamlQuote(v)).append("\n");
}
}
}
/**
* Appends the given Beta Settings as YAML to the given StringBuilder.
*
* @param betaSettings The beta settings map to append as YAML.
* @param builder The StringBuilder to append to.
*/
private void appendBetaSettings(Map betaSettings, StringBuilder builder) {
if (betaSettings != null && !betaSettings.isEmpty()) {
builder.append("beta_settings:\n");
for (Map.Entry setting : betaSettings.entrySet()) {
builder.append(
" " + yamlQuote(setting.getKey()) + ": " + yamlQuote(setting.getValue()) + "\n");
}
}
}
private void appendHealthCheck(HealthCheck healthCheck, StringBuilder builder) {
builder.append("health_check:\n");
if (healthCheck.getEnableHealthCheck()) {
builder.append(" enable_health_check: True\n");
} else {
builder.append(" enable_health_check: False\n");
}
appendIfNotNull(builder, " check_interval_sec: ", healthCheck.getCheckIntervalSec());
appendIfNotNull(builder, " timeout_sec: ", healthCheck.getTimeoutSec());
appendIfNotNull(builder, " unhealthy_threshold: ", healthCheck.getUnhealthyThreshold());
appendIfNotNull(builder, " healthy_threshold: ", healthCheck.getHealthyThreshold());
appendIfNotNull(builder, " restart_threshold: ", healthCheck.getRestartThreshold());
appendIfNotNull(builder, " host: ", healthCheck.getHost());
}
private void appendLivenessCheck(LivenessCheck livenessCheck, StringBuilder builder) {
builder.append("liveness_check:\n");
appendIfNotNull(builder, " path: ", livenessCheck.getPath());
appendIfNotNull(builder, " check_interval_sec: ", livenessCheck.getCheckIntervalSec());
appendIfNotNull(builder, " timeout_sec: ", livenessCheck.getTimeoutSec());
appendIfNotNull(builder, " failure_threshold: ", livenessCheck.getFailureThreshold());
appendIfNotNull(builder, " success_threshold: ", livenessCheck.getSuccessThreshold());
appendIfNotNull(builder, " host: ", livenessCheck.getHost());
appendIfNotNull(builder, " initial_delay_sec: ", livenessCheck.getInitialDelaySec());
}
private void appendReadinessCheck(ReadinessCheck readinessCheck, StringBuilder builder) {
builder.append("readiness_check:\n");
appendIfNotNull(builder, " path: ", readinessCheck.getPath());
appendIfNotNull(builder, " check_interval_sec: ", readinessCheck.getCheckIntervalSec());
appendIfNotNull(builder, " timeout_sec: ", readinessCheck.getTimeoutSec());
appendIfNotNull(builder, " failure_threshold: ", readinessCheck.getFailureThreshold());
appendIfNotNull(builder, " success_threshold: ", readinessCheck.getSuccessThreshold());
appendIfNotNull(builder, " host: ", readinessCheck.getHost());
appendIfNotNull(builder, " app_start_timeout_sec: ", readinessCheck.getAppStartTimeoutSec());
}
private void appendResources(Resources resources, StringBuilder builder) {
if (!resources.isEmpty()) {
builder.append("resources:\n");
appendIfNotZero(builder, " cpu: ", resources.getCpu());
appendIfNotZero(builder, " memory_gb: ", resources.getMemoryGb());
appendIfNotZero(builder, " disk_size_gb: ", resources.getDiskSizeGb());
}
}
private void appendNetwork(Network network, StringBuilder builder) {
if (!network.isEmpty()) {
builder.append("network:\n");
appendIfNotNull(builder, " instance_tag: ", network.getInstanceTag());
if (!network.getForwardedPorts().isEmpty()) {
builder.append(" forwarded_ports:\n");
for (String forwardedPort : network.getForwardedPorts()) {
builder.append(" - " + forwardedPort + "\n");
}
}
appendIfNotNull(builder, " name: ", network.getName());
appendIfNotNull(builder, " subnetwork_name: ", network.getSubnetworkName());
if (network.getSessionAffinity()) {
builder.append(" session_affinity: True\n");
} else {
builder.append(" session_affinity: False\n");
}
}
}
/**
* Appends the given collection to the StringBuilder as YAML, indenting each emitted line by
* numIndentSpaces.
*/
private static void appendObjectAsYaml(
StringBuilder builder, Object collection, int numIndentSpaces) {
StringBuilder prefixBuilder = new StringBuilder();
for (int i = 0; i < numIndentSpaces; ++i) {
prefixBuilder.append(' ');
}
final String indentPrefix = prefixBuilder.toString();
StringWriter stringWriter = new StringWriter();
YamlConfig yamlConfig = new YamlConfig();
yamlConfig.writeConfig.setIndentSize(2);
yamlConfig.writeConfig.setWriteRootTags(false);
YamlWriter writer = new YamlWriter(stringWriter, yamlConfig);
try {
writer.write(collection);
writer.close();
} catch (YamlException e) {
throw new AppEngineConfigException("Unable to generate YAML.", e);
}
// Add the requested number of spaces since we may be emitting children of a parent element.
for (String line : stringWriter.toString().split("\n")) {
builder.append(indentPrefix);
builder.append(line);
builder.append("\n");
}
}
/**
* Surrounds the provided string with single quotes, escaping any single
* quotes in the string by replacing them with ''.
*/
private String yamlQuote(String str) {
return "'" + str.replace("'", "''") + "'";
}
private void translateApiVersion(StringBuilder builder) {
if (apiVersion == null) {
builder.append("api_version: '" + NO_API_VERSION + "'\n");
} else {
builder.append("api_version: '" + apiVersion + "'\n");
}
}
private void translateWebXml(StringBuilder builder) {
builder.append("handlers:\n");
AbstractHandlerGenerator staticGenerator = null;
if (staticFiles.isEmpty()) {
// Don't bother emitting any static handlers if we have no static files.
staticGenerator = new EmptyHandlerGenerator();
} else {
staticGenerator = new StaticHandlerGenerator(appEngineWebXml.getPublicRoot());
}
DynamicHandlerGenerator dynamicGenerator =
new DynamicHandlerGenerator(webXml.getFallThroughToRuntime());
if (staticGenerator.size() + dynamicGenerator.size() > MAX_HANDLERS) {
// Are we going to generate too many handler entries? If so,
// try again with fallthrough set. This will prevent individual
// servlet/filter entries unless they are required for
// authentication or SSL purposes.
dynamicGenerator = new DynamicHandlerGenerator(true);
}
// Static handlers have require_matching_file first so try them first.
staticGenerator.translate(builder);
dynamicGenerator.translate(builder);
}
class StaticHandlerGenerator extends AbstractHandlerGenerator {
private final String root;
public StaticHandlerGenerator(String root) {
this.root = root;
}
@Override
protected Map getWelcomeProperties() {
// As an optimization, only include the filenames for which we
// know there is at least one static file on the filesystem.
List staticWelcomeFiles = new ArrayList();
for (String welcomeFile : webXml.getWelcomeFiles()) {
for (String staticFile : staticFiles) {
if (staticFile.endsWith("/" + welcomeFile)) {
staticWelcomeFiles.add(welcomeFile);
break;
}
}
}
return Collections.singletonMap(WELCOME_FILES, staticWelcomeFiles);
}
@Override
protected void addPatterns(GlobIntersector intersector) {
List includes = appEngineWebXml.getStaticFileIncludes();
if (includes.isEmpty()) {
intersector.addGlob(GlobFactory.createGlob("/*", STATIC_PROPERTY, true));
} else {
for (AppEngineWebXml.StaticFileInclude include : includes) {
String pattern = include.getPattern().replaceAll("\\*\\*", "*");
if (!pattern.startsWith("/")) {
pattern = "/" + pattern;
}
Map props = new HashMap();
props.put(STATIC_PROPERTY, true);
if (include.getExpiration() != null) {
props.put(EXPIRATION_PROPERTY, include.getExpiration());
}
// Add http_headers.
// include.getHttpHeaders shouldn't return null, but we check anyway,
// in case some future change makes this no longer true.
if (include.getHttpHeaders() != null) {
props.put(HTTP_HEADERS_PROPERTY, include.getHttpHeaders());
}
intersector.addGlob(GlobFactory.createGlob(pattern, props));
}
}
}
@Override
public void translateGlob(StringBuilder builder, Glob glob) {
String regex = glob.getRegularExpression().pattern();
if (!root.isEmpty()) {
if (regex.startsWith(root)){
regex = regex.substring(root.length(), regex.length());
}
}
@SuppressWarnings("unchecked")
List welcomeFiles =
(List) glob.getProperty(WELCOME_FILES, RESOLVER);
if (welcomeFiles != null) {
for (String welcomeFile : welcomeFiles) {
builder.append("- url: (" + regex + ")\n");
builder.append(" static_files: __static__" + root + "\\1" + welcomeFile + "\n");
builder.append(" upload: __NOT_USED__\n");
builder.append(" require_matching_file: True\n");
translateHandlerOptions(builder, glob);
translateAdditionalStaticOptions(builder, glob);
}
} else {
Boolean isStatic = (Boolean) glob.getProperty(STATIC_PROPERTY, RESOLVER);
if (isStatic != null && isStatic.booleanValue()) {
builder.append("- url: (" + regex + ")\n");
builder.append(" static_files: __static__" + root + "\\1\n");
builder.append(" upload: __NOT_USED__\n");
builder.append(" require_matching_file: True\n");
translateHandlerOptions(builder, glob);
translateAdditionalStaticOptions(builder, glob);
}
}
}
private void translateAdditionalStaticOptions(StringBuilder builder, Glob glob)
throws AppEngineConfigException {
String expiration = (String) glob.getProperty(EXPIRATION_PROPERTY, RESOLVER);
if (expiration != null) {
builder.append(" expiration: " + expiration + "\n");
}
@SuppressWarnings("unchecked")
Map httpHeaders =
(Map) glob.getProperty(HTTP_HEADERS_PROPERTY, RESOLVER);
if (httpHeaders != null && !httpHeaders.isEmpty()) {
builder.append(" http_headers:\n");
appendObjectAsYaml(builder, httpHeaders, 4);
}
}
}
/**
* According to the example In section 12.2.2 of Servlet Spec 3.0 , /baz/* should also match /baz,
* so add an additional glob for that.
*/
private static void extendMeaningOfTrailingStar(
GlobIntersector intersector, String pattern, String property, Object value) {
if (pattern.endsWith("/*") && pattern.length() > 2) {
intersector.addGlob(
GlobFactory.createGlob(pattern.substring(0, pattern.length() - 2), property, value));
}
}
class DynamicHandlerGenerator extends AbstractHandlerGenerator {
private final List patterns;
private boolean fallthrough;
private boolean hasJsps;
DynamicHandlerGenerator(boolean alwaysFallthrough) {
fallthrough = alwaysFallthrough;
patterns = new ArrayList();
for (String servletPattern : webXml.getServletPatterns()) {
if (servletPattern.equals("/") || servletPattern.equals("/*")) {
// The special servlet URL pattern "/" actually serves as the
// default servlet, which means that it matches all requests
// that aren't matched by other servlet patterns.
fallthrough = true;
} else if (servletPattern.equals(API_ENDPOINT_REGEX)) {
// The special servlet URL pattern "/_ah/spi/*" serves the
// Apiserving backend. Admin console looks for this pattern
// explicitly so it must not be collapsed by GlobIntersector.
hasApiEndpoint = true;
} else if (servletPattern.endsWith(".jsp")) {
hasJsps = true;
} else {
if (servletPattern.equals("")) { // New in Servlet 3.x, we map it to /
servletPattern = "/";
}
patterns.add(servletPattern);
}
}
}
@Override
protected Map getWelcomeProperties() {
if (fallthrough) {
// Don't bother adding handlers for dynamic welcome files,
// we're going to pass all requests through to the runtime
// anyway.
return null;
} else {
return Collections.singletonMap(DYNAMIC_PROPERTY, true);
}
}
@Override
protected void addPatterns(GlobIntersector intersector) {
if (fallthrough) {
intersector.addGlob(GlobFactory.createGlob(
"/*",
DYNAMIC_PROPERTY, true));
} else {
for (String servletPattern : patterns) {
intersector.addGlob(GlobFactory.createGlob(
servletPattern,
DYNAMIC_PROPERTY, true));
extendMeaningOfTrailingStar(intersector, servletPattern, DYNAMIC_PROPERTY, true);
}
if (hasJsps) {
// Just add a single rule for any JSPs so users can define more
// than 100. The extra load for serving 404's for mistyped
// jsp requests is trivial.
intersector.addGlob(GlobFactory.createGlob(
"*.jsp",
DYNAMIC_PROPERTY, true));
} else if (appEngineWebXml.getUseVm() || appEngineWebXml.isFlexible()) {
// The VM Runtime handles jsp files on the VM.
intersector.addGlob(GlobFactory.createGlob("*.jsp", DYNAMIC_PROPERTY, true));
}
intersector.addGlob(GlobFactory.createGlob(
"/_ah/*",
DYNAMIC_PROPERTY, true));
}
}
@Override
public void translateGlob(StringBuilder builder, Glob glob) {
String regex = glob.getRegularExpression().pattern();
Boolean isDynamic = (Boolean) glob.getProperty(DYNAMIC_PROPERTY, RESOLVER);
if (isDynamic != null && isDynamic.booleanValue()) {
builder.append("- url: " + regex + "\n");
builder.append(" script: unused\n");
translateHandlerOptions(builder, glob);
}
}
}
/**
* An {@code AbstractHandlerGenerator} that returns no globs.
*/
class EmptyHandlerGenerator extends AbstractHandlerGenerator {
@Override
protected void addPatterns(GlobIntersector intersector) {
}
@Override
protected void translateGlob(StringBuilder builder, Glob glob) {
}
@Override
protected Map getWelcomeProperties() {
return Collections.emptyMap();
}
}
abstract class AbstractHandlerGenerator {
private List globs = null;
protected boolean hasApiEndpoint;
public int size() {
return getGlobPatterns().size();
}
public void translate(StringBuilder builder) {
for (Glob glob : getGlobPatterns()) {
translateGlob(builder, glob);
}
}
abstract protected void addPatterns(GlobIntersector intersector);
abstract protected void translateGlob(StringBuilder builder, Glob glob);
/**
* @returns a map of welcome properties to apply to the welcome
* file entries, or {@code null} if no welcome file entries are
* necessary.
*/
abstract protected Map getWelcomeProperties();
protected List getGlobPatterns() {
if (globs == null) {
GlobIntersector intersector = new GlobIntersector();
addPatterns(intersector);
addSecurityConstraints(intersector);
addWelcomeFiles(intersector);
globs = intersector.getIntersection();
removeNearDuplicates(globs);
if (hasApiEndpoint) {
// Add /_ah/spi/* handler back in explicitly after intersection and duplicate
// removal, otherwise it will be subsumed by /_ah/* above. Admin console
// must see this explicitly as a signal that the app is serving an API
// endpoint. See http://go/swarmSignal2
globs.add(GlobFactory.createGlob(API_ENDPOINT_REGEX, DYNAMIC_PROPERTY, true));
}
}
return globs;
}
protected void addWelcomeFiles(GlobIntersector intersector) {
Map welcomeProperties = getWelcomeProperties();
if (welcomeProperties != null) {
// N.B.: Unfortunately we need to do both / and /*/
// rather than just */ here so any /* patterns interact
// correctly. If I think too hard about this my brain explodes
// so I've just left it this way for now.
intersector.addGlob(GlobFactory.createGlob("/", welcomeProperties));
intersector.addGlob(GlobFactory.createGlob("/*/", welcomeProperties));
}
}
protected void addSecurityConstraints(GlobIntersector intersector) {
for (SecurityConstraint constraint : webXml.getSecurityConstraints()) {
for (String pattern : constraint.getUrlPatterns()) {
intersector.addGlob(GlobFactory.createGlob(
pattern,
TRANSPORT_GUARANTEE_PROPERTY,
constraint.getTransportGuarantee()));
extendMeaningOfTrailingStar(intersector, pattern, TRANSPORT_GUARANTEE_PROPERTY,
constraint.getTransportGuarantee());
intersector.addGlob(GlobFactory.createGlob(
pattern,
REQUIRED_ROLE_PROPERTY,
constraint.getRequiredRole()));
extendMeaningOfTrailingStar(
intersector, pattern, REQUIRED_ROLE_PROPERTY, constraint.getRequiredRole());
}
}
}
protected void translateHandlerOptions(StringBuilder builder, Glob glob) {
SecurityConstraint.RequiredRole requiredRole =
(SecurityConstraint.RequiredRole) glob.getProperty(REQUIRED_ROLE_PROPERTY, RESOLVER);
if (requiredRole == null) {
requiredRole = SecurityConstraint.RequiredRole.NONE;
}
switch (requiredRole) {
case NONE:
builder.append(" login: optional\n");
break;
case ANY_USER:
builder.append(" login: required\n");
break;
case ADMIN:
builder.append(" login: admin\n");
break;
}
SecurityConstraint.TransportGuarantee transportGuarantee =
(SecurityConstraint.TransportGuarantee) glob.getProperty(TRANSPORT_GUARANTEE_PROPERTY,
RESOLVER);
if (transportGuarantee == null) {
transportGuarantee = SecurityConstraint.TransportGuarantee.NONE;
}
switch (transportGuarantee) {
case NONE:
if (appEngineWebXml.getSslEnabled()) {
builder.append(" secure: optional\n");
} else {
builder.append(" secure: never\n");
}
break;
case INTEGRAL:
case CONFIDENTIAL:
if (!appEngineWebXml.getSslEnabled()) {
throw new AppEngineConfigException(
"SSL must be enabled in appengine-web.xml to use transport-guarantee");
}
builder.append(" secure: always\n");
break;
}
String pattern = glob.getRegularExpression().pattern();
String id = webXml.getHandlerIdForPattern(pattern);
if (id != null) {
if (appEngineWebXml.isApiEndpoint(id)) {
builder.append(" api_endpoint: True\n");
}
}
}
private void removeNearDuplicates(List globs) {
// For each entry...
for (int i = 0; i < globs.size(); i++) {
Glob topGlob = globs.get(i);
// Find the following entry that completely subsumes it...
for (int j = i + 1; j < globs.size(); j++) {
Glob bottomGlob = globs.get(j);
if (bottomGlob.matchesAll(topGlob)) {
// If the properties match, the top entry can be removed.
if (propertiesMatch(topGlob, bottomGlob)) {
globs.remove(i);
i--;
}
// If not, the first entry is important so check the next.
break;
}
}
}
}
private boolean propertiesMatch(Glob glob1, Glob glob2) {
for (String property : PROPERTIES) {
Object value1 = glob1.getProperty(property, RESOLVER);
Object value2 = glob2.getProperty(property, RESOLVER);
if (value1 != value2 && (value1 == null || !value1.equals(value2))) {
return false;
}
}
return true;
}
}
}