From f7585ba835d6ff8f5b14c4139ee5010ec9159f7f Mon Sep 17 00:00:00 2001 From: "ludo@google.com" Date: Mon, 9 Sep 2013 22:43:47 +0000 Subject: [PATCH] App Engine 1.8.4. git-svn-id: http://googleappengine.googlecode.com/svn/trunk@383 80f5ef21-4148-0410-bacc-cfb02402ada8 --- .../utils/config/AbstractConfigXmlReader.java | 277 ++++ .../utils/config/AppEngineApplicationXml.java | 65 + .../config/AppEngineApplicationXmlReader.java | 48 + .../utils/config/AppEngineConfigException.java | 21 + .../apphosting/utils/config/AppEngineWebXml.java | 1561 ++++++++++++++++++++ .../utils/config/AppEngineWebXmlProcessor.java | 490 ++++++ .../utils/config/AppEngineWebXmlReader.java | 136 ++ .../google/apphosting/utils/config/AppYaml.java | 1402 ++++++++++++++++++ .../apphosting/utils/config/ApplicationXml.java | 235 +++ .../utils/config/ApplicationXmlReader.java | 122 ++ .../apphosting/utils/config/BackendsXml.java | 297 ++++ .../apphosting/utils/config/BackendsXmlReader.java | 120 ++ .../utils/config/BackendsYamlReader.java | 187 +++ .../apphosting/utils/config/ClassPathBuilder.java | 194 +++ .../google/apphosting/utils/config/CronXml.java | 180 +++ .../apphosting/utils/config/CronXmlReader.java | 135 ++ .../apphosting/utils/config/CronYamlReader.java | 89 ++ .../apphosting/utils/config/DispatchXml.java | 158 ++ .../apphosting/utils/config/DispatchXmlReader.java | 160 ++ .../utils/config/DispatchYamlReader.java | 119 ++ .../com/google/apphosting/utils/config/DosXml.java | 115 ++ .../apphosting/utils/config/DosXmlReader.java | 110 ++ .../apphosting/utils/config/DosYamlReader.java | 89 ++ .../google/apphosting/utils/config/EarInfo.java | 131 ++ .../utils/config/GenerationDirectory.java | 41 + .../apphosting/utils/config/IndexYamlReader.java | 207 +++ .../google/apphosting/utils/config/IndexesXml.java | 195 +++ .../apphosting/utils/config/IndexesXmlReader.java | 223 +++ .../apphosting/utils/config/PluginLoader.java | 56 + .../google/apphosting/utils/config/QueueXml.java | 639 ++++++++ .../apphosting/utils/config/QueueXmlReader.java | 243 +++ .../apphosting/utils/config/QueueYamlReader.java | 241 +++ .../google/apphosting/utils/config/WebModule.java | 156 ++ .../com/google/apphosting/utils/config/WebXml.java | 163 ++ .../apphosting/utils/config/WebXmlReader.java | 233 +++ .../google/apphosting/utils/config/XmlUtils.java | 54 + .../google/apphosting/utils/config/YamlUtils.java | 106 ++ 37 files changed, 8998 insertions(+) create mode 100644 java/src/main/com/google/apphosting/utils/config/AbstractConfigXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/AppEngineConfigException.java create mode 100644 java/src/main/com/google/apphosting/utils/config/AppEngineWebXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlProcessor.java create mode 100644 java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/AppYaml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/ApplicationXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/ApplicationXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/BackendsXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/BackendsXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/BackendsYamlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/ClassPathBuilder.java create mode 100644 java/src/main/com/google/apphosting/utils/config/CronXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/CronXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/CronYamlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/DispatchXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/DispatchXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/DispatchYamlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/DosXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/DosXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/DosYamlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/EarInfo.java create mode 100644 java/src/main/com/google/apphosting/utils/config/GenerationDirectory.java create mode 100644 java/src/main/com/google/apphosting/utils/config/IndexYamlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/IndexesXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/IndexesXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/PluginLoader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/QueueXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/QueueXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/QueueYamlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/WebModule.java create mode 100644 java/src/main/com/google/apphosting/utils/config/WebXml.java create mode 100644 java/src/main/com/google/apphosting/utils/config/WebXmlReader.java create mode 100644 java/src/main/com/google/apphosting/utils/config/XmlUtils.java create mode 100644 java/src/main/com/google/apphosting/utils/config/YamlUtils.java diff --git a/java/src/main/com/google/apphosting/utils/config/AbstractConfigXmlReader.java b/java/src/main/com/google/apphosting/utils/config/AbstractConfigXmlReader.java new file mode 100644 index 00000000..b3a53f7b --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AbstractConfigXmlReader.java @@ -0,0 +1,277 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser; +import org.mortbay.xml.XmlParser.Node; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Stack; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Abstract class for reading the XML files to configure an application. + * + * @param the type of the configuration object returned + * + * + */ +public abstract class AbstractConfigXmlReader { + + /** + * Callback notified as nodes are traversed in the parsed XML. + */ + public interface ParserCallback { + /** + * Node handling callback. + * + * @param node the newly-entered node + * @param ancestors a possibly-empty (but not null) stack of parent nodes + * of {@code node}. + * @throws AppEngineConfigException if something is wrong in the XML + */ + public void newNode(XmlParser.Node node, Stack ancestors); + } + + /** The path to the top level directory of the application. */ + protected final String appDir; + + /** Whether the config file must exist in a correct application. */ + protected final boolean required; + + /** A logger for messages. */ + protected Logger logger; + + /** + * Initializes the generic attributes of all our configuration XML readers. + * + * @param appDir pathname to the application directory + * @param required {@code true} if is an error for the config file not to exist. + */ + public AbstractConfigXmlReader(String appDir, boolean required) { + if (appDir.length() > 0 && appDir.charAt(appDir.length() - 1) != File.separatorChar) { + appDir += File.separatorChar; + } + this.appDir = appDir; + this.required = required; + logger = Logger.getLogger(this.getClass().getName()); + } + + /** + * Gets the absolute filename for the configuration file. + * + * @return concatenation of {@link #appDir} and {@link #getRelativeFilename()} + */ + public String getFilename() { + return appDir + getRelativeFilename(); + } + + /** + * Fetches the name of the configuration file processed by this instance, + * relative to the application directory. + * + * @return relative pathname for a configuration file + */ + protected abstract String getRelativeFilename(); + + /** + * Parses the input stream to compute an instance of {@code T}. + * + * @return the parsed config file + * @throws AppEngineConfigException if there is an error. + */ + protected abstract T processXml(InputStream is); + + /** + * Does the work of reading the XML file, processing it, and either returning + * an object representing the result or throwing error information. + * + * @return A {@link AppEngineWebXml} config object derived from the + * contents of the config xml, or {@code null} if no such file is defined and + * the config file is optional. + * + * @throws AppEngineConfigException If the file cannot be parsed properly + */ + protected T readConfigXml() { + InputStream is = null; + T configXml; + if (!required && !fileExists()) { + return null; + } + try { + is = getInputStream(); + configXml = processXml(is); + logger.info("Successfully processed " + getFilename()); + } catch (Exception e) { + String msg = "Received exception processing " + getFilename(); + logger.log(Level.SEVERE, msg, e); + if (e instanceof AppEngineConfigException) { + throw (AppEngineConfigException) e; + } + throw new AppEngineConfigException(msg, e); + } finally { + close(is); + } + return configXml; + } + + /** + * Tests for file existence. Test clases will often override this, to lie + * (and thus stay small by avoiding needing the real filesystem). + */ + protected boolean fileExists() { + return (new File(getFilename())).exists(); + } + + /** + * Tests for file existence. Test clases will often override this, to lie + * (and thus stay small by avoiding needing the real filesystem). + */ + protected boolean generatedFileExists() { + return getGeneratedFile().exists(); + } + + /** + * Opens an input stream, or fails with an AppEngineConfigException + * containing helpful information. Test classes will often override this. + * + * @return an open {@link InputStream} + * @throws AppEngineConfigException + */ + protected InputStream getInputStream() { + try { + return new FileInputStream(getFilename()); + } catch (FileNotFoundException fnfe) { + throw new AppEngineConfigException( + "Could not locate " + new File(getFilename()).getAbsolutePath(), fnfe); + } + } + + /** + * Returns a {@code File} for the generated variant of this file, or + * {@code null} if no generation is possible. This is not an indication that + * the file exists, only of where it would be if it does exist. + * + * @return the generated file, if there might be one; {@code null} if not. + */ + protected File getGeneratedFile() { + return null; + } + + /** + * Returns an InputStream of the generated contents, or {@code null} if no + * generated contents are available. + * + * @return input stream, or {@code null} + */ + protected InputStream getGeneratedStream() { + File file = getGeneratedFile(); + if (file == null || !file.exists()) { + return null; + } + try { + return new FileInputStream(file); + } catch (FileNotFoundException ex) { + throw new AppEngineConfigException("can't find generated " + file.getPath()); + } + } + + /** + * Creates an {@link XmlParser} to use when parsing this file. + */ + protected XmlParser createXmlParser() { + return new XmlParser(); + } + + /** + * Given an InputStream, create a Node corresponding to the top level xml + * element. + * + * @throws AppEngineConfigException If the input stream cannot be parsed. + */ + protected XmlParser.Node getTopLevelNode(InputStream is) { + XmlParser xmlParser = createXmlParser(); + try { + return xmlParser.parse(is); + } catch (IOException e) { + String msg = "Received IOException parsing the input stream for " + getFilename(); + logger.log(Level.SEVERE, msg, e); + throw new AppEngineConfigException(msg, e); + } catch (SAXException e) { + String msg = "Received SAXException parsing the input stream for " + getFilename(); + logger.log(Level.SEVERE, msg, e); + throw new AppEngineConfigException(msg, e); + } + } + + /** + * Parses the nodes of an XML file. This is limited XML parsing, + * in particular skipping any TEXT element and parsing only the nodes. + * + * @param parseCb the ParseCallback to call for each node + * @param is the input stream to read + * @throws AppEngineConfigException on any error + */ + protected void parse(ParserCallback parseCb, InputStream is) { + Stack stack = new Stack(); + XmlParser.Node top = getTopLevelNode(is); + parse(top, stack, parseCb); + } + + /** + * Recursive descent helper for {@link #parse(ParserCallback, InputStream)}, calling + * the callback for this node and recursing for its children. + * + * @param node the node being visited + * @param stack the anscestors of {@code node} + * @param parseCb the visitor callback + * @throws AppEngineConfigException for any configuration errors + */ + protected void parse(XmlParser.Node node, Stack stack, ParserCallback parseCb) { + parseCb.newNode(node, stack); + stack.push(node); + for (Object child : node) { + if (child instanceof XmlParser.Node) { + parse((XmlParser.Node) child, stack, parseCb); + } + } + stack.pop(); + } + + /** + * Closes the given input stream, converting any {@link IOException} thrown + * to an {@link AppEngineConfigException} if necessary. + * + * @throws AppEngineConfigException if the input stream cannot close + */ + protected void close(InputStream is) { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + throw new AppEngineConfigException(e); + } + } + } + + /** + * Gets the Node's first (index zero) content value, as a trimmed string. + * + * @param node the node to get the string from. + */ + protected String getString(Node node) { + String string = (String) node.get(0); + if (string == null) { + return null; + } else { + return string.trim(); + } + } + +} diff --git a/java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXml.java b/java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXml.java new file mode 100644 index 00000000..dc474e29 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXml.java @@ -0,0 +1,65 @@ +package com.google.apphosting.utils.config; + +/** + * Holder for appengine-applicaion.xml properties. + */ +public class AppEngineApplicationXml { + private final String applicationId; + + private AppEngineApplicationXml(String applicationId) { + this.applicationId = applicationId; + } + + public String getApplicationId() { + return applicationId; + } + + @Override + public int hashCode() { + final int prime = 31; + return prime + ((applicationId == null) ? 0 : applicationId.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj){ + return true; + } + if (obj == null){ + return false; + } + if (getClass() != obj.getClass()){ + return false; + } + AppEngineApplicationXml other = (AppEngineApplicationXml) obj; + if (applicationId == null) { + if (other.applicationId != null){ + return false; + } + } else if (!applicationId.equals(other.applicationId)){ + return false; + } + return true; + } + + @Override + public String toString() { + return "AppEngineApplicationXml: application=" + applicationId; + } + + /** + * Builder for an {@link AppEngineApplicationXml} + */ + static class Builder{ + private String applicationId; + + Builder setApplicationId(String applicationId) { + this.applicationId = applicationId; + return this; + } + + AppEngineApplicationXml build() { + return new AppEngineApplicationXml(applicationId); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXmlReader.java b/java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXmlReader.java new file mode 100644 index 00000000..a660036b --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AppEngineApplicationXmlReader.java @@ -0,0 +1,48 @@ +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser; + +import java.io.InputStream; + +/** + * Constructs an {@link AppEngineApplicationXml} from an xml document + * corresponding to appengine-application.xsd. + * + *

We use Jetty's {@link XmlParser} utility to match other Appengine XML + * parsing code. + * + */ +public class AppEngineApplicationXmlReader { + private static final String EMPTY_STRING = ""; + + /** + * Construct an {@link AppEngineApplicationXml} from the xml document + * within the provided {@link InputStream}. + * + * @param is The InputStream containing the xml we want to parse and process + * + * @return Object representation of the xml document + * @throws AppEngineConfigException If the input stream cannot be parsed + */ + public AppEngineApplicationXml processXml(InputStream is) throws AppEngineConfigException { + AppEngineApplicationXml.Builder builder = new AppEngineApplicationXml.Builder(); + String applicationId = EMPTY_STRING; + for (Object o : XmlUtils.parse(is)) { + if (!(o instanceof XmlParser.Node)) { + continue; + } + XmlParser.Node node = (XmlParser.Node) o; + if (node.getTag().equals("application")) { + applicationId = XmlUtils.getText(node); + } else { + throw new AppEngineConfigException("Unrecognized element <" + node.getTag() + + "> in appengine-application.xml."); + } + } + if (applicationId.equals(EMPTY_STRING)) { + throw new AppEngineConfigException( + "Missing or empty element in appengine-application.xml."); + } + return builder.setApplicationId(applicationId).build(); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/AppEngineConfigException.java b/java/src/main/com/google/apphosting/utils/config/AppEngineConfigException.java new file mode 100644 index 00000000..d1d5b043 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AppEngineConfigException.java @@ -0,0 +1,21 @@ +// Copyright 2008 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +/** + * Describes a problem with the configuration of App Engine. + * + */ +public class AppEngineConfigException extends RuntimeException { + + public AppEngineConfigException(String message) { + super(message); + } + + public AppEngineConfigException(String message, Throwable cause) { + super(message, cause); + } + + public AppEngineConfigException(Throwable cause) { + super(cause); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/AppEngineWebXml.java b/java/src/main/com/google/apphosting/utils/config/AppEngineWebXml.java new file mode 100644 index 00000000..e9f088ca --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AppEngineWebXml.java @@ -0,0 +1,1561 @@ +// Copyright 2008 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +import com.google.common.base.Objects; +import com.google.common.base.StringUtil; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.security.Permissions; +import java.security.UnresolvedPermission; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Struct describing the config data that lives in WEB-INF/appengine-web.xml. + * + * Any additions to this class should also be made to the YAML + * version in AppYaml.java. + * + */ +public class AppEngineWebXml { + /** + * Enumeration of supported scaling types. + */ + public static enum ScalingType {AUTOMATIC, MANUAL, BASIC} + + private final Map systemProperties = Maps.newHashMap(); + + private final Map vmSettings = Maps.newLinkedHashMap(); + + private final Map envVariables = Maps.newHashMap(); + + private final List userPermissions = new ArrayList(); + + public static final String WARMUP_SERVICE = "warmup"; + + public static final String URL_HANDLER_URLFETCH = "urlfetch"; + public static final String URL_HANDLER_NATIVE= "native"; + + private String appId; + + private String majorVersionId; + + private String module; + private String instanceClass; + + private final AutomaticScaling automaticScaling; + private final ManualScaling manualScaling; + private final BasicScaling basicScaling; + + private String sourceLanguage; + private boolean sslEnabled = true; + private boolean useSessions = false; + private boolean asyncSessionPersistence = false; + private String asyncSessionPersistenceQueueName; + + private final List staticFileIncludes; + private final List staticFileExcludes; + private final List resourceFileIncludes; + private final List resourceFileExcludes; + + private Pattern staticIncludePattern; + private Pattern staticExcludePattern; + private Pattern resourceIncludePattern; + private Pattern resourceExcludePattern; + + private String publicRoot = ""; + + private String appRoot; + + private final Set inboundServices; + private boolean precompilationEnabled = true; + + private final List adminConsolePages = new ArrayList(); + private final List errorHandlers = new ArrayList(); + + private ClassLoaderConfig classLoaderConfig; + + private String urlStreamHandlerType = null; + + private boolean threadsafe = false; + private boolean threadsafeValueProvided = false; + + private String autoIdPolicy; + + private boolean codeLock = false; + private boolean useVm = false; + private ApiConfig apiConfig; + private final List apiEndpointIds; + private Pagespeed pagespeed; + + /** + * Represent user's choice w.r.t the usage of Google's customized connector-j. + */ + public static enum UseGoogleConnectorJ { + NOT_STATED_BY_USER, + TRUE, + FALSE, + } + private UseGoogleConnectorJ useGoogleConnectorJ = UseGoogleConnectorJ.NOT_STATED_BY_USER; + + public AppEngineWebXml() { + automaticScaling = new AutomaticScaling(); + manualScaling = new ManualScaling(); + basicScaling = new BasicScaling(); + + staticFileIncludes = new ArrayList(); + staticFileExcludes = new ArrayList(); + staticFileExcludes.add("WEB-INF/**"); + staticFileExcludes.add("**.jsp"); + resourceFileIncludes = new ArrayList(); + resourceFileExcludes = new ArrayList(); + inboundServices = new LinkedHashSet(); + apiEndpointIds = new ArrayList(); + } + + /** + * @return An unmodifiable map whose entries correspond to the + * system properties defined in appengine-web.xml. + */ + public Map getSystemProperties() { + return Collections.unmodifiableMap(systemProperties); + } + + public void addSystemProperty(String key, String value) { + systemProperties.put(key, value); + } + + /** + * @return An unmodifiable map whose entires correspond to the + * vm settings defined in appengine-web.xml. + */ + public Map getVmSettings() { + return Collections.unmodifiableMap(vmSettings); + } + + public void addVmSetting(String key, String value) { + vmSettings.put(key, value); + } + + /** + * @return An unmodifiable map whose entires correspond to the + * environment variables defined in appengine-web.xml. + */ + public Map getEnvironmentVariables() { + return Collections.unmodifiableMap(envVariables); + } + + public void addEnvironmentVariable(String key, String value) { + envVariables.put(key, value); + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getMajorVersionId() { + return majorVersionId; + } + + public void setMajorVersionId(String majorVersionId) { + this.majorVersionId = majorVersionId; + } + + public String getSourceLanguage() { + return this.sourceLanguage; + } + + public void setSourceLanguage(String sourceLanguage) { + this.sourceLanguage = sourceLanguage; + } + + public String getModule() { + return module; + } + + /** + * Sets instanceClass (aka class in the xml/yaml files). Normalizes empty and null + * inputs to null. + */ + public void setInstanceClass(String instanceClass) { + this.instanceClass = StringUtil.toNullIfEmptyOrWhitespace(instanceClass); + } + + public String getInstanceClass() { + return instanceClass; + } + + public AutomaticScaling getAutomaticScaling() { + return automaticScaling; + } + + public ManualScaling getManualScaling() { + return manualScaling; + } + + public BasicScaling getBasicScaling() { + return basicScaling; + } + + public ScalingType getScalingType() { + if (!getBasicScaling().isEmpty()) { + return ScalingType.BASIC; + } else if (!getManualScaling().isEmpty()) { + return ScalingType.MANUAL; + } else { + return ScalingType.AUTOMATIC; + } + } + + public void setModule(String module) { + this.module = module; + } + + public void setSslEnabled(boolean ssl) { + sslEnabled = ssl; + } + + public boolean getSslEnabled() { + return sslEnabled; + } + + public void setSessionsEnabled(boolean sessions) { + useSessions = sessions; + } + + public boolean getSessionsEnabled() { + return useSessions; + } + + public void setAsyncSessionPersistence(boolean asyncSessionPersistence) { + this.asyncSessionPersistence = asyncSessionPersistence; + } + + public boolean getAsyncSessionPersistence() { + return asyncSessionPersistence; + } + + public void setAsyncSessionPersistenceQueueName(String asyncSessionPersistenceQueueName) { + this.asyncSessionPersistenceQueueName = asyncSessionPersistenceQueueName; + } + + public String getAsyncSessionPersistenceQueueName() { + return asyncSessionPersistenceQueueName; + } + + public List getStaticFileIncludes() { + return staticFileIncludes; + } + + public List getStaticFileExcludes() { + return staticFileExcludes; + } + + public StaticFileInclude includeStaticPattern(String pattern, String expiration) { + staticIncludePattern = null; + StaticFileInclude staticFileInclude = new StaticFileInclude(pattern, expiration); + staticFileIncludes.add(staticFileInclude); + return staticFileInclude; + } + + public void excludeStaticPattern(String url) { + staticExcludePattern = null; + staticFileExcludes.add(url); + } + + public List getResourcePatterns() { + return resourceFileIncludes; + } + + public List getResourceFileExcludes() { + return resourceFileExcludes; + } + + public void includeResourcePattern(String url) { + resourceExcludePattern = null; + resourceFileIncludes.add(url); + } + + public void excludeResourcePattern(String url) { + resourceIncludePattern = null; + resourceFileExcludes.add(url); + } + + public void addUserPermission(String className, String name, String actions) { + if (className.startsWith("java.")) { + throw new AppEngineConfigException("Cannot specify user-permissions for " + + "classes in java.* packages."); + } + + userPermissions.add(new UserPermission(className, name, actions)); + } + + public Permissions getUserPermissions() { + Permissions permissions = new Permissions(); + for (UserPermission permission : userPermissions) { + permissions.add(new UnresolvedPermission(permission.getClassName(), + permission.getName(), + permission.getActions(), + null)); + } + permissions.setReadOnly(); + return permissions; + } + + public void setPublicRoot(String root) { + if (root.indexOf('*') != -1) { + throw new AppEngineConfigException("public-root cannot contain wildcards"); + } + if (root.endsWith("/")) { + root = root.substring(0, root.length() - 1); + } + if (root.length() > 0 && !root.startsWith("/")) { + root = "/" + root; + } + staticIncludePattern = null; + publicRoot = root; + } + + public String getPublicRoot() { + return publicRoot; + } + + public void addInboundService(String service) { + inboundServices.add(service); + } + + public Set getInboundServices() { + return inboundServices; + } + + public boolean getPrecompilationEnabled() { + return precompilationEnabled; + } + + public void setPrecompilationEnabled(boolean precompilationEnabled) { + this.precompilationEnabled = precompilationEnabled; + } + + public boolean getWarmupRequestsEnabled() { + return inboundServices.contains(WARMUP_SERVICE); + } + + public void setWarmupRequestsEnabled(boolean warmupRequestsEnabled) { + if (warmupRequestsEnabled) { + inboundServices.add(WARMUP_SERVICE); + } else { + inboundServices.remove(WARMUP_SERVICE); + } + } + + public List getAdminConsolePages() { + return Collections.unmodifiableList(adminConsolePages); + } + + public void addAdminConsolePage(AdminConsolePage page) { + adminConsolePages.add(page); + } + + public List getErrorHandlers() { + return Collections.unmodifiableList(errorHandlers); + } + + public void addErrorHandler(ErrorHandler handler) { + errorHandlers.add(handler); + } + + public boolean getThreadsafe() { + return threadsafe; + } + + public boolean getThreadsafeValueProvided() { + return threadsafeValueProvided; + } + + public void setThreadsafe(boolean threadsafe) { + this.threadsafe = threadsafe; + this.threadsafeValueProvided = true; + } + + public void setAutoIdPolicy(String policy) { + autoIdPolicy = policy; + } + + public String getAutoIdPolicy() { + return autoIdPolicy; + } + + public boolean getCodeLock() { + return codeLock; + } + + public void setCodeLock(boolean codeLock) { + this.codeLock = codeLock; + } + + public void setUseVm(boolean useVm) { + this.useVm = useVm; + } + + public boolean getUseVm() { + return useVm; + } + + public ApiConfig getApiConfig() { + return apiConfig; + } + + public void setApiConfig(ApiConfig config) { + apiConfig = config; + } + + public ClassLoaderConfig getClassLoaderConfig() { + return classLoaderConfig; + } + + public void setClassLoaderConfig(ClassLoaderConfig classLoaderConfig) { + if (this.classLoaderConfig != null) { + throw new AppEngineConfigException("class-loader-config may only be specified once."); + } + this.classLoaderConfig = classLoaderConfig; + } + + public String getUrlStreamHandlerType() { + return urlStreamHandlerType; + } + + public void setUrlStreamHandlerType(String urlStreamHandlerType) { + if (this.classLoaderConfig != null) { + throw new AppEngineConfigException("url-stream-handler may only be specified once."); + } + if (!URL_HANDLER_URLFETCH.equals(urlStreamHandlerType) + && !URL_HANDLER_NATIVE.equals(urlStreamHandlerType)) { + throw new AppEngineConfigException( + "url-stream-handler must be " + URL_HANDLER_URLFETCH + " or " + URL_HANDLER_NATIVE + + " given " + urlStreamHandlerType); + } + this.urlStreamHandlerType = urlStreamHandlerType; + } + + /** + * Returns true if {@code url} matches one of the servlets or servlet + * filters listed in this web.xml that has api-endpoint set to true. + */ + public boolean isApiEndpoint(String id) { + return apiEndpointIds.contains(id); + } + + public void addApiEndpoint(String id) { + apiEndpointIds.add(id); + } + + public Pagespeed getPagespeed() { + return pagespeed; + } + + public void setPagespeed(Pagespeed pagespeed) { + this.pagespeed = pagespeed; + } + + public void setUseGoogleConnectorJ(boolean useGoogleConnectorJ) { + if (useGoogleConnectorJ) { + this.useGoogleConnectorJ = UseGoogleConnectorJ.TRUE; + } else { + this.useGoogleConnectorJ = UseGoogleConnectorJ.FALSE; + } + } + + public UseGoogleConnectorJ getUseGoogleConnectorJ() { + return useGoogleConnectorJ; + } + + @Override + public String toString() { + return "AppEngineWebXml{" + + "systemProperties=" + systemProperties + + ", envVariables=" + envVariables + + ", userPermissions=" + userPermissions + + ", appId='" + appId + '\'' + + ", majorVersionId='" + majorVersionId + '\'' + + ", sourceLanguage='" + sourceLanguage + '\'' + + ", module='" + module + '\'' + + ", instanceClass='" + instanceClass + '\'' + + ", automaticScaling=" + automaticScaling + + ", manualScaling=" + manualScaling + + ", basicScaling=" + basicScaling + + ", sslEnabled=" + sslEnabled + + ", useSessions=" + useSessions + + ", asyncSessionPersistence=" + asyncSessionPersistence + + ", asyncSessionPersistenceQueueName='" + asyncSessionPersistenceQueueName + '\'' + + ", staticFileIncludes=" + staticFileIncludes + + ", staticFileExcludes=" + staticFileExcludes + + ", resourceFileIncludes=" + resourceFileIncludes + + ", resourceFileExcludes=" + resourceFileExcludes + + ", staticIncludePattern=" + staticIncludePattern + + ", staticExcludePattern=" + staticExcludePattern + + ", resourceIncludePattern=" + resourceIncludePattern + + ", resourceExcludePattern=" + resourceExcludePattern + + ", publicRoot='" + publicRoot + '\'' + + ", appRoot='" + appRoot + '\'' + + ", inboundServices=" + inboundServices + + ", precompilationEnabled=" + precompilationEnabled + + ", adminConsolePages=" + adminConsolePages + + ", errorHandlers=" + errorHandlers + + ", threadsafe=" + threadsafe + + ", threadsafeValueProvided=" + threadsafeValueProvided + + ", autoIdPolicy=" + autoIdPolicy + + ", codeLock=" + codeLock + + ", apiConfig=" + apiConfig + + ", apiEndpointIds=" + apiEndpointIds + + ", pagespeed=" + pagespeed + + ", classLoaderConfig=" + classLoaderConfig + + ", urlStreamHandlerType=" + + (urlStreamHandlerType == null ? URL_HANDLER_URLFETCH : urlStreamHandlerType) + + ", useGoogleConnectorJ=" + useGoogleConnectorJ + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AppEngineWebXml that = (AppEngineWebXml) o; + + if (asyncSessionPersistence != that.asyncSessionPersistence) { + return false; + } + if (precompilationEnabled != that.precompilationEnabled) { + return false; + } + if (sslEnabled != that.sslEnabled) { + return false; + } + if (threadsafe != that.threadsafe) { + return false; + } + if (threadsafeValueProvided != that.threadsafeValueProvided) { + return false; + } + if (autoIdPolicy != null ? !autoIdPolicy.equals(that.autoIdPolicy) + : that.autoIdPolicy != null) { + return false; + } + if (codeLock != that.codeLock) { + return false; + } + if (useSessions != that.useSessions) { + return false; + } + if (adminConsolePages != null ? !adminConsolePages.equals(that.adminConsolePages) + : that.adminConsolePages != null) { + return false; + } + if (appId != null ? !appId.equals(that.appId) : that.appId != null) { + return false; + } + if (majorVersionId != null ? !majorVersionId.equals(that.majorVersionId) + : that.majorVersionId != null) { + return false; + } + if (module != null ? !module.equals(that.module) : that.module != null) { + return false; + } + if (instanceClass != null ? !instanceClass.equals(that.instanceClass) + : that.instanceClass != null) { + return false; + } + if (!automaticScaling.equals(that.automaticScaling)) { + return false; + } + if (!manualScaling.equals(that.manualScaling)) { + return false; + } + if (!basicScaling.equals(that.basicScaling)) { + return false; + } + if (appRoot != null ? !appRoot.equals(that.appRoot) : that.appRoot != null) { + return false; + } + if (asyncSessionPersistenceQueueName != null ? !asyncSessionPersistenceQueueName + .equals(that.asyncSessionPersistenceQueueName) + : that.asyncSessionPersistenceQueueName != null) { + return false; + } + if (envVariables != null ? !envVariables.equals(that.envVariables) + : that.envVariables != null) { + return false; + } + if (errorHandlers != null ? !errorHandlers.equals(that.errorHandlers) + : that.errorHandlers != null) { + return false; + } + if (inboundServices != null ? !inboundServices.equals(that.inboundServices) + : that.inboundServices != null) { + return false; + } + if (majorVersionId != null ? !majorVersionId.equals(that.majorVersionId) + : that.majorVersionId != null) { + return false; + } + if (sourceLanguage != null ? !sourceLanguage.equals(that.sourceLanguage) + : that.sourceLanguage != null) { + return false; + } + if (publicRoot != null ? !publicRoot.equals(that.publicRoot) : that.publicRoot != null) { + return false; + } + if (resourceExcludePattern != null ? !resourceExcludePattern.equals(that.resourceExcludePattern) + : that.resourceExcludePattern != null) { + return false; + } + if (resourceFileExcludes != null ? !resourceFileExcludes.equals(that.resourceFileExcludes) + : that.resourceFileExcludes != null) { + return false; + } + if (resourceFileIncludes != null ? !resourceFileIncludes.equals(that.resourceFileIncludes) + : that.resourceFileIncludes != null) { + return false; + } + if (resourceIncludePattern != null ? !resourceIncludePattern.equals(that.resourceIncludePattern) + : that.resourceIncludePattern != null) { + return false; + } + if (staticExcludePattern != null ? !staticExcludePattern.equals(that.staticExcludePattern) + : that.staticExcludePattern != null) { + return false; + } + if (staticFileExcludes != null ? !staticFileExcludes.equals(that.staticFileExcludes) + : that.staticFileExcludes != null) { + return false; + } + if (staticFileIncludes != null ? !staticFileIncludes.equals(that.staticFileIncludes) + : that.staticFileIncludes != null) { + return false; + } + if (staticIncludePattern != null ? !staticIncludePattern.equals(that.staticIncludePattern) + : that.staticIncludePattern != null) { + return false; + } + if (systemProperties != null ? !systemProperties.equals(that.systemProperties) + : that.systemProperties != null) { + return false; + } + if (vmSettings != null ? !vmSettings.equals(that.vmSettings) : that.vmSettings != null) { + return false; + } + + if (userPermissions != null ? !userPermissions.equals(that.userPermissions) + : that.userPermissions != null) { + return false; + } + if (apiConfig != null ? !apiConfig.equals(that.apiConfig) + : that.apiConfig != null) { + return false; + } + if (apiEndpointIds != null ? !apiEndpointIds.equals(that.apiEndpointIds) + : that.apiEndpointIds != null) { + return false; + } + if (pagespeed != null ? !pagespeed.equals(that.pagespeed) : that.pagespeed != null) { + return false; + } + if (classLoaderConfig != null ? !classLoaderConfig.equals(that.classLoaderConfig) : + that.classLoaderConfig != null) { + return false; + } + if (urlStreamHandlerType != null ? !urlStreamHandlerType.equals(that.urlStreamHandlerType) : + that.urlStreamHandlerType != null) { + return false; + } + if (useGoogleConnectorJ != that.useGoogleConnectorJ) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = systemProperties != null ? systemProperties.hashCode() : 0; + result = 31 * result + (envVariables != null ? envVariables.hashCode() : 0); + result = 31 * result + (userPermissions != null ? userPermissions.hashCode() : 0); + result = 31 * result + (appId != null ? appId.hashCode() : 0); + result = 31 * result + (majorVersionId != null ? majorVersionId.hashCode() : 0); + result = 31 * result + (sourceLanguage != null ? sourceLanguage.hashCode() : 0); + result = 31 * result + (module != null ? module.hashCode() : 0); + result = 31 * result + (instanceClass != null ? instanceClass.hashCode() : 0); + result = 31 * result + automaticScaling.hashCode(); + result = 31 * result + manualScaling.hashCode(); + result = 31 * result + basicScaling.hashCode(); + result = 31 * result + (sslEnabled ? 1 : 0); + result = 31 * result + (useSessions ? 1 : 0); + result = 31 * result + (asyncSessionPersistence ? 1 : 0); + result = + 31 * result + (asyncSessionPersistenceQueueName != null ? asyncSessionPersistenceQueueName + .hashCode() : 0); + result = 31 * result + (staticFileIncludes != null ? staticFileIncludes.hashCode() : 0); + result = 31 * result + (staticFileExcludes != null ? staticFileExcludes.hashCode() : 0); + result = 31 * result + (resourceFileIncludes != null ? resourceFileIncludes.hashCode() : 0); + result = 31 * result + (resourceFileExcludes != null ? resourceFileExcludes.hashCode() : 0); + result = 31 * result + (staticIncludePattern != null ? staticIncludePattern.hashCode() : 0); + result = 31 * result + (staticExcludePattern != null ? staticExcludePattern.hashCode() : 0); + result = 31 * result + (resourceIncludePattern != null ? resourceIncludePattern.hashCode() : 0); + result = 31 * result + (resourceExcludePattern != null ? resourceExcludePattern.hashCode() : 0); + result = 31 * result + (publicRoot != null ? publicRoot.hashCode() : 0); + result = 31 * result + (appRoot != null ? appRoot.hashCode() : 0); + result = 31 * result + (inboundServices != null ? inboundServices.hashCode() : 0); + result = 31 * result + (precompilationEnabled ? 1 : 0); + result = 31 * result + (adminConsolePages != null ? adminConsolePages.hashCode() : 0); + result = 31 * result + (errorHandlers != null ? errorHandlers.hashCode() : 0); + result = 31 * result + (threadsafe ? 1 : 0); + result = 31 * result + (autoIdPolicy != null ? autoIdPolicy.hashCode() : 0); + result = 31 * result + (threadsafeValueProvided ? 1 : 0); + result = 31 * result + (codeLock ? 1 : 0); + result = 31 * result + (apiConfig != null ? apiConfig.hashCode() : 0); + result = 31 * result + (apiEndpointIds != null ? apiEndpointIds.hashCode() : 0); + result = 31 * result + (pagespeed != null ? pagespeed.hashCode() : 0); + result = 31 * result + (classLoaderConfig != null ? classLoaderConfig.hashCode() : 0); + result = 31 * result + (urlStreamHandlerType != null ? urlStreamHandlerType.hashCode() : 0); + result = 31 * result + (useGoogleConnectorJ.hashCode()); + result = 31 * result + (vmSettings != null ? vmSettings.hashCode() : 0); + return result; + } + + public boolean includesResource(String path) { + if (resourceIncludePattern == null) { + if (resourceFileIncludes.size() == 0) { + resourceIncludePattern = Pattern.compile(".*"); + } else { + resourceIncludePattern = Pattern.compile(makeRegexp(resourceFileIncludes)); + } + } + if (resourceExcludePattern == null && resourceFileExcludes.size() > 0) { + resourceExcludePattern = Pattern.compile(makeRegexp(resourceFileExcludes)); + } else { + } + return includes(path, resourceIncludePattern, resourceExcludePattern); + } + + public boolean includesStatic(String path) { + if (staticIncludePattern == null) { + if (staticFileIncludes.size() == 0) { + String staticRoot; + if (publicRoot.length() > 0) { + staticRoot = publicRoot + "/**"; + } else { + staticRoot = "**"; + } + staticIncludePattern = Pattern.compile( + makeRegexp(Collections.singletonList(staticRoot))); + } else { + List patterns = new ArrayList(); + for (StaticFileInclude include : staticFileIncludes) { + patterns.add(include.getPattern()); + } + staticIncludePattern = Pattern.compile(makeRegexp(patterns)); + } + } + if (staticExcludePattern == null && staticFileExcludes.size() > 0) { + staticExcludePattern = Pattern.compile(makeRegexp(staticFileExcludes)); + } else { + } + return includes(path, staticIncludePattern, staticExcludePattern); + } + + /** + * Tests whether {@code path} is covered by the pattern {@code includes} + * while not being blocked by matching {@code excludes}. + * + * @param path a URL to test + * @param includes a non-{@code null} pattern for included URLs + * @param excludes a pattern for exclusion, or {@code null} to not exclude + * anything from the {@code includes} set. + */ + public boolean includes(String path, Pattern includes, Pattern excludes) { + assert(includes != null); + if (!includes.matcher(path).matches()) { + return false; + } + if (excludes != null && excludes.matcher(path).matches()) { + return false; + } + return true; + } + + public String makeRegexp(List patterns) { + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String item : patterns) { + if (first) { + first = false; + } else { + builder.append('|'); + } + + while (item.charAt(0) == '/') { + item = item.substring(1); + } + + builder.append('('); + if (appRoot != null) { + builder.append(makeFileRegex(appRoot)); + } + builder.append("/"); + builder.append(makeFileRegex(item)); + builder.append(')'); + } + return builder.toString(); + } + + /** + * Helper method to translate from appengine-web.xml "file globs" to + * proper regular expressions as used in app.yaml. + * + * @param fileGlob the glob to translate + * @return the regular expression string matching the input {@code file} pattern. + */ + static String makeFileRegex(String fileGlob) { + fileGlob = fileGlob.replaceAll("([^A-Za-z0-9\\-_/])", "\\\\$1"); + fileGlob = fileGlob.replaceAll("\\\\\\*\\\\\\*", ".*"); + fileGlob = fileGlob.replaceAll("\\\\\\*", "[^/]*"); + return fileGlob; + } + /** + * Sets the application root directory, as a prefix for the regexps in + * {@link #includeResourcePattern(String)} and friends. This is needed + * because we want to match complete filenames relative to root. + * + * @param appRoot + */ + public void setSourcePrefix(String appRoot) { + this.appRoot = appRoot; + this.resourceIncludePattern = null; + this.resourceExcludePattern = null; + this.staticIncludePattern = null; + this.staticExcludePattern = null; + } + + public String getSourcePrefix() { + return this.appRoot; + } + + /** + * Represents a {@link java.security.Permission} that needs to be + * granted to user code. + */ + private static class UserPermission { + private final String className; + private final String name; + private final String actions; + + private boolean hasHashCode = false; + private int hashCode; + + public UserPermission(String className, String name, String actions) { + this.className = className; + this.name = name; + this.actions = actions; + } + + public String getClassName() { + return className; + } + + public String getName() { + return name; + } + + public String getActions() { + return actions; + } + + @Override + public int hashCode() { + if (hasHashCode) { + return hashCode; + } + + int hash = className.hashCode(); + hash = 31 * hash + name.hashCode(); + if (actions != null) { + hash = 31 * hash + actions.hashCode(); + } + + hashCode = hash; + hasHashCode = true; + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof UserPermission) { + UserPermission perm = (UserPermission) obj; + if (className.equals(perm.className) && name.equals(perm.name)) { + if (actions == null ? perm.actions == null : actions.equals(perm.actions)) { + return true; + } + } + } + return false; + } + } + + /** + * Represents an <include> element within the + * <static-files> element. Currently this includes both a + * pattern and an optional expiration time specification. + */ + public static class StaticFileInclude { + private final String pattern; + private final String expiration; + private final Map httpHeaders; + + public StaticFileInclude(String pattern, String expiration) { + this.pattern = pattern; + this.expiration = expiration; + this.httpHeaders = new HashMap(); + } + + public String getPattern() { + return pattern; + } + + public Pattern getRegularExpression() { + return Pattern.compile(makeFileRegex(pattern)); + } + + public String getExpiration() { + return expiration; + } + + public Map getHttpHeaders() { + return httpHeaders; + } + + @Override + public int hashCode() { + return Objects.hashCode(pattern, expiration, httpHeaders); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StaticFileInclude)) { + return false; + } + + StaticFileInclude other = (StaticFileInclude) obj; + + if (pattern != null) { + if (!pattern.equals(other.pattern)) { + return false; + } + } else { + if (other.pattern != null) { + return false; + } + } + + if (expiration != null) { + if (!expiration.equals(other.expiration)) { + return false; + } + } else { + if (other.expiration != null) { + return false; + } + } + + if (httpHeaders != null) { + if (!httpHeaders.equals(other.httpHeaders)) { + return false; + } + } else { + if (other.httpHeaders != null) { + return false; + } + } + + return true; + } + } + + public static class AdminConsolePage { + private final String name; + private final String url; + + public AdminConsolePage(String name, String url) { + this.name = name; + this.url = url; + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AdminConsolePage other = (AdminConsolePage) obj; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + if (url == null) { + if (other.url != null) return false; + } else if (!url.equals(other.url)) return false; + return true; + } + } + + /** + * Represents an <error-handler> element. Currently this includes both + * a file name and an optional error code. + */ + public static class ErrorHandler { + private final String file; + private final String errorCode; + + public ErrorHandler(String file, String errorCode) { + this.file = file; + this.errorCode = errorCode; + } + + public String getFile() { + return file; + } + + public String getErrorCode() { + return errorCode; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((file == null) ? 0 : file.hashCode()); + result = prime * result + + ((errorCode == null) ? 0 : errorCode.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ErrorHandler)) { + return false; + } + + ErrorHandler handler = (ErrorHandler) obj; + if (!file.equals(handler.file)) { + return false; + } + + if (errorCode == null) { + if (handler.errorCode != null) { + return false; + } + } else { + if (!errorCode.equals(handler.errorCode)) { + return false; + } + } + + return true; + } + } + + /** + * Represents an <api-config> element. This is a singleton specifying + * url-pattern and servlet-class for the api config server. + */ + public static class ApiConfig { + private final String servletClass; + private final String url; + + public ApiConfig(String servletClass, String url) { + this.servletClass = servletClass; + this.url = url; + } + + public String getservletClass() { + return servletClass; + } + + public String getUrl() { + return url; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((servletClass == null) ? 0 : servletClass.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { return true; } + if (obj == null) { return false; } + if (getClass() != obj.getClass()) { return false; } + ApiConfig other = (ApiConfig) obj; + if (servletClass == null) { + if (other.servletClass != null) { return false; } + } else if (!servletClass.equals(other.servletClass)) { return false; } + if (url == null) { + if (other.url != null) { return false; } + } else if (!url.equals(other.url)) { return false; } + return true; + } + + @Override + public String toString() { + return "ApiConfig{servletClass=\"" + servletClass + "\", url=\"" + url + "\"}"; + } + } + + /** + * Holder for automatic settings. + */ + public static class AutomaticScaling { + private static final AutomaticScaling EMPTY_SETTINGS = new AutomaticScaling(); + + public static final String AUTOMATIC = "automatic"; + private String minPendingLatency; + private String maxPendingLatency; + private String minIdleInstances; + private String maxIdleInstances; + private String maxConcurrentRequests; + + public String getMinPendingLatency() { + return minPendingLatency; + } + + /** + * Sets minPendingLatency. Normalizes empty and null inputs to null. + */ + public void setMinPendingLatency(String minPendingLatency) { + this.minPendingLatency = StringUtil.toNullIfEmptyOrWhitespace(minPendingLatency); + } + + public String getMaxPendingLatency() { + return maxPendingLatency; + } + + /** + * Sets maxPendingLatency. Normalizes empty and null inputs to null. + */ + public void setMaxPendingLatency(String maxPendingLatency) { + this.maxPendingLatency = StringUtil.toNullIfEmptyOrWhitespace(maxPendingLatency); + } + + public String getMinIdleInstances() { + return minIdleInstances; + } + + /** + * Sets minIdleInstances. Normalizes empty and null inputs to null. + */ + public void setMinIdleInstances(String minIdleInstances) { + this.minIdleInstances = StringUtil.toNullIfEmptyOrWhitespace(minIdleInstances); + } + + public String getMaxIdleInstances() { + return maxIdleInstances; + } + + /** + * Sets maxIdleInstances. Normalizes empty and null inputs to null. + */ + public void setMaxIdleInstances(String maxIdleInstances) { + this.maxIdleInstances = StringUtil.toNullIfEmptyOrWhitespace(maxIdleInstances); + } + + public boolean isEmpty() { + return this.equals(EMPTY_SETTINGS); + } + + public String getMaxConcurrentRequests() { + return maxConcurrentRequests; + } + + /** + * Sets maxConcurrentRequests. Normalizes empty and null inputs to null. + */ + public void setMaxConcurrentRequests(String maxConcurrentRequests) { + this.maxConcurrentRequests = StringUtil.toNullIfEmptyOrWhitespace(maxConcurrentRequests); + } + + @Override + public int hashCode() { + return Objects.hashCode(maxPendingLatency, minPendingLatency, + maxIdleInstances, minIdleInstances, maxConcurrentRequests); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AutomaticScaling other = (AutomaticScaling) obj; + return Objects.equal(maxPendingLatency, other.maxPendingLatency) + && Objects.equal(minPendingLatency, other.minPendingLatency) + && Objects.equal(maxIdleInstances, other.maxIdleInstances) + && Objects.equal(minIdleInstances, other.minIdleInstances) + && Objects.equal(maxConcurrentRequests, other.maxConcurrentRequests); + } + + @Override + public String toString() { + return "AutomaticScaling [minPendingLatency=" + minPendingLatency + + ", maxPendingLatency=" + maxPendingLatency + + ", minIdleInstances=" + minIdleInstances + + ", maxIdleInstances=" + maxIdleInstances + + ", maxConcurrentRequests=" + maxConcurrentRequests + "]"; + } + } + + /** + * Holder for manual settings. + */ + public static class ManualScaling { + private static final ManualScaling EMPTY_SETTINGS = new ManualScaling(); + + private String instances; + + public String getInstances() { + return instances; + } + + /** + * Sets instances. Normalizes empty and null inputs to null. + */ + public void setInstances(String instances) { + this.instances = StringUtil.toNullIfEmptyOrWhitespace(instances); + } + + public boolean isEmpty() { + return this.equals(EMPTY_SETTINGS); + } + + @Override + public int hashCode() { + return Objects.hashCode(instances); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ManualScaling other = (ManualScaling) obj; + return Objects.equal(instances, other.instances); + } + + @Override + public String toString() { + return "ManualScaling [" + "instances=" + instances + "]"; + } + } + + /** + * Holder for basic settings. + */ + public static class BasicScaling { + private static final BasicScaling EMPTY_SETTINGS = new BasicScaling(); + + private String maxInstances; + private String idleTimeout; + + public String getMaxInstances() { + return maxInstances; + } + + public String getIdleTimeout() { + return idleTimeout; + } + + /** + * Sets maxInstances. Normalizes empty and null inputs to null. + */ + public void setMaxInstances(String maxInstances) { + this.maxInstances = StringUtil.toNullIfEmptyOrWhitespace(maxInstances); + } + + /** + * Sets idleTimeout. Normalizes empty and null inputs to null. + */ + public void setIdleTimeout(String idleTimeout) { + this.idleTimeout = StringUtil.toNullIfEmptyOrWhitespace(idleTimeout); + } + + public boolean isEmpty() { + return this.equals(EMPTY_SETTINGS); + } + + @Override + public int hashCode() { + return Objects.hashCode(maxInstances, idleTimeout); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + BasicScaling other = (BasicScaling) obj; + return Objects.equal(maxInstances, other.maxInstances) + && Objects.equal(idleTimeout, other.idleTimeout); + } + + @Override + public String toString() { + return "BasicScaling [" + "maxInstances=" + maxInstances + + ", idleTimeout=" + idleTimeout + "]"; + } + } + + /** + * Represents a <pagespeed> element. This is used to specify configuration for the Page + * Speed Service, which can be used to automatically optimize the loading speed of app engine + * sites. + */ + public static class Pagespeed { + private final List urlBlacklist = Lists.newArrayList(); + private final List domainsToRewrite = Lists.newArrayList(); + private final List enabledRewriters = Lists.newArrayList(); + private final List disabledRewriters = Lists.newArrayList(); + + public void addUrlBlacklist(String url) { + urlBlacklist.add(url); + } + + public List getUrlBlacklist() { + return Collections.unmodifiableList(urlBlacklist); + } + + public void addDomainToRewrite(String domain) { + domainsToRewrite.add(domain); + } + + public List getDomainsToRewrite() { + return Collections.unmodifiableList(domainsToRewrite); + } + + public void addEnabledRewriter(String rewriter) { + enabledRewriters.add(rewriter); + } + + public List getEnabledRewriters() { + return Collections.unmodifiableList(enabledRewriters); + } + + public void addDisabledRewriter(String rewriter) { + disabledRewriters.add(rewriter); + } + + public List getDisabledRewriters() { + return Collections.unmodifiableList(disabledRewriters); + } + + public boolean isEmpty() { + return urlBlacklist.isEmpty() && domainsToRewrite.isEmpty() && enabledRewriters.isEmpty() + && disabledRewriters.isEmpty(); + } + + @Override + public int hashCode() { + return Objects.hashCode(urlBlacklist, domainsToRewrite, enabledRewriters, disabledRewriters); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Pagespeed other = (Pagespeed) obj; + return Objects.equal(urlBlacklist, other.urlBlacklist) + && Objects.equal(domainsToRewrite, other.domainsToRewrite) + && Objects.equal(enabledRewriters, other.enabledRewriters) + && Objects.equal(disabledRewriters, other.disabledRewriters); + } + + @Override + public String toString() { + return "Pagespeed [urlBlacklist=" + urlBlacklist + ", domainsToRewrite=" + domainsToRewrite + + ", enabledRewriters=" + enabledRewriters + ", disabledRewriters=" + disabledRewriters + + "]"; + } + } + + public static class ClassLoaderConfig { + private final List entries = Lists.newArrayList(); + + public void add(PrioritySpecifierEntry entry) { + entries.add(entry); + } + + public List getEntries() { + return entries; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((entries == null) ? 0 : entries.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ClassLoaderConfig other = (ClassLoaderConfig) obj; + if (entries == null) { + if (other.entries != null) return false; + } else if (!entries.equals(other.entries)) return false; + return true; + } + + @Override + public String toString() { + return "ClassLoaderConfig{entries=\"" + entries + "\"}"; + } + } + + public static class PrioritySpecifierEntry { + private String filename; + private Double priority; + + private void checkNotAlreadySet() { + if (filename != null) { + throw new AppEngineConfigException("Found more that one file name matching tag. " + + "Only one of 'filename' attribute allowed."); + } + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + checkNotAlreadySet(); + this.filename = filename; + } + + public Double getPriority() { + return priority; + } + + public double getPriorityValue() { + if (priority == null) { + return 1.0d; + } + return priority; + } + + public void setPriority(String priority) { + if (this.priority != null) { + throw new AppEngineConfigException("The 'priority' tag may only be specified once."); + } + + if (priority == null) { + this.priority = null; + return; + } + + this.priority = Double.parseDouble(priority); + } + + public void checkClassLoaderConfig() { + if (filename == null) { + throw new AppEngineConfigException("Must have a filename attribute."); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((filename == null) ? 0 : filename.hashCode()); + result = prime * result + ((priority == null) ? 0 : priority.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + PrioritySpecifierEntry other = (PrioritySpecifierEntry) obj; + if (filename == null) { + if (other.filename != null) return false; + } else if (!filename.equals(other.filename)) return false; + if (priority == null) { + if (other.priority != null) return false; + } else if (!priority.equals(other.priority)) return false; + return true; + } + + @Override + public String toString() { + return "PrioritySpecifierEntry{filename=\"" + filename + "\", priority=\"" + priority + "\"}"; + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlProcessor.java b/java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlProcessor.java new file mode 100644 index 00000000..f288d714 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlProcessor.java @@ -0,0 +1,490 @@ +// Copyright 2008 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +import com.google.apphosting.utils.config.AppEngineWebXml.AdminConsolePage; +import com.google.apphosting.utils.config.AppEngineWebXml.ApiConfig; +import com.google.apphosting.utils.config.AppEngineWebXml.AutomaticScaling; +import com.google.apphosting.utils.config.AppEngineWebXml.BasicScaling; +import com.google.apphosting.utils.config.AppEngineWebXml.ClassLoaderConfig; +import com.google.apphosting.utils.config.AppEngineWebXml.ErrorHandler; +import com.google.apphosting.utils.config.AppEngineWebXml.ManualScaling; +import com.google.apphosting.utils.config.AppEngineWebXml.Pagespeed; +import com.google.apphosting.utils.config.AppEngineWebXml.PrioritySpecifierEntry; + +import org.mortbay.xml.XmlParser; +import org.mortbay.xml.XmlParser.Node; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Constructs an {@link AppEngineWebXml} from an xml document corresponding to + * appengine-web.xsd. We use Jetty's {@link XmlParser} utility for + * convenience. + * + * @TODO(user): Add a real link to the xsd once it exists and do schema + * validation. + * + */ +class AppEngineWebXmlProcessor { + + enum FileType { STATIC, RESOURCE } + + private static final Logger logger = Logger.getLogger(AppEngineWebXmlProcessor.class.getName()); + + /** + * Construct an {@link AppEngineWebXml} from the xml document + * identified by the provided {@link InputStream}. + * + * @param is The InputStream containing the xml we want to parse and process. + * + * @return Object representation of the xml document. + * @throws AppEngineConfigException If the input stream cannot be parsed. + */ + public AppEngineWebXml processXml(InputStream is) { + XmlParser.Node config = getTopLevelNode(is); + AppEngineWebXml appEngineWebXml = new AppEngineWebXml(); + appEngineWebXml.setWarmupRequestsEnabled(true); + for (Object o : config) { + if (!(o instanceof XmlParser.Node)) { + continue; + } + XmlParser.Node node = (XmlParser.Node) o; + processSecondLevelNode(node, appEngineWebXml); + } + checkScalingConstraints(appEngineWebXml); + return appEngineWebXml; + } + + /** + * Given an AppEngineWebXml, ensure it has no more than one of the scaling options available. + * + * @throws AppEngineConfigException If there is more than one scaling option selected. + */ + private static void checkScalingConstraints(AppEngineWebXml appEngineWebXml) { + int count = appEngineWebXml.getManualScaling().isEmpty() ? 0 : 1; + count += appEngineWebXml.getBasicScaling().isEmpty() ? 0 : 1; + count += appEngineWebXml.getAutomaticScaling().isEmpty() ? 0 : 1; + if (count > 1) { + throw new AppEngineConfigException( + "There may be only one of 'automatic-scaling', 'manual-scaling' or " + + "'basic-scaling' elements."); + } + } + + /** + * Given an InputStream, create a Node corresponding to the top level xml + * element. + * + * @throws AppEngineConfigException If the input stream cannot be parsed. + */ + XmlParser.Node getTopLevelNode(InputStream is) { + XmlParser xmlParser = new XmlParser(); + try { + return xmlParser.parse(is); + } catch (IOException e) { + String msg = "Received IOException parsing the input stream."; + logger.log(Level.SEVERE, msg, e); + throw new AppEngineConfigException(msg, e); + } catch (SAXException e) { + String msg = "Received SAXException parsing the input stream."; + logger.log(Level.SEVERE, msg, e); + throw new AppEngineConfigException(msg, e); + } + } + + private void processSecondLevelNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + String elementName = node.getTag(); + if (elementName.equals("system-properties")) { + processSystemPropertiesNode(node, appEngineWebXml); + } else if (elementName.equals("vm-settings")) { + processVmSettingsNode(node, appEngineWebXml); + } else if (elementName.equals("env-variables")) { + processEnvironmentVariablesNode(node, appEngineWebXml); + } else if (elementName.equals("application")) { + processApplicationNode(node, appEngineWebXml); + } else if (elementName.equals("version")) { + processVersionNode(node, appEngineWebXml); + } else if (elementName.equals("source-language")) { + processSourceLanguageNode(node, appEngineWebXml); + } else if (elementName.equals("module")) { + processModuleNode(node, appEngineWebXml); + } else if (elementName.equals("instance-class")) { + processInstanceClassNode(node, appEngineWebXml); + } else if (elementName.equals("automatic-scaling")) { + processAutomaticScalingNode(node, appEngineWebXml); + } else if (elementName.equals("manual-scaling")) { + processManualScalingNode(node, appEngineWebXml); + } else if (elementName.equals("basic-scaling")) { + processBasicScalingNode(node, appEngineWebXml); + } else if (elementName.equals("static-files")) { + processFilesetNode(node, appEngineWebXml, FileType.STATIC); + } else if (elementName.equals("resource-files")) { + processFilesetNode(node, appEngineWebXml, FileType.RESOURCE); + } else if (elementName.equals("ssl-enabled")) { + processSslEnabledNode(node, appEngineWebXml); + } else if (elementName.equals("sessions-enabled")) { + processSessionsEnabledNode(node, appEngineWebXml); + } else if (elementName.equals("async-session-persistence")) { + processAsyncSessionPersistenceNode(node, appEngineWebXml); + } else if (elementName.equals("user-permissions")) { + processPermissionsNode(node, appEngineWebXml); + } else if (elementName.equals("public-root")) { + processPublicRootNode(node, appEngineWebXml); + } else if (elementName.equals("inbound-services")) { + processInboundServicesNode(node, appEngineWebXml); + } else if (elementName.equals("precompilation-enabled")) { + processPrecompilationEnabledNode(node, appEngineWebXml); + } else if (elementName.equals("admin-console")) { + processAdminConsoleNode(node, appEngineWebXml); + } else if (elementName.equals("static-error-handlers")) { + processErrorHandlerNode(node, appEngineWebXml); + } else if (elementName.equals("warmup-requests-enabled")) { + processWarmupRequestsEnabledNode(node, appEngineWebXml); + } else if (elementName.equals("threadsafe")) { + processThreadsafeNode(node, appEngineWebXml); + } else if (elementName.equals("auto-id-policy")) { + processAutoIdPolicyNode(node, appEngineWebXml); + } else if (elementName.equals("code-lock")) { + processCodeLockNode(node, appEngineWebXml); + } else if (elementName.equals("vm")) { + processVmNode(node, appEngineWebXml); + } else if (elementName.equals("api-config")) { + processApiConfigNode(node, appEngineWebXml); + } else if (elementName.equals("pagespeed")) { + processPagespeedNode(node, appEngineWebXml); + } else if (elementName.equals("class-loader-config")) { + processClassLoaderConfig(node, appEngineWebXml); + } else if (elementName.equals("url-stream-handler")) { + processUrlStreamHandler(node, appEngineWebXml); + } else if (elementName.equals("use-google-connector-j")) { + processUseGoogleConnectorJNode(node, appEngineWebXml); + } else { + throw new AppEngineConfigException("Unrecognized element <" + elementName + ">"); + } + } + + private void processApplicationNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setAppId(getTextNode(node)); + } + + private void processPublicRootNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setPublicRoot(getTextNode(node)); + } + + private void processVersionNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setMajorVersionId(getTextNode(node)); + } + + private void processSourceLanguageNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setSourceLanguage(getTextNode(node)); + } + + private void processModuleNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setModule(getTextNode(node)); + } + + private void processInstanceClassNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setInstanceClass(getTextNode(node)); + } + + private String getChildNodeText(XmlParser.Node parentNode, String childTag) { + String result = null; + XmlParser.Node node = parentNode.get(childTag); + if (node != null) { + result = (String) node.get(0); + } + return result; + } + + private void processAutomaticScalingNode(XmlParser.Node settingsNode, + AppEngineWebXml appEngineWebXml) { + AutomaticScaling automaticScaling = appEngineWebXml.getAutomaticScaling(); + automaticScaling.setMinPendingLatency(getChildNodeText(settingsNode, "min-pending-latency")); + automaticScaling.setMaxPendingLatency(getChildNodeText(settingsNode, "max-pending-latency")); + automaticScaling.setMinIdleInstances(getChildNodeText(settingsNode, "min-idle-instances")); + automaticScaling.setMaxIdleInstances(getChildNodeText(settingsNode, "max-idle-instances")); + automaticScaling.setMaxConcurrentRequests(getChildNodeText(settingsNode, + "max-concurrent-requests")); + } + + private void processManualScalingNode(XmlParser.Node settingsNode, + AppEngineWebXml appEngineWebXml) { + ManualScaling manualScaling = appEngineWebXml.getManualScaling(); + manualScaling.setInstances(getChildNodeText(settingsNode, "instances")); + } + + private void processBasicScalingNode(XmlParser.Node settingsNode, + AppEngineWebXml appEngineWebXml) { + BasicScaling basicScaling = appEngineWebXml.getBasicScaling(); + basicScaling.setMaxInstances(getChildNodeText(settingsNode, "max-instances")); + basicScaling.setIdleTimeout(getChildNodeText(settingsNode, "idle-timeout")); + } + + private void processSslEnabledNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setSslEnabled(getBooleanValue(node)); + } + + private void processSessionsEnabledNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setSessionsEnabled(getBooleanValue(node)); + } + + private void processAsyncSessionPersistenceNode( + XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + boolean enabled = getBooleanAttributeValue(node, "enabled"); + appEngineWebXml.setAsyncSessionPersistence(enabled); + String queueName = trim(node.getAttribute("queue-name")); + appEngineWebXml.setAsyncSessionPersistenceQueueName(queueName); + } + + private void processPrecompilationEnabledNode(XmlParser.Node node, + AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setPrecompilationEnabled(getBooleanValue(node)); + } + + private void processWarmupRequestsEnabledNode(XmlParser.Node node, + AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setWarmupRequestsEnabled(getBooleanValue(node)); + } + + private void processThreadsafeNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setThreadsafe(getBooleanValue(node)); + } + + private void processAutoIdPolicyNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setAutoIdPolicy(getTextNode(node)); + } + + private void processCodeLockNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setCodeLock(getBooleanValue(node)); + } + + private void processVmNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setUseVm(getBooleanValue(node)); + } + + private void processFilesetNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml, + FileType type) { + Iterator nodeIter = getNodeIterator(node, "include"); + while (nodeIter.hasNext()) { + XmlParser.Node includeNode = nodeIter.next(); + String path = trim(includeNode.getAttribute("path")); + if (type == FileType.STATIC) { + String expiration = trim(includeNode.getAttribute("expiration")); + AppEngineWebXml.StaticFileInclude staticFileInclude = + appEngineWebXml.includeStaticPattern(path, expiration); + + Map httpHeaders = staticFileInclude.getHttpHeaders(); + Iterator httpHeaderIter = getNodeIterator(includeNode, "http-header"); + while (httpHeaderIter.hasNext()) { + XmlParser.Node httpHeaderNode = httpHeaderIter.next(); + String name = httpHeaderNode.getAttribute("name"); + String value = httpHeaderNode.getAttribute("value"); + + if (httpHeaders.containsKey(name)) { + throw new AppEngineConfigException("Two http-header elements have the same name."); + } + + httpHeaders.put(name, value); + } + } else { + appEngineWebXml.includeResourcePattern(path); + } + } + + nodeIter = getNodeIterator(node, "exclude"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String path = trim(subNode.getAttribute("path")); + if (type == FileType.STATIC) { + appEngineWebXml.excludeStaticPattern(path); + } else { + appEngineWebXml.excludeResourcePattern(path); + } + } + } + + private Iterator getNodeIterator(XmlParser.Node node, String filter) { + @SuppressWarnings("unchecked") + Iterator iterator = node.iterator(filter); + return iterator; + } + + private void processSystemPropertiesNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Iterator nodeIter = getNodeIterator(node, "property"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String propertyName = trim(subNode.getAttribute("name")); + String propertyValue = trim(subNode.getAttribute("value")); + appEngineWebXml.addSystemProperty(propertyName, propertyValue); + } + } + + private void processVmSettingsNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Iterator nodeIter = getNodeIterator(node, "setting"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String name = trim(subNode.getAttribute("name")); + String value = trim(subNode.getAttribute("value")); + appEngineWebXml.addVmSetting(name, value); + } + } + + private void processEnvironmentVariablesNode( + XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Iterator nodeIter = getNodeIterator(node, "env-var"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String propertyName = trim(subNode.getAttribute("name")); + String propertyValue = trim(subNode.getAttribute("value")); + appEngineWebXml.addEnvironmentVariable(propertyName, propertyValue); + } + } + + private void processPermissionsNode( + XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Iterator nodeIter = getNodeIterator(node, "permission"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String className = trim(subNode.getAttribute("class")); + String name = trim(subNode.getAttribute("name")); + String actions = trim(subNode.getAttribute("actions")); + appEngineWebXml.addUserPermission(className, name, actions); + } + } + + private void processInboundServicesNode( + XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Iterator nodeIter = getNodeIterator(node, "service"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String service = getTextNode(subNode); + appEngineWebXml.addInboundService(service); + } + } + + private void processAdminConsoleNode( + XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Iterator nodeIter = getNodeIterator(node, "page"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String name = trim(subNode.getAttribute("name")); + String url = trim(subNode.getAttribute("url")); + appEngineWebXml.addAdminConsolePage(new AdminConsolePage(name, url)); + } + } + + private void processErrorHandlerNode( + XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Iterator nodeIter = getNodeIterator(node, "handler"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String file = trim(subNode.getAttribute("file")); + String errorCode = trim(subNode.getAttribute("error-code")); + appEngineWebXml.addErrorHandler(new ErrorHandler(file, errorCode)); + } + } + + private void processApiConfigNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + String servlet = trim(node.getAttribute("servlet-class")); + String url = trim(node.getAttribute("url-pattern")); + appEngineWebXml.setApiConfig(new ApiConfig(servlet, url)); + + String id = null; + String endpoint = null; + Iterator subNodeIter = getNodeIterator(node, "endpoint-servlet-mapping-id"); + while (subNodeIter.hasNext()) { + XmlParser.Node subNode = subNodeIter.next(); + id = trim(getTextNode(subNode)); + if (id != null && id.length() > 0) { + appEngineWebXml.addApiEndpoint(id); + } + } + } + + private void processPagespeedNode(XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + Pagespeed pagespeed = new Pagespeed(); + Iterator nodeIter = getNodeIterator(node, "url-blacklist"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String urlMatcher = getTextNode(subNode); + pagespeed.addUrlBlacklist(urlMatcher); + } + nodeIter = getNodeIterator(node, "domain-to-rewrite"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String domain = getTextNode(subNode); + pagespeed.addDomainToRewrite(domain); + } + nodeIter = getNodeIterator(node, "enabled-rewriter"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String rewriter = getTextNode(subNode); + pagespeed.addEnabledRewriter(rewriter); + } + nodeIter = getNodeIterator(node, "disabled-rewriter"); + while (nodeIter.hasNext()) { + XmlParser.Node subNode = nodeIter.next(); + String rewriter = getTextNode(subNode); + pagespeed.addDisabledRewriter(rewriter); + } + appEngineWebXml.setPagespeed(pagespeed); + } + + private void processClassLoaderConfig( + XmlParser.Node node, AppEngineWebXml appEngineWebXml) { + ClassLoaderConfig config = new ClassLoaderConfig(); + appEngineWebXml.setClassLoaderConfig(config); + Iterator nodeIter = getNodeIterator(node, "priority-specifier"); + while (nodeIter.hasNext()) { + processClassPathPrioritySpecifier(nodeIter.next(), config); + } + } + + private void processClassPathPrioritySpecifier(Node node, ClassLoaderConfig config) { + PrioritySpecifierEntry entry = new PrioritySpecifierEntry(); + entry.setFilename(node.getAttribute("filename")); + entry.setPriority(node.getAttribute("priority")); + entry.checkClassLoaderConfig(); + config.add(entry); + } + + private void processUrlStreamHandler(Node node, AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setUrlStreamHandlerType(getTextNode(node)); + } + + private boolean getBooleanValue(XmlParser.Node node) { + return toBoolean(getTextNode(node)); + } + + private boolean getBooleanAttributeValue(XmlParser.Node node, String attribute) { + return toBoolean(node.getAttribute(attribute)); + } + + private boolean toBoolean(String value) { + value = value.trim(); + return (value.equalsIgnoreCase("true") || value.equals("1")); + } + + private String getTextNode(XmlParser.Node node) { + String value = (String) node.get(0); + if (value == null) { + value = ""; + } + return value.trim(); + } + + private String trim(String attribute) { + return attribute == null ? null : attribute.trim(); + } + + private void processUseGoogleConnectorJNode(XmlParser.Node node, + AppEngineWebXml appEngineWebXml) { + appEngineWebXml.setUseGoogleConnectorJ(getBooleanValue(node)); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlReader.java b/java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlReader.java new file mode 100644 index 00000000..728c1f0e --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AppEngineWebXmlReader.java @@ -0,0 +1,136 @@ +// Copyright 2008 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Creates an {@link AppEngineWebXml} instance from + * WEB-INF/appengine-web.xml. If you want to read the configuration + * from something that isn't a file, subclass and override + * {@link #getInputStream()}. + * + */ +public class AppEngineWebXmlReader { + private static final Logger logger = + Logger.getLogger(AppEngineWebXmlReader.class.getName()); + + private static final String CONCURRENT_REQUESTS_URL = + "http://code.google.com/appengine/docs/java/config/appconfig.html#Using_Concurrent_Requests"; + + private static final String DATASTORE_AUTO_IDS_URL = + "http://developers.google.com/appengine/docs/java/datastore/entities#Kinds_and_Identifiers"; + + private static final String APPCFG_AUTO_IDS_URL = + "http://developers.google.com/appengine/docs/java/config/appconfig#auto_id_policy"; + + public static final String DEFAULT_RELATIVE_FILENAME = "WEB-INF/appengine-web.xml"; + + private final String filename; + + /** + * Creates a reader for appengine-web.xml. + * + * @param appDir The directory in which the config file resides. + * @param relativeFilename The path to the config file, relative to + * {@code appDir}. + */ + public AppEngineWebXmlReader(String appDir, String relativeFilename) { + if (appDir.length() > 0 && appDir.charAt(appDir.length() - 1) != File.separatorChar) { + appDir += File.separatorChar; + } + this.filename = appDir + relativeFilename; + } + + /** + * Creates a reader for appengine-web.xml. + * + * @param appDir The directory in which the config file resides. The + * path to the config file relative to the directory is assumed to be + * {@link #DEFAULT_RELATIVE_FILENAME}. + */ + public AppEngineWebXmlReader(String appDir) { + this(appDir, DEFAULT_RELATIVE_FILENAME); + } + + /** + * @return A {@link AppEngineWebXml} config object derived from the + * contents of WEB-INF/appengine-web.xml. + * + * @throws AppEngineConfigException If WEB-INF/appengine-web.xml does + * not exist. Also thrown if we are unable to parse the xml. + */ + public AppEngineWebXml readAppEngineWebXml() { + InputStream is = null; + AppEngineWebXml appEngineWebXml; + try { + is = getInputStream(); + appEngineWebXml = processXml(is); + logger.info("Successfully processed " + getFilename()); + if (!appEngineWebXml.getThreadsafeValueProvided()) { + if (allowMissingThreadsafeElement()) { + logger.warning("appengine-web.xml does not contain a element. This will " + + "be treated as an error the next time you deploy.\nSee " + CONCURRENT_REQUESTS_URL + + " for more information.\nYou probably want to enable concurrent requests."); + } else { + throw new AppEngineConfigException("appengine-web.xml does not contain a " + + "element.\nSee " + CONCURRENT_REQUESTS_URL + " for more information.\nYou probably " + + "want to enable concurrent requests."); + } + } + if ("legacy".equals(appEngineWebXml.getAutoIdPolicy())) { + logger.warning("You have set the datastore auto id policy to 'legacy'. It is recommended " + + "that you select 'default' instead.\nLegacy auto ids are deprecated. You can " + + "continue to allocate legacy ids manually using the allocateIds() API functions.\n" + + "For more information see:\n" + + APPCFG_AUTO_IDS_URL + "\n" + DATASTORE_AUTO_IDS_URL + "\n"); + } + } catch (Exception e) { + String msg = "Received exception processing " + getFilename(); + logger.log(Level.SEVERE, msg, e); + if (e instanceof AppEngineConfigException) { + throw (AppEngineConfigException) e; + } + throw new AppEngineConfigException(msg, e); + } finally { + close(is); + } + return appEngineWebXml; + } + + protected boolean allowMissingThreadsafeElement() { + return false; + } + + public String getFilename() { + return filename; + } + + private void close(InputStream is) { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + throw new AppEngineConfigException(e); + } + } + } + + protected AppEngineWebXml processXml(InputStream is) { + return new AppEngineWebXmlProcessor().processXml(is); + } + + protected InputStream getInputStream() { + try { + return new FileInputStream(getFilename()); + } catch (FileNotFoundException fnfe) { + throw new AppEngineConfigException( + "Could not locate " + new File(getFilename()).getAbsolutePath(), fnfe); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/AppYaml.java b/java/src/main/com/google/apphosting/utils/config/AppYaml.java new file mode 100644 index 00000000..e91c75ec --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/AppYaml.java @@ -0,0 +1,1402 @@ +// Copyright 2010 Google. All Rights Reserved. +package com.google.apphosting.utils.config; + +import com.google.common.base.Preconditions; +import com.google.common.xml.XmlEscapers; + +import net.sourceforge.yamlbeans.YamlConfig; +import net.sourceforge.yamlbeans.YamlException; +import net.sourceforge.yamlbeans.YamlReader; + +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * JavaBean representation of the Java app.yaml file. + * + */ +public class AppYaml { + + /** + * Plugin service to modify app.yaml with runtime-specific defaults. + */ + public static interface Plugin { + AppYaml process(AppYaml yaml); + } + + /** + * A {@code Handler} element from app.yaml. Maps to {@code servlet}, {@code servlet-mapping}, + * {@code filter}, and {@code filter-mapping} elements in web.xml + * + */ + public static class Handler { + + public static enum Type {SERVLET, JSP, FILTER, NONE} + + private String url; + private String jsp; + private String servlet; + private String filter; + private LoginType login; + private Security secure; + private Map init_params; + private String name; + private boolean load_on_startup; + + public enum LoginType { admin, required } + public enum Security { always, optional, never } + private boolean api_endpoint = false; + + private String script; + + private static final String MULTIPLE_HANDLERS = "Cannot set both %s and %s for the same url."; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + YamlUtils.validateUrl(url); + this.url = url; + } + + public String getJsp() { + return jsp; + } + + public void setJsp(String jsp) { + this.jsp = jsp; + checkHandlers(); + } + + public String getServlet() { + return servlet; + } + + public void setServlet(String servlet) { + this.servlet = servlet; + checkHandlers(); + } + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + checkHandlers(); + } + + public Type getType() { + if (servlet != null) { + return Type.SERVLET; + } + if (filter != null) { + return Type.FILTER; + } + if (jsp != null) { + return Type.JSP; + } + return Type.NONE; + } + + public String getTarget() { + if (servlet != null) { + return servlet; + } + if (filter != null) { + return filter; + } + if (jsp != null) { + return jsp; + } + return null; + } + + public void setScript(String script) { + this.script = script; + } + + public String getScript() { + return this.script; + } + + public LoginType getLogin() { + return login; + } + + public void setLogin(LoginType login) { + this.login = login; + } + + public Security getSecure() { + return secure; + } + + public void setSecure(Security secure) { + if (secure == Security.never) { + throw new AppEngineConfigException("Java does not support secure: never"); + } + this.secure = secure; + } + + public Map getInit_params() { + return init_params; + } + + public void setInit_params(Map init_params) { + this.init_params = init_params; + } + + public String getName() { + return (name == null ? getTarget() : name); + } + + public void setLoad_on_startup(boolean loadOnStartup) { + this.load_on_startup = loadOnStartup; + } + + public boolean getLoad_on_startup() { + return this.load_on_startup; + } + + public void setName(String name) { + this.name = name; + } + + public String getApi_endpoint() { + return "" + this.api_endpoint; + } + + public void setApi_endpoint(String api_endpoint) { + this.api_endpoint = YamlUtils.parseBoolean(api_endpoint); + } + + public boolean isApiEndpoint() { + return this.api_endpoint; + } + + private void checkHandlers() { + if (jsp != null && servlet != null) { + throw new AppEngineConfigException(String.format(MULTIPLE_HANDLERS, "jsp", "servlet")); + } + if (jsp != null && filter != null) { + throw new AppEngineConfigException(String.format(MULTIPLE_HANDLERS, "jsp", "filter")); + } + if (filter != null && servlet != null) { + throw new AppEngineConfigException(String.format(MULTIPLE_HANDLERS, "filter", "servlet")); + } + } + + /** + * Generates the {@code servlet} or {@code filter} element of web.xml + * corresponding to this handler. + */ + private void generateDefinitionXml(XmlWriter xml) { + if (getServlet() != null || getJsp() != null) { + generateServletDefinition(xml); + } else if (getFilter() != null) { + generateFilterDefintion(xml); + } + } + + private void generateMappingXml(XmlWriter xml) { + if (getServlet() != null || getJsp() != null) { + generateServletMapping(xml); + } else if (getFilter() != null) { + generateFilterMapping(xml); + } + generateSecurityConstraints(xml); + } + + private void generateSecurityConstraints(XmlWriter xml) { + if (secure == Security.always || login == LoginType.required || login == LoginType.admin) { + xml.startElement("security-constraint"); + xml.startElement("web-resource-collection"); + xml.simpleElement("web-resource-name", "aname"); + xml.simpleElement("url-pattern", getUrl()); + xml.endElement("web-resource-collection"); + if (login == LoginType.required) { + securityConstraint(xml, "auth", "role-name", "*"); + } else if (login == LoginType.admin) { + securityConstraint(xml, "auth", "role-name", "admin"); + } + if (secure == Security.always) { + securityConstraint(xml, "user-data", "transport-guarantee", "CONFIDENTIAL"); + } + xml.endElement("security-constraint"); + } + } + + private void securityConstraint(XmlWriter xml, String type, String name, String value) { + type = type + "-constraint"; + xml.startElement(type); + xml.simpleElement(name, value); + xml.endElement(type); + } + + /** + * Generates a {@code filter} element of web.xml corresponding to this handler. + */ + private void generateFilterDefintion(XmlWriter xml) { + xml.startElement("filter"); + xml.simpleElement("filter-name", getName()); + xml.simpleElement("filter-class", getFilter()); + generateInitParams(xml); + xml.endElement("filter"); + } + + /** + * Generates a {@code filter-mapping} element of web.xml corresponding to this handler. + */ + private void generateFilterMapping(XmlWriter xml) { + xml.startElement("filter-mapping"); + xml.simpleElement("filter-name", getName()); + xml.simpleElement("url-pattern", getUrl()); + xml.endElement("filter-mapping"); + } + + /** + * Generates a {@code servlet} or {@code jsp-file} element of web.xml corresponding to this + * handler. + */ + private void generateServletDefinition(XmlWriter xml) { + xml.startElement("servlet"); + xml.simpleElement("servlet-name", getName()); + if (getJsp() == null) { + xml.simpleElement("servlet-class", getServlet()); + } else { + xml.simpleElement("jsp-file", getJsp()); + } + generateInitParams(xml); + if (load_on_startup) { + xml.simpleElement("load-on-startup", "1"); + } + xml.endElement("servlet"); + } + + /** + * Merges another handler into this handler, assuming that the other handler + * has the same name, type and target. This operation is intended to be + * used for generating a Servlet or Filter *definition* as opposed to a + * mapping, and therefore the urls of this handler and the other handler + * are not involved in the merge operation. The load_on_startup values + * of the two handlers will be OR'd and the init_params will be unioned. + */ + public void mergeDefinitions(Handler otherHandler) { + Preconditions.checkArgument(this.getName().equals(otherHandler.getName()), + "Cannot merge handler named " + this.getName() + " with handler named " + + otherHandler.getName()); + Preconditions.checkArgument(this.getType() == otherHandler.getType(), + "Cannot merge handler of type " + this.getType() + " with handler of type " + + otherHandler.getType()); + Preconditions.checkArgument(this.getTarget().equals(otherHandler.getTarget()), + "Cannont merge handler with target " + this.getTarget() + " with handler with target " + + otherHandler.getTarget()); + this.load_on_startup = this.load_on_startup || otherHandler.load_on_startup; + Map mergedInitParams = new LinkedHashMap(); + if (this.init_params != null) { + mergedInitParams.putAll(this.init_params); + } + if (otherHandler.init_params != null) { + for (String key : otherHandler.init_params.keySet()) { + String thisValue = mergedInitParams.get(key); + String otherValue = otherHandler.init_params.get(key); + if (thisValue == null) { + mergedInitParams.put(key, otherValue); + } else if (!thisValue.equals(otherValue)) { + throw new IllegalArgumentException( + "Cannot merge handlers with conflicting values for the init_param: " + key + " : " + + thisValue + " vs " + otherValue); + } + } + } + if (mergedInitParams.size() != 0) { + this.init_params = mergedInitParams; + } + } + + /** + * Generates a {@code servlet-mapping} element of web.xml corresponding to this handler. + */ + private void generateServletMapping(XmlWriter xml) { + if (isApiEndpoint()) { + xml.startElement("servlet-mapping", "id", xml.nextApiEndpointId()); + } else { + xml.startElement("servlet-mapping"); + } + xml.simpleElement("servlet-name", getName()); + xml.simpleElement("url-pattern", getUrl()); + xml.endElement("servlet-mapping"); + } + + private void generateInitParams(XmlWriter xml) { + if (init_params != null) { + for (Map.Entry param : init_params.entrySet()) { + xml.startElement("init-param"); + xml.simpleElement("param-name", param.getKey()); + xml.simpleElement("param-value", param.getValue()); + xml.endElement("init-param"); + } + } + } + + private void generateEndpointServletMappingId(XmlWriter xml) { + if (isApiEndpoint()) { + xml.simpleElement("endpoint-servlet-mapping-id", xml.nextApiEndpointId()); + } + } + } + + public static class ResourceFile { + private static final String EMPTY_MESSAGE = "Missing include or exclude."; + private static final String BOTH_MESSAGE = "Cannot specify both include and exclude."; + + protected String include; + protected String exclude; + protected Map httpHeaders; + + public String getInclude() { + if (exclude == null && include == null) { + throw new AppEngineConfigException(EMPTY_MESSAGE); + } + return include; + } + + public void setInclude(String include) { + if (exclude != null) { + throw new AppEngineConfigException(BOTH_MESSAGE); + } + this.include = include; + } + + public String getExclude() { + if (exclude == null && include == null) { + throw new AppEngineConfigException(EMPTY_MESSAGE); + } + return exclude; + } + + public void setExclude(String exclude) { + if (include != null) { + throw new AppEngineConfigException(BOTH_MESSAGE); + } + this.exclude = exclude; + } + + public Map getHttp_headers() { + if (include == null) { + throw new AppEngineConfigException("Missing include."); + } + + return httpHeaders; + } + + public void setHttp_headers(Map httpHeaders) { + if (include == null) { + throw new AppEngineConfigException("Missing include."); + } + + this.httpHeaders = httpHeaders; + } + } + + public static class StaticFile extends ResourceFile { + private static final String NO_INCLUDE = "Missing include."; + private static final String INCLUDE_ONLY = "Expiration can only be specified with include."; + private String expiration; + + public String getExpiration() { + if (expiration != null && include == null) { + throw new AppEngineConfigException(NO_INCLUDE); + } + return expiration; + } + + public void setExpiration(String expiration) { + if (exclude != null) { + throw new AppEngineConfigException(INCLUDE_ONLY); + } + this.expiration = expiration; + } + + @Override + public void setExclude(String exclude) { + if (expiration != null) { + throw new AppEngineConfigException(INCLUDE_ONLY); + } + super.setExclude(exclude); + } + } + + public static class AdminConsole { + private List pages; + + public List getPages() { + return pages; + } + + public void setPages(List pages) { + this.pages = pages; + } + } + + public static class AdminPage { + private String name; + private String url; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } + + public static class AsyncSessionPersistence { + private boolean enabled = false; + private String queue_name; + + public String getEnabled() { + return "" + enabled; + } + + public void setEnabled(String enabled) { + this.enabled = YamlUtils.parseBoolean(enabled); + } + + public String getQueue_name() { + return this.queue_name; + } + + public void setQueue_name(String queue_name) { + this.queue_name = queue_name; + } + } + + public static class ErrorHandler { + private String file; + private String errorCode; + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getError_code() { + return errorCode; + } + + public void setError_code(String errorCode) { + this.errorCode = errorCode; + } + } + + /** + * AutomaticScaling bean. + */ + public static class AutomaticScaling { + private String minPendingLatency; + private String maxPendingLatency; + private String minIdleInstances; + private String maxIdleInstances; + private String maxConcurrentRequests; + + public String getMin_pending_latency() { + return minPendingLatency; + } + + public void setMin_pending_latency(String minPendingLatency) { + this.minPendingLatency = minPendingLatency; + } + + public String getMax_pending_latency() { + return maxPendingLatency; + } + + public void setMax_pending_latency(String maxPendingLatency) { + this.maxPendingLatency = maxPendingLatency; + } + + public String getMin_idle_instances() { + return minIdleInstances; + } + + public void setMin_idle_instances(String minIdleInstances) { + this.minIdleInstances = minIdleInstances; + } + + public String getMax_idle_instances() { + return maxIdleInstances; + } + + public void setMax_idle_instances(String maxIdleInstances) { + this.maxIdleInstances = maxIdleInstances; + } + + public String getMax_concurrent_requests() { + return maxConcurrentRequests; + } + + public void setMax_concurrent_requests(String maxConcurrentRequests) { + this.maxConcurrentRequests = maxConcurrentRequests; + } + } + + /** + * ManualScaling bean. + */ + public static class ManualScaling { + private String instances; + + public String getInstances() { + return instances; + } + + public void setInstances(String instances) { + this.instances = instances; + } + } + + /** + * BasicScaling bean. + */ + public static class BasicScaling { + private String maxInstances; + private String idleTimeout; + + public String getMax_instances() { + return maxInstances; + } + + public void setMax_instances(String maxInstances) { + this.maxInstances = maxInstances; + } + public String getIdle_timeout() { + return idleTimeout; + } + + public void setIdle_timeout(String idleTimeout) { + this.idleTimeout = idleTimeout; + } + } + + private String application; + private String version; + private String source_language; + private String module; + private String instanceClass; + private AutomaticScaling automatic_scaling; + private ManualScaling manual_scaling; + private BasicScaling basic_scaling; + private String runtime; + private List handlers; + private String public_root; + private List static_files; + private List resource_files; + private boolean ssl_enabled = true; + private boolean precompilation_enabled = true; + private boolean sessions_enabled = false; + private AsyncSessionPersistence async_session_persistence; + private boolean threadsafe = false; + private String auto_id_policy; + private boolean threadsafeWasSet = false; + private boolean codeLock = false; + private Map system_properties; + private Map env_variables; + private Map context_params; + private List welcome_files; + private List listeners; + private List inbound_services; + private AdminConsole admin_console; + private List error_handlers; + private ApiConfig api_config; + private Pagespeed pagespeed; + private String web_xml; + + private static final String REQUIRED_FIELD = "Missing required element '%s'."; + + public String getApplication() { + if (application == null) { + throw new AppEngineConfigException(String.format(REQUIRED_FIELD, "application")); + } + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setSource_language(String sourceLanguage) { + this.source_language = sourceLanguage; + } + + public String getSource_language() { + return source_language; + } + + public String getModule() { + return module; + } + + public void setModule(String module) { + this.module = module; + } + + public String getInstance_class() { + return instanceClass; + } + + public void setInstance_class(String instanceClass) { + this.instanceClass = instanceClass; + } + + public AutomaticScaling getAutomatic_scaling() { + return automatic_scaling; + } + + public void setAutomatic_scaling(AutomaticScaling automaticScaling) { + this.automatic_scaling = automaticScaling; + } + + public ManualScaling getManual_scaling() { + return manual_scaling; + } + + public void setManual_scaling(ManualScaling manualScaling) { + this.manual_scaling = manualScaling; + } + + public BasicScaling getBasic_scaling() { + return basic_scaling; + } + + public void setBasic_scaling(BasicScaling basicScaling) { + this.basic_scaling = basicScaling; + } + + public String getRuntime() { + return runtime; + } + + public void setRuntime(String runtime) { + this.runtime = runtime; + } + + public List getHandlers() { + return handlers; + } + + public void setHandlers(List handlers) { + this.handlers = handlers; + if (this.api_config != null) { + this.api_config.setHandlers(handlers); + } + } + + public String getPublic_root() { + return public_root; + } + + public void setPublic_root(String public_root) { + this.public_root = public_root; + } + + public List getStatic_files() { + return static_files; + } + + public void setStatic_files(List static_files) { + this.static_files = static_files; + } + + public List getResource_files() { + return resource_files; + } + + public void setResource_files(List resource_files) { + this.resource_files = resource_files; + } + + public String getSsl_enabled() { + return "" + ssl_enabled; + } + + public void setSsl_enabled(String ssl_enabled) { + this.ssl_enabled = YamlUtils.parseBoolean(ssl_enabled); + } + + public boolean isSslEnabled() { + return ssl_enabled; + } + + public String getPrecompilation_enabled() { + return "" + precompilation_enabled; + } + + public boolean isPrecompilationEnabled() { + return precompilation_enabled; + } + + public void setPrecompilation_enabled(String precompilation_enabled) { + this.precompilation_enabled = YamlUtils.parseBoolean(precompilation_enabled); + } + + public String getSessions_enabled() { + return "" + sessions_enabled; + } + + public boolean isSessionsEnabled() { + return sessions_enabled; + } + + public void setSessions_enabled(String sessions_enabled) { + this.sessions_enabled = YamlUtils.parseBoolean(sessions_enabled); + } + + public AsyncSessionPersistence getAsync_session_persistence() { + return async_session_persistence; + } + + public void setAsync_session_persistence(AsyncSessionPersistence async_session_persistence) { + this.async_session_persistence = async_session_persistence; + } + + public String getThreadsafe() { + return "" + threadsafe; + } + + public boolean isThreadsafeSet() { + return threadsafeWasSet; + } + + public void setThreadsafe(String threadsafe) { + this.threadsafe = YamlUtils.parseBoolean(threadsafe); + threadsafeWasSet = true; + } + + public String getAuto_id_policy() { + return auto_id_policy; + } + + public void setAuto_id_policy(String policy) { + auto_id_policy = policy; + } + + public String getCode_lock() { + return "" + codeLock; + } + + public void setCode_lock(String codeLock) { + this.codeLock = YamlUtils.parseBoolean(codeLock); + } + + public Map getSystem_properties() { + return system_properties; + } + + public void setSystem_properties(Map system_properties) { + this.system_properties = system_properties; + } + + public Map getEnv_variables() { + return env_variables; + } + + public void setEnv_variables(Map env_variables) { + this.env_variables = env_variables; + } + + public List getWelcome_files() { + return welcome_files; + } + + public void setWelcome_files(List welcome_files) { + this.welcome_files = welcome_files; + } + + public Map getContext_params() { + return context_params; + } + + public void setContext_params(Map context_params) { + this.context_params = context_params; + } + + public List getListeners() { + return listeners; + } + + public void setListeners(List listeners) { + this.listeners = listeners; + } + + public String getWeb_xml() { + return web_xml; + } + + public void setWeb_xml(String web_xml) { + this.web_xml = web_xml; + } + + public List getInbound_services() { + return inbound_services; + } + + public void setInbound_services(List inbound_services) { + this.inbound_services = inbound_services; + } + + public AdminConsole getAdmin_console() { + return admin_console; + } + + public void setAdmin_console(AdminConsole admin_console) { + this.admin_console = admin_console; + } + + public List getError_handlers() { + return error_handlers; + } + + public void setError_handlers(List error_handlers) { + this.error_handlers = error_handlers; + } + + public ApiConfig getApi_config() { + return api_config; + } + + public void setApi_config(ApiConfig api_config) { + this.api_config = api_config; + if (handlers != null) { + this.api_config.setHandlers(handlers); + } + } + + public Pagespeed getPagespeed() { + return pagespeed; + } + + public void setPagespeed(Pagespeed pagespeed) { + this.pagespeed = pagespeed; + } + + public AppYaml applyPlugins() { + AppYaml yaml = this; + for (Plugin plugin : PluginLoader.loadPlugins(Plugin.class)) { + AppYaml modified = plugin.process(yaml); + if (modified != null) { + yaml = modified; + } + } + return yaml; + } + + /** + * Represents an api-config: top level app.yaml stanza + * This is a singleton specifying url: and servlet: for the api config server. + */ + public static class ApiConfig { + private String url; + private String servlet; + private List handlers; + + public void setHandlers(List handlers) { + this.handlers = handlers; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + YamlUtils.validateUrl(url); + this.url = url; + } + + public String getServlet() { + return servlet; + } + + public void setServlet(String servlet) { + this.servlet = servlet; + } + + private void generateXml(XmlWriter xml) { + xml.startElement("api-config", "servlet-class", getServlet(), "url-pattern", getUrl()); + if (handlers != null) { + for (Handler handler : handlers) { + handler.generateEndpointServletMappingId(xml); + } + } + xml.endElement("api-config"); + } + } + + /** + * Represents a <pagespeed> element. This is used to specify configuration for the Page + * Speed Service, which can be used to automatically optimize the loading speed of app engine + * sites. + */ + public static class Pagespeed { + private List urlBlacklist; + private List domainsToRewrite; + private List enabledRewriters; + private List disabledRewriters; + + public void setUrl_blacklist(List urls) { + urlBlacklist = urls; + } + + public List getUrl_blacklist() { + return urlBlacklist; + } + + public void setDomains_to_rewrite(List domains) { + domainsToRewrite = domains; + } + + public List getDomains_to_rewrite() { + return domainsToRewrite; + } + + public void setEnabled_rewriters(List rewriters) { + enabledRewriters = rewriters; + } + + public List getEnabled_rewriters() { + return enabledRewriters; + } + + public void setDisabled_rewriters(List rewriters) { + disabledRewriters = rewriters; + } + + public List getDisabled_rewriters() { + return disabledRewriters; + } + + private void generateXml(XmlWriter xml) { + if (!isEmpty()) { + xml.startElement("pagespeed"); + appendElements(xml, "url-blacklist", urlBlacklist); + appendElements(xml, "domain-to-rewrite", domainsToRewrite); + appendElements(xml, "enabled-rewriter", enabledRewriters); + appendElements(xml, "disabled-rewriter", disabledRewriters); + xml.endElement("pagespeed"); + } + } + + private void appendElements(XmlWriter xml, String name, List l) { + if (l != null) { + for (String elt : l) { + xml.simpleElement(name, elt); + } + } + } + + private boolean isEmpty() { + return !hasElements(urlBlacklist) && !hasElements(domainsToRewrite) + && !hasElements(enabledRewriters) && !hasElements(disabledRewriters); + } + + private boolean hasElements(List coll) { + return coll != null && !coll.isEmpty(); + } + } + + private class XmlWriter { + private static final String XML_HEADER = ""; + private final PrintWriter writer; + private int indent = 0; + private int apiEndpointId = 0; + + public XmlWriter(Writer w) { + writer = new PrintWriter(w); + writer.println(XML_HEADER); + } + + public void startElement(String name, String... attributes) { + startElement(name, false, attributes); + writer.println(); + } + + public void startElement(String name, boolean empty, String... attributes) { + indent(); + writer.print("<"); + writer.print(name); + for (int i = 0; i < attributes.length; i += 2) { + String attributeName = attributes[i]; + String value = attributes[i + 1]; + if (value != null) { + writer.print(" "); + writer.print(attributeName); + writer.print("='"); + writer.print(escapeAttribute(value)); + writer.print("'"); + } + } + if (empty) { + writer.println("/>"); + } else { + writer.print(">"); + indent += 2; + } + } + + public void endElement(String name) { + endElement(name, true); + } + + public void endElement(String name, boolean needIndent) { + indent -= 2; + if (needIndent) { + indent(); + } + writer.print(""); + } + + public void emptyElement(String name, String... attributes) { + startElement(name, true, attributes); + } + + public void simpleElement(String name, String value, String... attributes) { + startElement(name, false, attributes); + writer.print(escapeContent(value)); + endElement(name, false); + } + + public void writeUnescaped(String xmlContent) { + writer.println(xmlContent); + } + + private void indent() { + for (int i = 0; i < indent; i++) { + writer.print(" "); + } + } + + private String escapeContent(String value) { + if (value == null) { + return null; + } + return XmlEscapers.xmlContentEscaper().escape(value); + } + + private String escapeAttribute(String value) { + if (value == null) { + return null; + } + return XmlEscapers.xmlAttributeEscaper().escape(value); + } + + private String nextApiEndpointId() { + return String.format("endpoint-%1$d", ++apiEndpointId); + } + } + + private void addOptionalElement(XmlWriter xml, String name, String value) { + if (value != null) { + xml.simpleElement(name, value); + } + } + + public void generateAppEngineWebXml(Writer writer) { + XmlWriter xml = new XmlWriter(writer); + xml.startElement("appengine-web-app", "xmlns", "http://appengine.google.com/ns/1.0"); + xml.simpleElement("application", getApplication()); + addOptionalElement(xml, "version", getVersion()); + addOptionalElement(xml, "source-language", getSource_language()); + addOptionalElement(xml, "module", getModule()); + addOptionalElement(xml, "instance-class", getInstance_class()); + addOptionalElement(xml, "public-root", public_root); + addOptionalElement(xml, "auto-id-policy", getAuto_id_policy()); + if (automatic_scaling != null) { + xml.startElement("automatic-scaling"); + addOptionalElement(xml, "min-pending-latency", automatic_scaling.getMin_pending_latency()); + addOptionalElement(xml, "max-pending-latency", automatic_scaling.getMax_pending_latency()); + addOptionalElement(xml, "min-idle-instances", automatic_scaling.getMin_idle_instances()); + addOptionalElement(xml, "max-idle-instances", automatic_scaling.getMax_idle_instances()); + addOptionalElement(xml, "max-concurrent-requests", + automatic_scaling.getMax_concurrent_requests()); + xml.endElement("automatic-scaling"); + } + if (manual_scaling != null) { + xml.startElement("manual-scaling"); + xml.simpleElement("instances", manual_scaling.getInstances()); + xml.endElement("manual-scaling"); + } + if (basic_scaling != null) { + xml.startElement("basic-scaling"); + xml.simpleElement("max-instances", basic_scaling.getMax_instances()); + addOptionalElement(xml, "idle-timeout", basic_scaling.getIdle_timeout()); + xml.endElement("basic-scaling"); + } + xml.startElement("static-files"); + if (static_files != null) { + for (StaticFile file : static_files) { + String name, path; + if (file.getInclude() != null) { + generateInclude(file, xml); + } else { + xml.emptyElement("exclude", "path", file.getExclude()); + } + } + } + xml.endElement("static-files"); + xml.startElement("resource-files"); + if (resource_files != null) { + for (ResourceFile file : resource_files) { + String name, path; + if (file.getInclude() != null) { + name = "include"; + path = file.getInclude(); + } else { + name = "exclude"; + path = file.getExclude(); + } + xml.emptyElement(name, "path", path); + } + } + xml.endElement("resource-files"); + xml.simpleElement("ssl-enabled", getSsl_enabled()); + xml.simpleElement("precompilation-enabled", getPrecompilation_enabled()); + if (isThreadsafeSet()) { + xml.simpleElement("threadsafe", getThreadsafe()); + } + xml.simpleElement("code-lock", getCode_lock()); + xml.simpleElement("sessions-enabled", getSessions_enabled()); + if (async_session_persistence != null) { + xml.simpleElement("async-session-persistence", null, + "enabled", getAsync_session_persistence().getEnabled(), + "queue-name", getAsync_session_persistence().getQueue_name()); + } + if (system_properties != null) { + xml.startElement("system-properties"); + for (Map.Entry entry : system_properties.entrySet()) { + xml.emptyElement("property", "name", entry.getKey(), "value", entry.getValue()); + } + xml.endElement("system-properties"); + } + if (env_variables != null) { + xml.startElement("env-variables"); + for (Map.Entry entry : env_variables.entrySet()) { + xml.emptyElement("env-var", "name", entry.getKey(), "value", entry.getValue()); + } + xml.endElement("env-variables"); + } + boolean warmupService = false; + if (inbound_services != null) { + xml.startElement("inbound-services"); + for (String service : inbound_services) { + if (AppEngineWebXml.WARMUP_SERVICE.equals(service)) { + warmupService = true; + } else { + xml.simpleElement("service", service); + } + } + xml.endElement("inbound-services"); + } + xml.simpleElement("warmup-requests-enabled", Boolean.toString(warmupService)); + if (admin_console != null && admin_console.getPages() != null) { + xml.startElement("admin-console"); + for (AdminPage page : admin_console.getPages()) { + xml.emptyElement("page", "name", page.getName(), "url", page.getUrl()); + } + xml.endElement("admin-console"); + } + if (error_handlers != null) { + xml.startElement("static-error-handlers"); + for (ErrorHandler handler : error_handlers) { + xml.emptyElement("handler", + "file", handler.getFile(), + "error-code", handler.getError_code()); + } + xml.endElement("static-error-handlers"); + } + if (api_config != null) { + api_config.generateXml(xml); + } + if (pagespeed != null) { + pagespeed.generateXml(xml); + } + xml.endElement("appengine-web-app"); + } + + /** + * Generates the {@code servlet}, {@code servlet-mapping}, {@code filter}, and + * {@code filter-mapping} elements of web.xml corresponding to the {@link #handlers} list. There + * may be multiple {@link Handler handlers} corresponding to the same servlet or filter, because a + * single handler can only specify one URL pattern and the user may wish to map several URL + * patterns to the same servlet or filter. In this case we want to have multiple + * {@code servlet-mapping} or {@code filter-mapping} elements but only a single {@code servlet} or + * {@code filter} element. + */ + private void generateHandlerXml(XmlWriter xmlWriter) { + if (handlers == null) { + return; + } + Map servletsByName = new LinkedHashMap(handlers.size()); + Map filtersByName = new LinkedHashMap(handlers.size()); + for (Handler handler : handlers) { + String name = handler.getName(); + if (name != null) { + Handler.Type type = handler.getType(); + boolean isServlet = (type == Handler.Type.SERVLET || type == Handler.Type.JSP); + boolean isFilter = (type == Handler.Type.FILTER); + Handler existing = (isServlet ? servletsByName.get(name) : filtersByName.get(name)); + if (existing != null) { + existing.mergeDefinitions(handler); + } else { + if (isServlet) { + servletsByName.put(name, handler); + } + if (isFilter) { + filtersByName.put(name, handler); + } + } + } + } + for (Handler handler : servletsByName.values()) { + handler.generateDefinitionXml(xmlWriter); + } + for (Handler handler : filtersByName.values()) { + handler.generateDefinitionXml(xmlWriter); + } + for (Handler handler : handlers) { + handler.generateMappingXml(xmlWriter); + } + } + + public void generateWebXml(Writer writer) { + XmlWriter xml = new XmlWriter(writer); + xml.startElement("web-app", "version", "2.5", + "xmlns", "http://java.sun.com/xml/ns/javaee", + "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance", + "xsi:schemaLocation", + "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" + ); + generateHandlerXml(xml); + if (context_params != null) { + for (Map.Entry entry : context_params.entrySet()) { + xml.startElement("context-param"); + xml.simpleElement("param-name", entry.getKey()); + xml.simpleElement("param-value", entry.getValue()); + xml.endElement("context-param"); + } + } + if (welcome_files != null) { + xml.startElement("welcome-file-list"); + for (String file : welcome_files) { + xml.simpleElement("welcome-file", file); + } + xml.endElement("welcome-file-list"); + } + if (listeners != null) { + for (String listener : listeners) { + xml.startElement("listener"); + xml.simpleElement("listener-class", listener); + xml.endElement("listener"); + } + } + if (web_xml != null) { + xml.writeUnescaped(web_xml); + } + xml.endElement("web-app"); + } + + public static AppYaml parse(Reader reader) { + YamlReader yaml = new YamlReader(reader); + prepareParser(yaml.getConfig()); + try { + AppYaml appYaml = yaml.read(AppYaml.class); + if (appYaml == null) { + throw new YamlException("Unable to parse yaml file"); + } + return appYaml.applyPlugins(); + } catch (YamlException e) { + Throwable innerException = e.getCause(); + + while (innerException != null) { + if (innerException instanceof AppEngineConfigException) { + throw (AppEngineConfigException) innerException; + } + innerException = innerException.getCause(); + } + + throw new AppEngineConfigException(e.getMessage(), e); + } + } + + public static AppYaml parse(String yaml) { + return parse(new StringReader(yaml)); + } + + public static void prepareParser(YamlConfig config) { + config.setPropertyElementType(AppYaml.class, "handlers", Handler.class); + config.setPropertyElementType(AppYaml.class, "static_files", StaticFile.class); + config.setPropertyElementType(AppYaml.class, "resource_files", ResourceFile.class); + config.setPropertyElementType(AppYaml.class, "system_properties", String.class); + config.setPropertyElementType(AppYaml.class, "context_params", String.class); + config.setPropertyElementType(AppYaml.class, "env_variables", String.class); + config.setPropertyElementType(AppYaml.class, "welcome_files", String.class); + config.setPropertyElementType(AppYaml.class, "listeners", String.class); + config.setPropertyElementType(AppYaml.class, "inbound_services", String.class); + config.setPropertyElementType(Handler.class, "init_params", String.class); + config.setPropertyElementType(AdminConsole.class, "pages", AdminPage.class); + config.setPropertyElementType(AppYaml.class, "error_handlers", ErrorHandler.class); + config.setPropertyElementType(Pagespeed.class, "url_blacklist", String.class); + config.setPropertyElementType(Pagespeed.class, "domains_to_rewrite", String.class); + config.setPropertyElementType(Pagespeed.class, "enabled_rewriters", String.class); + config.setPropertyElementType(Pagespeed.class, "disabled_rewriters", String.class); + } + + private void generateInclude(StaticFile include, XmlWriter xml) { + String path = include.getInclude(); + Map httpHeaders = include.getHttp_headers(); + if (httpHeaders == null || httpHeaders.isEmpty()) { + xml.emptyElement("include", + "path", include.getInclude(), + "expiration", include.getExpiration()); + } else { + xml.startElement("include", + false, + "path", include.getInclude(), + "expiration", include.getExpiration()); + for (Map.Entry entry : httpHeaders.entrySet()) { + xml.emptyElement("http-header", + "name", entry.getKey(), + "value", entry.getValue()); + } + xml.endElement("include"); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/ApplicationXml.java b/java/src/main/com/google/apphosting/utils/config/ApplicationXml.java new file mode 100644 index 00000000..83d54cf0 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/ApplicationXml.java @@ -0,0 +1,235 @@ +package com.google.apphosting.utils.config; + +import com.google.common.collect.ImmutableList; + +import java.util.List; + +/** + * Holder for application.xml properties. Only properties needed to upload + * an application or run it in the Java Development Server are included. + * + */ +public class ApplicationXml { + private final Modules modules; + + private ApplicationXml(Modules modules) { + this.modules = modules; + } + + public Modules getModules() { + return modules; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((modules == null) ? 0 : modules.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ApplicationXml other = (ApplicationXml) obj; + if (modules == null) { + if (other.modules != null) { + return false; + } + } else if (!modules.equals(other.modules)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "ApplicationXml: modules=" + modules; + } + + /** + * Returns a {@link Builder}. + */ + static Builder builder() { + return new Builder(); + } + + /** + * Builder for an {@link ApplicationXml}. + */ + public static class Builder { + Modules.Builder modulesBuilder = Modules.Builder.builder(); + + private Builder() { + } + + /** + * Returns a {@link Modules.Builder} for adding modules. + */ + public Modules.Builder getModulesBuilder() { + return modulesBuilder; + } + + /** + * Builds and returns a new {@link ApplicationXml}. + */ + public ApplicationXml build() { + return new ApplicationXml(modulesBuilder.build()); + } + } + + /** + * Holder for an application's modules properties. + */ + public static class Modules { + private final ImmutableList web; + + private Modules(ImmutableList web) { + this.web = web; + } + + public List getWeb() { + return web; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((web == null) ? 0 : web.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Modules other = (Modules) obj; + if (web == null) { + if (other.web != null) { + return false; + } + } else if (!web.equals(other.web)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Modules: web=" + web; + } + + /** + * Builder for {@link Modules}. + */ + static class Builder { + ImmutableList.Builder webBuilder = ImmutableList.builder(); + + private Builder() { + } + + /** + * Returns a new {@link Builder}. + */ + private static Builder builder() { + return new Builder(); + } + + /** + * Adds a {@link Web}. + */ + Builder addWeb(Web web) { + webBuilder.add(web); + return this; + } + + /** + * Builds and returns the newly constructed {@link Modules}. + */ + Modules build() { + return new Modules(webBuilder.build()); + } + } + + /** + * Holder for properties for a web module. + */ + public static class Web { + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((contextRoot == null) ? 0 : contextRoot.hashCode()); + result = prime * result + ((webUri == null) ? 0 : webUri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Web other = (Web) obj; + if (contextRoot == null) { + if (other.contextRoot != null) { + return false; + } + } else if (!contextRoot.equals(other.contextRoot)) { + return false; + } + if (webUri == null) { + if (other.webUri != null) { + return false; + } + } else if (!webUri.equals(other.webUri)) { + return false; + } + return true; + } + + private final String webUri; + private final String contextRoot; + + Web(String webUri, String contextRoot) { + this.webUri = webUri; + this.contextRoot = contextRoot; + } + + public String getWebUri() { + return webUri; + } + + public String getContextRoot() { + return contextRoot; + } + + @Override + public String toString() { + return "Web: webUri=" + webUri + " contextRoot=" + contextRoot; + } + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/ApplicationXmlReader.java b/java/src/main/com/google/apphosting/utils/config/ApplicationXmlReader.java new file mode 100644 index 00000000..3792f7ef --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/ApplicationXmlReader.java @@ -0,0 +1,122 @@ +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser; +import org.mortbay.xml.XmlParser.Node; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +/** + * Constructs an {@link ApplicationXml} from an xml document + * corresponding to http://java.sun.com/xml/ns/javaee/application_5.xsd. + * + * We use Jetty's {@link XmlParser} utility to match other Appengine XML + * parsing code. + * + */ +public class ApplicationXmlReader { + /** + * Construct an {@link ApplicationXml} from the xml document + * within the provided {@link InputStream}. + * + * @param is The InputStream containing the xml we want to parse and process. + * + * @return Object representation of the xml document. + * @throws AppEngineConfigException If the input stream cannot be parsed. + */ + public ApplicationXml processXml(InputStream is) { + ApplicationXml.Builder builder = ApplicationXml.builder(); + HashSet contextRoots = new HashSet(); + for (Object o : XmlUtils.parse(is)) { + if (!(o instanceof XmlParser.Node)) { + continue; + } + XmlParser.Node node = (XmlParser.Node) o; + if ("icon".equals(node.getTag())) { + } else if ("display-name".equals(node.getTag())) { + } else if ("description".equals(node.getTag())) { + } else if ("module".equals(node.getTag())) { + handleModuleNode(node, builder.getModulesBuilder(), contextRoots); + } else if ("security-role".equals(node.getTag())) { + } else if ("library-directory".equals(node.getTag())) { + } else { + reportUnrecognizedTag(node.getTag()); + } + } + return builder.build(); + } + + private void handleModuleNode(Node module, ApplicationXml.Modules.Builder builder, + Set contextRoots) { + for (Object o : module) { + if (!(o instanceof XmlParser.Node)) { + continue; + } + XmlParser.Node node = (XmlParser.Node) o; + if ("alt-dd".equals(node.getTag())) { + } else if ("connector".equals(node.getTag())) { + } else if ("ejb".equals(node.getTag())) { + } else if ("java".equals(node.getTag())) { + } else if ("web".equals(node.getTag())) { + handleWebNode(node, builder, contextRoots); + } else { + reportUnrecognizedTag(node.getTag()); + } + } + } + + private void handleWebNode(Node web, ApplicationXml.Modules.Builder builder, + Set contextRoots) { + String contextRoot = null; + String webUri = null; + for (Object o : web) { + if (!(o instanceof XmlParser.Node)) { + continue; + } + XmlParser.Node node = (XmlParser.Node) o; + if ("web-uri".equals(node.getTag())) { + if (webUri != null) { + throw new AppEngineConfigException( + "web-uri multiply defined in application.xml web module."); + } + webUri = XmlUtils.getText(node); + if (webUri.isEmpty()) { + throw new AppEngineConfigException( + "web-uri is empty in application.xml web module."); + } + } else if ("context-root".equals(node.getTag())) { + if (contextRoot != null) { + throw new AppEngineConfigException( + "context-root multiply defined in application.xml web module."); + } + contextRoot = XmlUtils.getText(node); + if (contextRoot.isEmpty()) { + throw new AppEngineConfigException( + "context-root is empty in application.xml web module."); + } + if (contextRoots.contains(contextRoot)) { + throw new AppEngineConfigException( + "context-root value '" + contextRoot + "' is not unique."); + } + contextRoots.add(contextRoot); + } else { + reportUnrecognizedTag(node.getTag()); + } + } + if (null == webUri) { + throw new AppEngineConfigException( + "web-uri not defined in application.xml web module."); + } + if (null == contextRoot) { + throw new AppEngineConfigException( + "context-root not defined in application.xml web module."); + } + builder.addWeb(new ApplicationXml.Modules.Web(webUri, contextRoot)); + } + + private void reportUnrecognizedTag(String tag) throws AppEngineConfigException { + throw new AppEngineConfigException("Unrecognized element <" + tag + + "> in application.xml."); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/BackendsXml.java b/java/src/main/com/google/apphosting/utils/config/BackendsXml.java new file mode 100644 index 00000000..75fd0d31 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/BackendsXml.java @@ -0,0 +1,297 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import com.google.common.base.Joiner; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.Collections; + +/** + * Parsed backends.xml file. + * + */ +public class BackendsXml { + + public static final Set

Must be called after calling {@link #getUrls()}. + */ + public String getLogMessage() { + if (sortedUrls == null) { + throw new IllegalStateException( + "Cannot call getLogMessage() without first calling getUrls()"); + } + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < usedPrioritySpecifiers.length; ++i) { + if (!usedPrioritySpecifiers[i]) { + builder.append("priority-specifier: filename: "); + builder.append(priorityEntries.get(i).getFilename()); + if (priorityEntries.get(i).getPriority() != null) { + builder.append(" priority: "); + builder.append(priorityEntries.get(i).getPriorityValue()); + } + } + } + String errors = builder.toString(); + if (!errors.isEmpty()) { + return + "appengine-web.xml contains unused class-loader-config priority-specifier values.\n" + + "unused values: " + errors + "\nresulting classpath: " + Arrays.toString(sortedUrls) + + "\nTo remove this warning, remove the unused priority-specifier values " + + "from appengine-web.xml or add a file matching the unused priority-specifier values."; + } + return ""; + } + + /** + * Scans through a collection of URLs for various patterns and adds them + * with the correct priority. + */ + public void addUrls(Collection urls) { + for (URL url : urls) { + if (CLASSES_REGEX.matcher(url.getPath()).matches()) { + addClassesUrl(url); + } else if (APPENGINE_API_REGEX.matcher(url.getPath()).matches()) { + addAppengineJar(url); + } else { + addAppJar(url); + } + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/CronXml.java b/java/src/main/com/google/apphosting/utils/config/CronXml.java new file mode 100644 index 00000000..2cc11c2d --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/CronXml.java @@ -0,0 +1,180 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import com.google.cron.GrocTimeSpecification; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parsed cron.xml file. + * + * Any additions to this class should also be made to the YAML + * version in CronYamlReader.java. + * + */ +public class CronXml { + + /** + * Describes a single cron entry. + * + */ + public static class Entry { + + private static final String TZ_GMT = "UTC"; + + String url; + String desc; + String tz; + String schedule; + String target; + + /** Create an empty cron entry. */ + public Entry() { + desc = ""; + tz = TZ_GMT; + url = null; + schedule = null; + target = null; + } + + /** Records the human-readable description of this cron entry. */ + public void setDescription(String description) { + this.desc = description.replace('\n', ' '); + } + + /** Records the URL of this cron entry */ + public void setUrl(String url) { + this.url = url.replace('\n', ' '); + } + + /** + * Records the schedule of this cron entry. May throw + * {@link AppEngineConfigException} if the schedule does not parse + * correctly. + * + * @param schedule the schedule to save + */ + public void setSchedule(String schedule) { + schedule = schedule.replace('\n', ' '); + this.schedule = schedule; + } + + /** + * Sets the timezone for this cron entry's schedule. Defaults to "GMT" + * @param timezone timezone for the cron entry's {@code schedule}. + */ + public void setTimezone(String timezone) { + this.tz = timezone.replace('\n', ' '); + } + + public void setTarget(String target) { + this.target = target; + } + + public String getUrl() { + return url; + } + + public String getDescription() { + return desc; + } + + public String getSchedule() { + return schedule; + } + + public String getTimezone() { + return tz; + } + + public String getTarget() { + return target; + } + + } + + private List entries; + + /** Create an empty configuration object. */ + public CronXml() { + entries = new ArrayList(); + } + + /** + * Puts a new entry into the list defined by the config file. + * + * @throws AppEngineConfigException if the previously-last entry is still + * incomplete. + * @return the new entry + */ + public Entry addNewEntry() { + validateLastEntry(); + Entry entry = new Entry(); + entries.add(entry); + return entry; + } + + /** + * Puts an entry into the list defined by the config file. + * + * @throws AppEngineConfigException if the entry is still incomplete. + */ + public void addEntry(Entry entry) { + validateLastEntry(); + entries.add(entry); + validateLastEntry(); + } + + /** + * Get the entries. Used for testing. + */ + public List getEntries() { + return entries; + } + + /** + * Check that the last entry defined is complete. + * @throws AppEngineConfigException if it is not. + */ + public void validateLastEntry() { + if (entries.size() == 0) { + return; + } + Entry last = entries.get(entries.size() - 1); + if (last.getUrl() == null) { + throw new AppEngineConfigException("no URL for cronentry"); + } + if (last.getSchedule() == null) { + throw new AppEngineConfigException("no schedule for cronentry " + last.getUrl()); + } + try { + GrocTimeSpecification parsedSchedule = + GrocTimeSpecification.create(last.schedule); + } catch (IllegalArgumentException iae) { + throw new AppEngineConfigException("schedule " + last.schedule + " failed to parse", + iae.getCause()); + } + } + + /** + * Get the YAML equivalent of this cron.xml file. + * + * @return contents of an equivalent {@code cron.yaml} file. + */ + public String toYaml() { + StringBuilder builder = new StringBuilder("cron:\n"); + for (Entry ent : entries) { + builder.append("- description: '" + ent.getDescription().replace("'", "''") + "'\n"); + builder.append(" url: " + ent.getUrl() + "\n"); + builder.append(" schedule: " + ent.getSchedule() + "\n"); + builder.append(" timezone: " + ent.getTimezone() + "\n"); + String target = ent.getTarget(); + if (target != null) { + builder.append(" target: " + target + "\n"); + } + } + return builder.toString(); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/CronXmlReader.java b/java/src/main/com/google/apphosting/utils/config/CronXmlReader.java new file mode 100644 index 00000000..6833f55c --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/CronXmlReader.java @@ -0,0 +1,135 @@ +// Copyright 2008 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser.Node; + +import java.io.InputStream; +import java.util.Stack; + +/** + * Creates an {@link CronXml} instance from + * WEB-INF/cron.xml. If you want to read the configuration + * from a different file, subclass and override {@link #getFilename()}. If you + * want to read the configuration from something that isn't a file, subclass + * and override {@link #getInputStream()}. + * + */ +public class CronXmlReader extends AbstractConfigXmlReader { + + private static final String FILENAME = "WEB-INF/cron.xml"; + + private static final String CRONENTRIES_TAG = "cronentries"; + private static final String CRON_TAG = "cron"; + private static final String DESCRIPTION_TAG = "description"; + private static final String SCHEDULE_TAG = "schedule"; + private static final String TARGET_TAG = "target"; + private static final String TIMEZONE_TAG = "timezone"; + private static final String URL_TAG = "url"; + + /** + * Constructs the reader for {@code cron.xml} in a given application directory. + * @param appDir the application directory + */ + public CronXmlReader(String appDir) { + super(appDir, false); + } + + /** + * Parses the config file. + * @return A {@link CronXml} object representing the parsed configuration. + */ + public CronXml readCronXml() { + return readConfigXml(); + } + + @Override + protected CronXml processXml(InputStream is) { + final CronXml cronXml = new CronXml(); + parse(new ParserCallback() { + boolean first = true; + CronXml.Entry entry; + + @Override + public void newNode(Node node, Stack ancestors) { + switch (ancestors.size()) { + case 0: + if (!CRONENTRIES_TAG.equalsIgnoreCase(node.getTag())) { + throw new AppEngineConfigException(getFilename() + " does not contain <" + + CRONENTRIES_TAG + ">"); + } + if (!first) { + throw new AppEngineConfigException(getFilename() + " contains multiple <" + + CRONENTRIES_TAG + ">"); + } + first = false; + break; + + case 1: + if (CRON_TAG.equalsIgnoreCase(node.getTag())) { + entry = cronXml.addNewEntry(); + } else { + throw new AppEngineConfigException(getFilename() + " contains <" + + node.getTag() + "> instead of <" + CRON_TAG + "/>"); + } + break; + + case 2: + assert(entry != null); + if (DESCRIPTION_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setDescription((String) node.get(0)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + DESCRIPTION_TAG + ">"); + } + } else if (URL_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setUrl((String) node.get(0)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + URL_TAG + ">"); + } + } else if (SCHEDULE_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setSchedule((String) node.get(0)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + SCHEDULE_TAG + ">"); + } + } else if (TIMEZONE_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setTimezone((String) node.get(0)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + TIMEZONE_TAG + ">"); + } + } else if (TARGET_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setTarget((String) node.get(0)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + TARGET_TAG + ">"); + } + } else { + throw new AppEngineConfigException(getFilename() + " contains unknown <" + + node.getTag() + "> inside <" + CRON_TAG + "/>"); + } + break; + + default: + throw new AppEngineConfigException(getFilename() + + " has a syntax error; node <" + + node.getTag() + "> is too deeply nested to be valid."); + } + } + }, is); + cronXml.validateLastEntry(); + return cronXml; + } + + @Override + protected String getRelativeFilename() { + return FILENAME; + } + +} diff --git a/java/src/main/com/google/apphosting/utils/config/CronYamlReader.java b/java/src/main/com/google/apphosting/utils/config/CronYamlReader.java new file mode 100644 index 00000000..e588b692 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/CronYamlReader.java @@ -0,0 +1,89 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.io.File; +import java.io.FileReader; +import java.io.FileNotFoundException; +import java.io.Reader; +import java.io.StringReader; +import java.util.List; +import net.sourceforge.yamlbeans.YamlException; +import net.sourceforge.yamlbeans.YamlReader; + +/** + * Class to parse queue.yaml into a QueueXml object. + * + */ +public class CronYamlReader { + + /** + * Wrapper around CronXml to make the JavaBeans properties match the YAML file syntax. + */ + public static class CronYaml { + private List entries; + + public List getCron() { + return entries; + } + + public void setCron(List entries) { + this.entries = entries; + } + + public CronXml toXml() { + CronXml xml = new CronXml(); + if (entries != null) { + for (CronXml.Entry entry : entries) { + xml.addEntry(entry); + } + } + return xml; + } + } + + private static final String FILENAME = "cron.yaml"; + private String appDir; + + public CronYamlReader(String appDir) { + if (appDir.length() > 0 && appDir.charAt(appDir.length() - 1) != File.separatorChar) { + appDir += File.separatorChar; + } + this.appDir = appDir; + } + + public String getFilename() { + return appDir + CronYamlReader.FILENAME; + } + + public CronXml parse() { + if (new File(getFilename()).exists()) { + try { + return parse(new FileReader(getFilename())); + } catch (FileNotFoundException ex) { + throw new AppEngineConfigException("Cannot find file " + getFilename(), ex); + } + } + return null; + } + + public static CronXml parse(Reader yaml) { + YamlReader reader = new YamlReader(yaml); + reader.getConfig().setPropertyElementType(CronYaml.class, + "cron", + CronXml.Entry.class); + try { + CronYaml cronYaml = reader.read(CronYaml.class); + if (cronYaml == null) { + throw new AppEngineConfigException("Empty cron configuration."); + } + return cronYaml.toXml(); + } catch (YamlException ex) { + throw new AppEngineConfigException(ex.getMessage(), ex); + } + } + + public static CronXml parse(String yaml) { + return parse(new StringReader(yaml)); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/DispatchXml.java b/java/src/main/com/google/apphosting/utils/config/DispatchXml.java new file mode 100644 index 00000000..4f71aa48 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/DispatchXml.java @@ -0,0 +1,158 @@ +package com.google.apphosting.utils.config; + +import com.google.common.collect.ImmutableList; + +import java.util.List; + +/** + * Parsed configuration from a dispatch.xml file. + */ +public class DispatchXml { + + private final List dispatchEntries; + + public static Builder builder() { + return new Builder(); + } + + private DispatchXml(List dispatchEntries) { + this.dispatchEntries = dispatchEntries; + } + + /** + * Returns the YAML equivalent of this dispatch.xml file. + * + * @return contents of an equivalent {@code dos.yaml} file. + */ + public String toYaml() { + StringBuilder builder = new StringBuilder("dispatch:\n"); + for (DispatchEntry entry : dispatchEntries) { + builder.append("- url: " + yamlQuote(entry.getUrl()) + "\n"); + builder.append(" module: " + entry.getModule() + "\n"); + } + return builder.toString(); + } + + /** + * 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("'", "''") + "'"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((dispatchEntries == null) ? 0 : dispatchEntries.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DispatchXml)) { + return false; + } + DispatchXml other = (DispatchXml) obj; + if (dispatchEntries == null) { + if (other.dispatchEntries != null) { + return false; + } + } else if (!dispatchEntries.equals(other.dispatchEntries)) { + return false; + } + return true; + } + + /** + * Builder for a {@link DispatchXml}. + */ + public static class Builder { + private final ImmutableList.Builder dispatchEntriesBuilder = + ImmutableList.builder(); + + private Builder() { + } + + public Builder addDispatchEntry(DispatchEntry entry) { + entry.validate(); + dispatchEntriesBuilder.add(entry); + return this; + } + + public DispatchXml build() { + return new DispatchXml(dispatchEntriesBuilder.build()); + } + } + + /** + * Describes a single dispatch entry. + */ + public static class DispatchEntry { + + private final String url; + private final String module; + + /** Create an empty blacklist entry. */ + public DispatchEntry(String url, String module) { + this.url = url; + this.module = module; + } + + public String getUrl() { + return url; + } + + public String getModule() { + return module; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((module == null) ? 0 : module.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DispatchEntry)) { + return false; + } + DispatchEntry other = (DispatchEntry) obj; + if (module == null) { + if (other.module != null) { + return false; + } + } else if (!module.equals(other.module)) { + return false; + } + if (url == null) { + if (other.url != null) { + return false; + } + } else if (!url.equals(other.url)) { + return false; + } + return true; + } + + private void validate() { + if (url == null) { + throw new AppEngineConfigException("Invalid url in dispatch.xml - " + url); + } + if (module == null) { + throw new AppEngineConfigException("Invalid module in dispatch.xml - " + module); + } + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/DispatchXmlReader.java b/java/src/main/com/google/apphosting/utils/config/DispatchXmlReader.java new file mode 100644 index 00000000..f15aed01 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/DispatchXmlReader.java @@ -0,0 +1,160 @@ +package com.google.apphosting.utils.config; + +import com.google.apphosting.utils.config.DispatchXml.DispatchEntry; + +import org.mortbay.xml.XmlParser.Node; + +import java.io.File; +import java.io.InputStream; +import java.util.Stack; + +/** + * Creates a {@link DispatchXml} from dispatch.yaml. + */ +public class DispatchXmlReader extends AbstractConfigXmlReader { + + public static final String DEFAULT_RELATIVE_FILENAME = "WEB-INF" + File.separatorChar + + "dispatch.xml"; + + private final String relativeFilename; + + public DispatchXmlReader(String warDirectory, String relativeFilename) { + super(warDirectory, false); + this.relativeFilename = relativeFilename; + } + + /** + * Parses the dispatch.xml file if one exists into an {@link DispatchXml} and otherwise + * returns null. + */ + public DispatchXml readDispatchXml() { + return readConfigXml(); + } + + @Override + protected DispatchXml processXml(InputStream is) { + DispatchXmlParserCallback dispatchParserCallback = new DispatchXmlParserCallback(); + parse(dispatchParserCallback, is); + return dispatchParserCallback.getDispatchXml(); + } + + @Override + protected String getRelativeFilename() { + return relativeFilename; + } + + private class DispatchXmlParserCallback implements ParserCallback { + private final DispatchXml.Builder dispatchXmlBuilder = DispatchXml.builder(); + + private static final String DISPATCH_ENTRIES_TAG = "dispatch-entries"; + private static final String DISPATCH_TAG = "dispatch"; + private static final String URL_TAG = "url"; + private static final String MODULE_TAG = "module"; + + private boolean first = true; + + private boolean dispatchComplete; + private String url = null; + private String module = null; + + DispatchXml getDispatchXml() { + if (first) { + throw new IllegalStateException( + "getDispatchXml() called before parsing a valid dispatch.xml"); + } + checkForIncompleteDispatchElement(); + return dispatchXmlBuilder.build(); + } + + @Override + public void newNode(Node node, Stack ancestors) { + switch (ancestors.size()) { + case 0: + if (!DISPATCH_ENTRIES_TAG.equalsIgnoreCase(node.getTag())) { + throwExpectingTag(DISPATCH_ENTRIES_TAG, node.getTag()); + } + if (!first) { + throwDuplicateTag(DISPATCH_ENTRIES_TAG, null); + } + first = false; + break; + + case 1: + if (DISPATCH_TAG.equalsIgnoreCase(node.getTag())) { + checkForIncompleteDispatchElement(); + dispatchComplete = false; + } else { + throwExpectingTag(DISPATCH_TAG, node.getTag()); + } + break; + + case 2: + if (URL_TAG.equalsIgnoreCase(node.getTag())) { + if (dispatchComplete || url != null) { + throwDuplicateTag(URL_TAG, DISPATCH_TAG); + } else if (node.size() == 1 && node.get(0) instanceof String) { + url = (String) node.get(0); + } else { + throwBadElementContents(URL_TAG); + } + } else if (MODULE_TAG.equalsIgnoreCase(node.getTag())) { + if (dispatchComplete || module != null) { + throwDuplicateTag(MODULE_TAG, DISPATCH_TAG); + } else if (node.size() == 1 && node.get(0) instanceof String) { + module = (String) node.get(0); + } else { + throwBadElementContents(MODULE_TAG); + } + } else { + throwUnsupportedTag(node.getTag(), DISPATCH_TAG); + } + if (url != null && module != null) { + dispatchXmlBuilder.addDispatchEntry(new DispatchEntry(url, module)); + url = null; + module = null; + dispatchComplete = true; + } + break; + + default: + throw new AppEngineConfigException( + String.format("Syntax error; node <%s> is too deeply nested in file %s", + node.getTag(), getFilename())); + } + } + + private void checkForIncompleteDispatchElement() { + if (module != null) { + throwExpectingTag("url", "/dispatch"); + } + if (url != null) { + throwExpectingTag("module", "/dispatch"); + } + } + + private void throwExpectingTag(String expecting, String got) { + throw new AppEngineConfigException(String.format("Expecting <%s> but got <%s> in file %s", + expecting, got, getFilename())); + } + + private void throwUnsupportedTag(String tag, String parent) { + throw new AppEngineConfigException(String.format( + "Tag <%s> not supported in element <%s> in file %s", tag, parent, getFilename())); + } + + private void throwDuplicateTag(String duplicateTag, String parentTag) { + if (parentTag == null) { + throw new AppEngineConfigException(String.format("Duplicate <%s> in file %s", + duplicateTag, getFilename())); + } else { + throw new AppEngineConfigException(String.format("Duplicate <%s> inside <%s> in file %s", + duplicateTag, parentTag, getFilename())); + } + } + + private void throwBadElementContents(String badTag) { + throw new AppEngineConfigException(String.format( + "Invalid contents in element <%s> in file %s", badTag, getFilename())); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/DispatchYamlReader.java b/java/src/main/com/google/apphosting/utils/config/DispatchYamlReader.java new file mode 100644 index 00000000..7a696827 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/DispatchYamlReader.java @@ -0,0 +1,119 @@ +package com.google.apphosting.utils.config; + +import com.google.apphosting.utils.config.DispatchXml.DispatchEntry; +import com.google.common.annotations.VisibleForTesting; + +import net.sourceforge.yamlbeans.YamlException; +import net.sourceforge.yamlbeans.YamlReader; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.Reader; +import java.util.List; + +/** + * Class to parse dispatch.yaml into a {@link DispatchXml}. + * + */ +public class DispatchYamlReader { + private static final String DISPATCH_FILENAME = "dispatch.yaml"; + private final String parentDirectory; + + /** + * Constructs a {@link DispatchYamlReader}. + * @param parentDirectory the directory containing the dispatch.yaml file. + */ + public DispatchYamlReader(String parentDirectory) { + if (parentDirectory.length() > 0 + && parentDirectory.charAt(parentDirectory.length() - 1) != File.separatorChar) { + parentDirectory += File.separatorChar; + } + this.parentDirectory = parentDirectory; + } + + public String getFilename() { + return parentDirectory + DISPATCH_FILENAME; + } + + public DispatchXml parse() { + DispatchXml result = null; + try { + return parseImpl(new FileReader(getFilename())); + } catch (FileNotFoundException ex) { + } + return null; + } + + @VisibleForTesting + static DispatchXml parseImpl(Reader yaml) { + YamlReader reader = new YamlReader(yaml); + reader.getConfig().setPropertyElementType(DispatchYaml.class, "dispatch", + DispatchYamlEntry.class); + try { + DispatchYaml dispatchYaml = reader.read(DispatchYaml.class); + if (dispatchYaml == null) { + throw new AppEngineConfigException("Empty dispatch.yaml configuration."); + } + return dispatchYaml.toXml(); + } catch (YamlException ex) { + throw new AppEngineConfigException(ex.getMessage(), ex); + } + } + + /** + * Top level bean for a parsed dispatch.yaml file that meets the + * requirements of {@link YamlReader}. + */ + public static class DispatchYaml { + private List dispatchEntries; + + public List getDispatch() { + return dispatchEntries; + } + + public void setDispatch(List entries) { + this.dispatchEntries = entries; + } + + public DispatchXml toXml() { + DispatchXml.Builder builder = DispatchXml.builder(); + if (dispatchEntries != null) { + for (DispatchYamlEntry entry : dispatchEntries) { + builder.addDispatchEntry(entry.asDispatchEntry()); + } + } + return builder.build(); + } + } + + /** + * Bean for a parsed single uri to module mapping entry in a + * dispatch.yaml file that meets the requirements of + * {@link YamlReader}. + */ + public static class DispatchYamlEntry { + private String url; + private String module; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getModule() { + return module; + } + + public void setModule(String module) { + this.module = module; + } + + DispatchEntry asDispatchEntry() { + return new DispatchEntry(url, module); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/DosXml.java b/java/src/main/com/google/apphosting/utils/config/DosXml.java new file mode 100644 index 00000000..78ea7a0c --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/DosXml.java @@ -0,0 +1,115 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import com.google.net.base.CidrAddressBlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parsed dos.xml file. + * + * Any additions to this class should also be made to the YAML + * version in DosYamlReader.java. + * + */ +public class DosXml { + + /** + * Describes a single blacklist entry. + */ + public static class BlacklistEntry { + + String subnet; + String desc; + + /** Create an empty blacklist entry. */ + public BlacklistEntry() { + desc = ""; + subnet = null; + } + + /** Records the human-readable description of this blacklist entry. */ + public void setDescription(String description) { + this.desc = description.replace('\n', ' '); + } + + /** Records the subnet of this blacklist entry. */ + public void setSubnet(String subnet) { + try { + CidrAddressBlock parsedSubnet = CidrAddressBlock.create(subnet); + this.subnet = subnet; + } catch (IllegalArgumentException iae) { + this.subnet = null; + throw new AppEngineConfigException("subnet " + subnet + " failed to parse", + iae.getCause()); + } + } + + public String getSubnet() { + return subnet; + } + + public String getDescription() { + return desc; + } + } + + private List blacklistEntries; + + /** Create an empty configuration object. */ + public DosXml() { + blacklistEntries = new ArrayList(); + } + + /** + * Puts a new blacklist entry into the list defined by the config file. + * + * @throws AppEngineConfigException if the previously-last blacklist entry is + * still incomplete. + * @return the new entry + */ + public BlacklistEntry addNewBlacklistEntry() { + validateLastEntry(); + BlacklistEntry entry = new BlacklistEntry(); + blacklistEntries.add(entry); + return entry; + } + + public void addBlacklistEntry(BlacklistEntry entry) { + validateLastEntry(); + blacklistEntries.add(entry); + validateLastEntry(); + } + + /** + * Check that the last blacklist entry defined is complete. + * @throws AppEngineConfigException if it is not. + */ + public void validateLastEntry() { + if (blacklistEntries.size() == 0) { + return; + } + BlacklistEntry last = blacklistEntries.get(blacklistEntries.size() - 1); + if (last.getSubnet() == null) { + throw new AppEngineConfigException("no subnet for blacklist"); + } + } + + /** + * Get the YAML equivalent of this dos.xml file. + * + * @return contents of an equivalent {@code dos.yaml} file. + */ + public String toYaml() { + StringBuilder builder = new StringBuilder("blacklist:\n"); + for (BlacklistEntry ent : blacklistEntries) { + builder.append("- subnet: " + ent.getSubnet() + "\n"); + if (!ent.getDescription().equals("")) { + builder.append(" description: " + ent.getDescription() + "\n"); + } + } + return builder.toString(); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/DosXmlReader.java b/java/src/main/com/google/apphosting/utils/config/DosXmlReader.java new file mode 100644 index 00000000..e0069c0c --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/DosXmlReader.java @@ -0,0 +1,110 @@ +// Copyright 2010 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser.Node; + +import java.io.InputStream; +import java.util.Stack; + +/** + * Creates an {@link DosXml} instance from + * WEB-INF/dos.xml. If you want to read the configuration + * from a different file, subclass and override {@link #getFilename()}. If you + * want to read the configuration from something that isn't a file, subclass + * and override {@link #getInputStream()}. + * + */ +public class DosXmlReader extends AbstractConfigXmlReader { + + private static final String FILENAME = "WEB-INF/dos.xml"; + + private static final String BLACKLISTENTRIES_TAG = "blacklistentries"; + private static final String BLACKLIST_TAG = "blacklist"; + private static final String DESCRIPTION_TAG = "description"; + private static final String SUBNET_TAG = "subnet"; + + /** + * Constructs the reader for {@code dos.xml} in a given application directory. + * @param appDir the application directory + */ + public DosXmlReader(String appDir) { + super(appDir, false); + } + + /** + * Parses the config file. + * @return A {@link DosXml} object representing the parsed configuration. + */ + public DosXml readDosXml() { + return readConfigXml(); + } + + @Override + protected DosXml processXml(InputStream is) { + final DosXml dosXml = new DosXml(); + parse(new ParserCallback() { + boolean first = true; + DosXml.BlacklistEntry blacklistEntry; + + @Override + public void newNode(Node node, Stack ancestors) { + switch (ancestors.size()) { + case 0: + if (!BLACKLISTENTRIES_TAG.equalsIgnoreCase(node.getTag())) { + throw new AppEngineConfigException(getFilename() + " does not contain <" + + BLACKLISTENTRIES_TAG + ">"); + } + if (!first) { + throw new AppEngineConfigException(getFilename() + " contains multiple <" + + BLACKLISTENTRIES_TAG + ">"); + } + first = false; + break; + + case 1: + if (BLACKLIST_TAG.equalsIgnoreCase(node.getTag())) { + blacklistEntry = dosXml.addNewBlacklistEntry(); + } else { + throw new AppEngineConfigException(getFilename() + " contains <" + + node.getTag() + "> instead of <" + BLACKLIST_TAG + "/>"); + } + break; + + case 2: + assert(blacklistEntry != null); + if (DESCRIPTION_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + blacklistEntry.setDescription((String) node.get(0)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + DESCRIPTION_TAG + ">"); + } + } else if (SUBNET_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + blacklistEntry.setSubnet((String) node.get(0)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + SUBNET_TAG + ">"); + } + } else { + throw new AppEngineConfigException(getFilename() + " contains unknown <" + + node.getTag() + "> inside <" + BLACKLIST_TAG + "/>"); + } + break; + + default: + throw new AppEngineConfigException(getFilename() + + " has a syntax error; node <" + + node.getTag() + "> is too deeply nested to be valid."); + } + } + }, is); + dosXml.validateLastEntry(); + return dosXml; + } + + @Override + protected String getRelativeFilename() { + return FILENAME; + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/DosYamlReader.java b/java/src/main/com/google/apphosting/utils/config/DosYamlReader.java new file mode 100644 index 00000000..20effdb5 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/DosYamlReader.java @@ -0,0 +1,89 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.io.File; +import java.io.FileReader; +import java.io.FileNotFoundException; +import java.io.Reader; +import java.io.StringReader; +import java.util.List; +import net.sourceforge.yamlbeans.YamlException; +import net.sourceforge.yamlbeans.YamlReader; + +/** + * Class to parse dos.yaml into a DosXml object. + * + */ +public class DosYamlReader { + + /** + * Wrapper around DosXml to make the JavaBeans properties match the YAML file syntax. + */ + public static class DosYaml { + private List entries; + + public List getBlacklist() { + return entries; + } + + public void setBlacklist(List entries) { + this.entries = entries; + } + + public DosXml toXml() { + DosXml xml = new DosXml(); + if (entries != null) { + for (DosXml.BlacklistEntry entry : entries) { + xml.addBlacklistEntry(entry); + } + } + return xml; + } + } + + private static final String FILENAME = "dos.yaml"; + private String appDir; + + public DosYamlReader(String appDir) { + if (appDir.length() > 0 && appDir.charAt(appDir.length() - 1) != File.separatorChar) { + appDir += File.separatorChar; + } + this.appDir = appDir; + } + + public String getFilename() { + return appDir + DosYamlReader.FILENAME; + } + + public DosXml parse() { + if (new File(getFilename()).exists()) { + try { + return parse(new FileReader(getFilename())); + } catch (FileNotFoundException ex) { + throw new AppEngineConfigException("Cannot find file " + getFilename(), ex); + } + } + return null; + } + + public static DosXml parse(Reader yaml) { + YamlReader reader = new YamlReader(yaml); + reader.getConfig().setPropertyElementType(DosYaml.class, + "blacklist", + DosXml.BlacklistEntry.class); + try { + DosYaml dosYaml = reader.read(DosYaml.class); + if (dosYaml == null) { + throw new AppEngineConfigException("Empty dos configuration."); + } + return dosYaml.toXml(); + } catch (YamlException ex) { + throw new AppEngineConfigException(ex.getMessage(), ex); + } + } + + public static DosXml parse(String yaml) { + return parse(new StringReader(yaml)); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/EarInfo.java b/java/src/main/com/google/apphosting/utils/config/EarInfo.java new file mode 100644 index 00000000..c8624123 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/EarInfo.java @@ -0,0 +1,131 @@ +package com.google.apphosting.utils.config; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * Holder for information from an EAR directory. + * + */ +public class EarInfo { + private static final Logger LOGGER = Logger.getLogger(EarInfo.class.getName()); + + private final File earDirectory; + private final AppEngineApplicationXml appEngineApplicationXml; + private final ApplicationXml applicationXml; + private final List webModules; + private final Map moduleMap; + + EarInfo(File earDirectory, AppEngineApplicationXml appEngineApplicationXml, + ApplicationXml applicationXml, List webModules) + throws AppEngineConfigException{ + this.earDirectory = earDirectory; + this.appEngineApplicationXml = appEngineApplicationXml; + this.applicationXml = applicationXml; + this.webModules = ImmutableList.copyOf(webModules.iterator()); + + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (WebModule webModule : webModules) { + builder.put(webModule.getModuleName(), webModule); + } + try { + moduleMap = builder.build(); + } catch (IllegalArgumentException iae) { + if (iae.getMessage().startsWith("duplicate key: ")) { + String msg = "Invalid EAR - Duplicate module name"; + LOGGER.info(msg); + throw new AppEngineConfigException(msg, iae); + } else { + throw iae; + } + } + } + + public File getEarDirectory() { + return earDirectory; + } + + public ApplicationXml getApplicationXml() { + return applicationXml; + } + + public AppEngineApplicationXml getAppengineApplicationXml() { + return appEngineApplicationXml; + } + + public List getWebModules() { + return webModules; + } + + WebModule getWebModule(String moduleName) { + return moduleMap.get(moduleName); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((appEngineApplicationXml == null) ? 0 : appEngineApplicationXml.hashCode()); + result = prime * result + ((applicationXml == null) ? 0 : applicationXml.hashCode()); + result = prime * result + ((earDirectory == null) ? 0 : earDirectory.hashCode()); + result = prime * result + ((webModules == null) ? 0 : webModules.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + EarInfo other = (EarInfo) obj; + if (appEngineApplicationXml == null) { + if (other.appEngineApplicationXml != null) { + return false; + } + } else if (!appEngineApplicationXml.equals(other.appEngineApplicationXml)) { + return false; + } + if (applicationXml == null) { + if (other.applicationXml != null) { + return false; + } + } else if (!applicationXml.equals(other.applicationXml)) { + return false; + } + if (earDirectory == null) { + if (other.earDirectory != null) { + return false; + } + } else if (!earDirectory.equals(other.earDirectory)) { + return false; + } + if (webModules == null) { + if (other.webModules != null) { + return false; + } + } else if (!webModules.equals(other.webModules)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "EarInfo: earDirectory=" + earDirectory + + " appEngineApplicationXml=" + appEngineApplicationXml + + " applicationXml=" + applicationXml + + " webModules=" + webModules; + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/GenerationDirectory.java b/java/src/main/com/google/apphosting/utils/config/GenerationDirectory.java new file mode 100644 index 00000000..2241b06f --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/GenerationDirectory.java @@ -0,0 +1,41 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.io.File; + +/** + * Utility class to establish the location of generated files, given an + * application base directory. + * + * + */ +public class GenerationDirectory { + + /** + * The directory in which files are generated. The directory + * is relative to the WEB-INF folder of the current application. + */ + public static final String GENERATED_DIR_PROPERTY = "appengine.generated.dir"; + + /** The default value for {@code GENERATED_DIR_PROPERTY}. */ + private static final String GENERATED_DIR_DEFAULT = "appengine-generated"; + + /** + * Returns the generation directory for the current application. + * + * @param dir location of the application. The default generation directory + * will be inside this, if not overridden via the system property + * {#link GENERATED_DIR_PROPERTY}. + * @return pathname to generated file directory. The directory may + * or may not have been created. + */ + public static File getGenerationDirectory(File dir) { + return new File(System.getProperty(GENERATED_DIR_PROPERTY, + new File(new File(dir, "WEB-INF"), GENERATED_DIR_DEFAULT).getPath())); + } + + private GenerationDirectory() { + } + +} diff --git a/java/src/main/com/google/apphosting/utils/config/IndexYamlReader.java b/java/src/main/com/google/apphosting/utils/config/IndexYamlReader.java new file mode 100644 index 00000000..69cc46d0 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/IndexYamlReader.java @@ -0,0 +1,207 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import net.sourceforge.yamlbeans.YamlException; +import net.sourceforge.yamlbeans.YamlReader; + +import java.io.Reader; +import java.io.StringReader; +import java.util.LinkedList; +import java.util.List; + +/** + * Class to parse index.yaml into a IndexesXml object. + * + */ +public class IndexYamlReader { + + public static final String INDEX_DEFINITIONS_TAG = + "!!python/object:google.appengine.datastore.datastore_index.IndexDefinitions"; + public static final String INDEX_TAG = + "!!python/object:google.appengine.datastore.datastore_index.Index"; + public static final String PROPERTY_TAG = + "!!python/object:google.appengine.datastore.datastore_index.Property"; + + /** + * Wrapper around IndexesXml to make the JavaBeans properties match the YAML + * file syntax. + */ + public static class IndexYaml { + + /** + * JavaBean wrapper for Index entries in IndexesXml. + */ + public static class Index { + public String kind; + protected boolean ancestor; + public List properties; + + public void setAncestor(String ancestor) { + this.ancestor = YamlUtils.parseBoolean(ancestor); + } + + public String getAncestor() { + return "" + ancestor; + } + } + + /** + * JavaBean wrapper for IndexesXml properties. + */ + public static class Property { + public String name; + private String direction = "asc"; + + public void setDirection(String direction) { + if ("desc".equals(direction) || "asc".equals(direction)) { + this.direction = direction; + } else { + throw new AppEngineConfigException( + "Invalid direction '" + direction + "': expected 'asc' or 'desc'."); + } + } + + public String getDirection() { + return direction; + } + + public boolean isAscending() { + return "asc".equals(direction); + } + } + + private List indexes; + + public List getIndexes() { + return indexes; + } + + public void setIndexes(List indexes) { + this.indexes = indexes; + } + + public IndexesXml toXml(IndexesXml xml) { + if (indexes == null) { + throw new AppEngineConfigException("Empty index configuration."); + } + if (xml == null) { + xml = new IndexesXml(); + } + for (Index yamlIndex : indexes) { + if (yamlIndex.kind == null) { + throw new AppEngineConfigException("Index missing required element 'kind'"); + } + IndexesXml.Index xmlIndex = xml.addNewIndex(yamlIndex.kind, yamlIndex.ancestor); + if (yamlIndex.properties != null) { + for (Property property : yamlIndex.properties) { + if (property.name == null) { + throw new AppEngineConfigException("Property is missing required element 'name'."); + } + xmlIndex.addNewProperty(property.name, property.isAscending()); + } + } + } + return xml; + } + } + + public static IndexesXml parse(Reader yaml, IndexesXml xml) { + List list; + try { + list = parseMultiple(yaml, xml); + } catch (YamlException ex) { + throw new AppEngineConfigException(ex.getMessage(), ex); + } + if (0 == list.size()) { + throw new AppEngineConfigException("Empty index configuration."); + } + if (list.size() > 1) { + throw new AppEngineConfigException( + "yaml unexepectedly contains more than one document: " + list.size()); + } + return list.get(0); + } + + /** + * Parses the Yaml from {@code yaml} into a Yaml document and deserializes + * the document into an instance of {@link IndexesXml}. + * @param yaml A {@link String} from which to read the Yaml. This String is allowed to + * be in the style generated by the admin server, including the Python-specific tags. + * This method will safely ignore those tags. + * @return An instance of {@link IndexesXml}. + */ + public static IndexesXml parse(String yaml) { + return parse(new StringReader(clean(yaml)), null); + } + + /** + * Parses the Yaml from {@code yaml} into one or more documents, and + * deserializes the documents into one or more instances of {@link IndexesXml} + * which are returned in a {@link List}. + * + * @param yaml A {@link String} from which to read the Yyaml. This String is + * allowed to be in the style generated by the admin server, including + * the Python-specific tags. This method will safely ignore those tags. + * @return A {@link List} of {@link IndexesXml} instances representing one or + * more parsed Yaml documents. + */ + public static List parseMultiple(String yaml) { + try { + return parseMultiple(new StringReader(clean(yaml)), null); + } catch (YamlException ex) { + throw new AppEngineConfigException(ex.getMessage(), ex); + } + } + + /** + * Cleans a Yaml String by removing Pyton-specific tags. + * These tags are written by the admin server when it generates + * Yaml representing indexes. + * @param yaml A {@link String} containing Yaml + * @return The cleaned {@link String} + */ + private static String clean(String yaml) { + return yaml + .replaceAll(INDEX_DEFINITIONS_TAG, "") + .replaceAll(INDEX_TAG, "") + .replaceAll(PROPERTY_TAG, "") + .trim(); + } + + /** + * Parses the Yaml from {@code yaml} into one or more documents, and + * deserializes the documents into one or more instances of {@link IndexesXml} + * which are returned in a {@link List}. + * + * @param yaml A {@link Reader} from which to read the yaml + * @param xml A possibly {@code null} {@link IndexesXml} instance. If this + * parameter is not {@code null} then each of the yaml documents will + * be deserialized into it. This parameter is intended to be + * used in cases where there is only one Yaml document expected and so + * there will only be one instance of {@link IndexesXml} in the + * returned {@link List}. If this parameter is not {@code null} and the + * returned list has length greater than 1, then the list will contain + * multiple copies of this parameter. + * @return A {@link List} of {@link IndexesXml} instances representing one or + * more parsed Yaml documents. + * @throws YamlException If the Yaml parser has trobule parsing. + */ + private static List parseMultiple(Reader yaml, IndexesXml xml) throws YamlException { + YamlReader reader = new YamlReader(yaml); + reader.getConfig().setPropertyElementType(IndexYaml.class, "indexes", IndexYaml.Index.class); + reader.getConfig().setPropertyElementType( + IndexYaml.Index.class, "properties", IndexYaml.Property.class); + List list = new LinkedList(); + while (true) { + IndexYaml indexYaml = reader.read(IndexYaml.class); + if (null == indexYaml) { + break; + } else { + list.add(indexYaml.toXml(xml)); + } + } + return list; + } + +} diff --git a/java/src/main/com/google/apphosting/utils/config/IndexesXml.java b/java/src/main/com/google/apphosting/utils/config/IndexesXml.java new file mode 100644 index 00000000..9ccf27ff --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/IndexesXml.java @@ -0,0 +1,195 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Parsed datastore-indexes.xml file. + * + * Any additions to this class should also be made to the YAML + * version in IndexYamlReader.java. + * + */ +public class IndexesXml implements Iterable{ + + public class PropertySort { + + private String propName; + private boolean ascending; + + public PropertySort (String propName, boolean ascending) { + this.propName = propName; + this.ascending = ascending; + } + + public String getPropertyName() { + return propName; + } + + public boolean isAscending() { + return ascending; + } + } + + /** + */ + public class Index { + + private String kind; + private boolean ancestors; + private List properties; + + public Index (String kind, boolean ancestors) { + this.kind = kind; + this.ancestors = ancestors; + this.properties = new ArrayList(); + } + + public void addNewProperty(String name, boolean ascending) { + properties.add(new PropertySort(name, ascending)); + } + + public String getKind() { + return kind; + } + + public boolean doIndexAncestors() { + return ancestors; + } + + public List getProperties() { + return properties; + } + + /** + * Builds a Yaml String representing this index, using the style of Yaml + * generation appropriate for a local indexes.yaml files. + * @return A Yaml String + */ + private String toLocalStyleYaml(){ + StringBuilder builder = new StringBuilder(50 * (1 + properties.size())); + builder.append("- kind: \"" + kind + "\"\n"); + if (ancestors) { + builder.append(" ancestor: yes\n"); + } + builder.append(" properties:\n"); + for (PropertySort prop : properties) { + builder.append(" - name: \"" + prop.getPropertyName() + "\"\n"); + builder.append(" direction: " + (prop.isAscending() ? "asc" : "desc")+ "\n"); + } + return builder.toString(); + } + + /** + * Builds a Yaml string representing this index, mimicking the style of Yaml + * generation used on the admin server. Since the admin server is written in + * python, it generates a slightly different style of yaml. This method is + * useful only for testing that the client-side code is able to parse this + * style of yaml. + * + * @return An admin-server-style Yaml String. + */ + private String toServerStyleYaml() { + StringBuilder builder = new StringBuilder(50 * (1 + properties.size())); + builder.append("- ").append(IndexYamlReader.INDEX_TAG).append("\n"); + builder.append(" kind: " + kind + "\n"); + if (ancestors) { + builder.append(" ancestor: yes\n"); + } + builder.append(" properties:\n"); + for (PropertySort prop : properties) { + builder + .append(" - ") + .append(IndexYamlReader.PROPERTY_TAG) + .append(" {direction: ") + .append((prop.isAscending() ? "asc" : "desc")) + .append(",\n") + .append(" ") + .append("name: " + prop.getPropertyName()) + .append("}\n"); + } + return builder.toString(); + } + + public String toXmlString() { + StringBuilder builder = new StringBuilder(100 * (1 + properties.size())); + builder.append("\n"); + for (PropertySort prop : properties) { + String direction = (prop.isAscending() ? "asc" : "desc"); + builder.append( + " \n"); + } + builder.append("\n"); + return builder.toString(); + } + } + + private List indexes; + + public IndexesXml() { + indexes = new ArrayList(); + } + + @Override + public Iterator iterator() { + return indexes.iterator(); + } + + public int size(){ + return indexes.size(); + } + + public Index addNewIndex(String kind, boolean ancestors) { + Index index = new Index(kind, ancestors); + indexes.add(index); + return index; + } + + /** + * Adds the given {@link Index} to the collection + * contained in this object. Note that given {@link Index} + * is not cloned. The provided object instance will become + * incorporated into this object's collection. + * @param index + */ + public void addNewIndex(Index index){ + indexes.add(index); + } + + public String toYaml() { + return toYaml(false); + } + + /** + * Builds yaml string representing the indexes + * + * @param serverStyle Use the admin server style of yaml generation. Since the + * admin server is written in python, it generates a slightly different + * style of yaml. Setting this parameter to {@code true} is useful only + * for testing that the client-side code is able to parse this style of + * yaml. + * @return A Yaml string. + */ + public String toYaml(boolean serverStyle) { + StringBuilder builder = new StringBuilder(1024); + if (serverStyle) { + builder.append(IndexYamlReader.INDEX_DEFINITIONS_TAG).append("\n"); + } + builder.append("indexes:"); + int numIndexes = (null == indexes ? 0 : indexes.size()); + if (0 == numIndexes && serverStyle) { + builder.append(" []"); + } + builder.append("\n"); + for (Index index : indexes) { + String indexYaml = (serverStyle ? index.toServerStyleYaml() : index.toLocalStyleYaml()); + builder.append(indexYaml); + } + return builder.toString(); + } + +} diff --git a/java/src/main/com/google/apphosting/utils/config/IndexesXmlReader.java b/java/src/main/com/google/apphosting/utils/config/IndexesXmlReader.java new file mode 100644 index 00000000..f09afae2 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/IndexesXmlReader.java @@ -0,0 +1,223 @@ +// Copyright 2008 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser.Node; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.InputStream; +import java.io.Reader; +import java.util.Stack; +import java.util.logging.Level; + +/** + * Creates an {@link IndexesXml} instance from + * WEB-INF/datastore-indexes.xml. If you want to read the + * configuration from a different file, subclass and override + * {@link #getFilename()}. If you want to read the configuration from + * something that isn't a file, subclass and override + * {@link #getInputStream()}. + * + */ +public class IndexesXmlReader extends AbstractConfigXmlReader { + + /** + * Relative-to-{@code GenerationDirectory.GENERATED_DIR_PROPERTY} file for + * generated index. + */ + public static final String GENERATED_INDEX_FILENAME = "datastore-indexes-auto.xml"; + public static final String INDEX_FILENAME = "datastore-indexes.xml"; + public static final String INDEX_YAML_FILENAME = "WEB-INF/index.yaml"; + + /** Name of the XML tag in {@code datastore-indexes.xml} for autoindexing */ + public static final String AUTOINDEX_TAG = "auto-update"; + + private static final String FILENAME = "WEB-INF/datastore-indexes.xml"; + + private static final String INDEXES_TAG = "datastore-indexes"; + private static final String INDEX_TAG = "datastore-index"; + private static final String KIND_PROP = "kind"; + private static final String ANCESTORS_PROP = "ancestor"; + private static final String ANCESTORS_VALUE_YES = "true"; + private static final String ANCESTORS_VALUE_NO = "false"; + private static final String PROPERTY_TAG = "property"; + private static final String NAME_PROP = "name"; + private static final String DIRECTION_PROP = "direction"; + private static final String DIRECTION_VALUE_ASC = "asc"; + private static final String DIRECTION_VALUE_DESC = "desc"; + + private IndexesXml indexesXml; + + /** + * Constructs a reader for the {@code indexes.xml} configuration of a given app. + * @param appDir root directory of the application + */ + public IndexesXmlReader(String appDir) { + super(appDir, false); + } + + /** + * Reads the configuration file. + * @return an {@link IndexesXml} representing the parsed configuration. + */ + public IndexesXml readIndexesXml() { + return readConfigXml(); + } + + /** + * Reads the index configuration. If neither the user-generated nor the + * auto-generated config file exists, returns a {@code null}. Otherwise, + * reads both files (if available) and returns the union of both sets of + * indexes. + * + * @throws AppEngineConfigException If the file cannot be parsed properly + */ + @Override + protected IndexesXml readConfigXml() { + InputStream is = null; + String filename = null; + + indexesXml = new IndexesXml(); + try { + if (fileExists()) { + filename = getFilename(); + is = getInputStream(); + processXml(is); + logger.info("Successfully processed " + filename); + } + if (yamlFileExists()) { + filename = getYamlFilename(); + IndexYamlReader.parse(getYamlReader(), indexesXml); + logger.info("Successfully processed " + filename); + } + if (generatedFileExists()) { + filename = getGeneratedFile().getPath(); + is = getGeneratedStream(); + processXml(is); + logger.info("Successfully processed " + filename); + } + } catch (Exception e) { + String msg = "Received exception processing " + filename; + logger.log(Level.SEVERE, msg, e); + if (e instanceof AppEngineConfigException) { + throw (AppEngineConfigException) e; + } + throw new AppEngineConfigException(msg, e); + } finally { + close(is); + } + return indexesXml; + } + + @Override + protected IndexesXml processXml(InputStream is) { + parse(new ParserCallback() { + boolean first = true; + IndexesXml.Index index; + + @Override + public void newNode(Node node, Stack ancestors) { + switch (ancestors.size()) { + case 0: + if (!INDEXES_TAG.equalsIgnoreCase(node.getTag())) { + throw new AppEngineConfigException(getFilename() + " does not contain <" + + INDEXES_TAG + ">"); + } + if (!first) { + throw new AppEngineConfigException(getFilename() + " contains multiple <" + + INDEXES_TAG + ">"); + } + first = false; + break; + + case 1: + if (INDEX_TAG.equalsIgnoreCase(node.getTag())) { + String kind = node.getAttribute(KIND_PROP); + if (kind == null) { + throw new AppEngineConfigException(getFilename() + " has <" + INDEX_TAG + + "> missing required attribute \"" + KIND_PROP + "\""); + } + String anc = node.getAttribute(ANCESTORS_PROP).toLowerCase(); + boolean ancestorProp = false; + if (anc != null) { + if (anc.equals(ANCESTORS_VALUE_YES)) { + ancestorProp = true; + } else if (!anc.equals(ANCESTORS_VALUE_NO)) { + throw new AppEngineConfigException(getFilename() + " has <" + INDEX_TAG + + "> with attribute \"" + ANCESTORS_PROP + "\" not \"" + ANCESTORS_VALUE_YES + + "\" or \"" + ANCESTORS_VALUE_NO + "\""); + } + } + index = indexesXml.addNewIndex(kind, ancestorProp); + + } else { + throw new AppEngineConfigException(getFilename() + " contains <" + + node.getTag() + "> instead of <" + INDEX_TAG + "/>"); + } + break; + + case 2: + assert(index != null); + if (PROPERTY_TAG.equalsIgnoreCase(node.getTag())) { + String name = node.getAttribute(NAME_PROP); + if (name == null) { + throw new AppEngineConfigException(getFilename() + " has <" + PROPERTY_TAG + + "> missing required attribute \"" + NAME_PROP + "\""); + } + String direction = node.getAttribute(DIRECTION_PROP).toLowerCase(); + boolean ascending = true; + if (direction != null) { + if (direction.equals(DIRECTION_VALUE_DESC)) { + ascending = false; + } else if (!direction.equals(DIRECTION_VALUE_ASC)) { + throw new AppEngineConfigException(getFilename() + " has <" + PROPERTY_TAG + + "> with attribute \"" + DIRECTION_PROP + "\" not \"" + DIRECTION_VALUE_ASC + + "\" or \"" + DIRECTION_VALUE_DESC + "\""); + } + } + index.addNewProperty(name, ascending); + } else { + throw new AppEngineConfigException(getFilename() + " contains <" + + node.getTag() + "> instead of <" + PROPERTY_TAG + "/>"); + } + break; + + default: + throw new AppEngineConfigException(getFilename() + + " has a syntax error; node <" + + node.getTag() + "> is too deeply nested to be valid."); + } + } + }, is); + return indexesXml; + } + + @Override + protected String getRelativeFilename() { + return FILENAME; + } + + @Override + protected File getGeneratedFile() { + File genFile = new File(GenerationDirectory.getGenerationDirectory(new File(appDir)), + GENERATED_INDEX_FILENAME); + return genFile; + } + + protected String getYamlFilename() { + return appDir + INDEX_YAML_FILENAME; + } + + protected boolean yamlFileExists() { + return new File(getYamlFilename()).exists(); + } + + protected Reader getYamlReader() { + try { + return new FileReader(getYamlFilename()); + } catch (FileNotFoundException ex) { + throw new AppEngineConfigException("Cannot find file" + getYamlFilename()); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/PluginLoader.java b/java/src/main/com/google/apphosting/utils/config/PluginLoader.java new file mode 100644 index 00000000..1b93e436 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/PluginLoader.java @@ -0,0 +1,56 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.ServiceLoader; +import java.util.logging.Logger; + +/** + * Utility for loading plugins in AppConfig and DevAppserver. + * + */ +public class PluginLoader { + + /** + * Name of the system property used to specify the plugin search path. + */ + public final static String PLUGIN_PATH = "com.google.appengine.plugin.path"; + private final static Logger logger = Logger.getLogger(PluginLoader.class.getName()); + + private static ClassLoader loader = null; + + /** + * Searches for plugins of the specified type. + */ + public static Iterable loadPlugins(Class pluginClass) { + return ServiceLoader.load(pluginClass, getPluginClassLoader()); + } + + private synchronized static ClassLoader getPluginClassLoader() { + if (loader == null) { + ClassLoader parent = PluginLoader.class.getClassLoader(); + String path = System.getProperty(PLUGIN_PATH); + if (path == null) { + loader = parent; + } else { + String[] paths = path.split(File.pathSeparator); + ArrayList urls = new ArrayList(paths.length); + for (int i = 0; i < paths.length; ++i) { + try { + urls.add(new File(paths[i]).toURI().toURL()); + } catch (MalformedURLException ex) { + logger.severe("Skipping invalid plugin path " + paths[i]); + } + } + loader = new URLClassLoader(urls.toArray(new URL[urls.size()]), parent); + } + } + return loader; + } + +} diff --git a/java/src/main/com/google/apphosting/utils/config/QueueXml.java b/java/src/main/com/google/apphosting/utils/config/QueueXml.java new file mode 100644 index 00000000..a55a5efc --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/QueueXml.java @@ -0,0 +1,639 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parsed queue.xml file. + * + * Any additions to this class should also be made to the YAML + * version in QueueYamlReader.java. + * + */ +public class QueueXml { + + static final String RATE_REGEX = "([0-9]+(\\.[0-9]+)?)/([smhd])"; + static final Pattern RATE_PATTERN = Pattern.compile(RATE_REGEX); + + static final String TOTAL_STORAGE_LIMIT_REGEX = "^([0-9]+(\\.[0-9]*)?[BKMGT]?)"; + static final Pattern TOTAL_STORAGE_LIMIT_PATTERN = Pattern.compile(TOTAL_STORAGE_LIMIT_REGEX); + + private static final int MAX_QUEUE_NAME_LENGTH = 100; + private static final String QUEUE_NAME_REGEX = "[a-zA-Z\\d-]{1," + MAX_QUEUE_NAME_LENGTH + "}"; + private static final Pattern QUEUE_NAME_PATTERN = Pattern.compile(QUEUE_NAME_REGEX); + + private static final String TASK_AGE_LIMIT_REGEX = + "([0-9]+(?:\\.?[0-9]*(?:[eE][\\-+]?[0-9]+)?)?)([smhd])"; + private static final Pattern TASK_AGE_LIMIT_PATTERN = Pattern.compile(TASK_AGE_LIMIT_REGEX); + + private static final String MODE_REGEX = "push|pull"; + private static final Pattern MODE_PATTERN = Pattern.compile(MODE_REGEX); + + private static final int MAX_TARGET_LENGTH = 100; + private static final String TARGET_REGEX = "[a-z\\d\\-]{1," + MAX_TARGET_LENGTH + "}"; + private static final Pattern TARGET_PATTERN = Pattern.compile(TARGET_REGEX); + + /** + * The default queue name. Keep this in sync with + * {@link com.google.appengine.api.taskqueue.Queue#DEFAULT_QUEUE}. + */ + private static final String DEFAULT_QUEUE = "default"; + + /** + * Enumerates the allowed units for Queue rate. + */ + public enum RateUnit { + SECOND('s', 1), + MINUTE('m', SECOND.getSeconds() * 60), + HOUR('h', MINUTE.getSeconds() * 60), + DAY('d', HOUR.getSeconds() * 24); + + final char ident; + final int seconds; + + RateUnit(char ident, int seconds) { + this.ident = ident; + this.seconds = seconds; + } + + static RateUnit valueOf(char unit) { + switch (unit) { + case 's' : return SECOND; + case 'm' : return MINUTE; + case 'h' : return HOUR; + case 'd' : return DAY; + } + throw new AppEngineConfigException("Invalid rate was specified."); + } + + public char getIdent() { + return ident; + } + + public int getSeconds() { + return seconds; + } + } + + /** + * Access control list for a queue. + */ + public static class AclEntry { + private String userEmail; + private String writerEmail; + + public AclEntry() { + userEmail = null; + writerEmail = null; + } + + public void setUserEmail(String userEmail) { + this.userEmail = userEmail; + } + + public String getUserEmail() { + return userEmail; + } + + public void setWriterEmail(String writerEmail) { + this.writerEmail = writerEmail; + } + + public String getWriterEmail() { + return writerEmail; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((userEmail == null) ? 0 : userEmail.hashCode()); + result = prime * result + ((writerEmail == null) ? 0 : writerEmail.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AclEntry other = (AclEntry) obj; + if (userEmail == null) { + if (other.userEmail != null) return false; + } else if (!userEmail.equals(other.userEmail)) return false; + if (writerEmail == null) { + if (other.writerEmail != null) return false; + } else if (!writerEmail.equals(other.writerEmail)) return false; + return true; + } + } + + /** + * Describes a queue's optional retry parameters. + */ + public static class RetryParameters { + private Integer retryLimit; + private Integer ageLimitSec; + private Double minBackoffSec; + private Double maxBackoffSec; + private Integer maxDoublings; + + public RetryParameters() { + retryLimit = null; + ageLimitSec = null; + minBackoffSec = null; + maxBackoffSec = null; + maxDoublings = null; + } + + public Integer getRetryLimit() { + return retryLimit; + } + + public void setRetryLimit(int retryLimit) { + this.retryLimit = retryLimit; + } + + public void setRetryLimit(String retryLimit) { + this.retryLimit = Integer.valueOf(retryLimit); + } + + public Integer getAgeLimitSec() { + return ageLimitSec; + } + + public void setAgeLimitSec(String ageLimitString) { + Matcher matcher = TASK_AGE_LIMIT_PATTERN.matcher(ageLimitString); + if (!matcher.matches() || matcher.groupCount() != 2) { + throw new AppEngineConfigException("Invalid task age limit was specified."); + } + double rateUnitSec = RateUnit.valueOf(matcher.group(2).charAt(0)).getSeconds(); + Double ageLimit = Double.valueOf(matcher.group(1)) * rateUnitSec; + this.ageLimitSec = ageLimit.intValue(); + } + + public Double getMinBackoffSec() { + return minBackoffSec; + } + + public void setMinBackoffSec(double minBackoffSec) { + this.minBackoffSec = minBackoffSec; + } + + public void setMinBackoffSec(String minBackoffSec) { + this.minBackoffSec = Double.valueOf(minBackoffSec); + } + + public Double getMaxBackoffSec() { + return maxBackoffSec; + } + + public void setMaxBackoffSec(double maxBackoffSec) { + this.maxBackoffSec = maxBackoffSec; + } + + public void setMaxBackoffSec(String maxBackoffSec) { + this.maxBackoffSec = Double.valueOf(maxBackoffSec); + } + + public Integer getMaxDoublings() { + return maxDoublings; + } + + public void setMaxDoublings(int maxDoublings) { + this.maxDoublings = maxDoublings; + } + + public void setMaxDoublings(String maxDoublings) { + this.maxDoublings = Integer.valueOf(maxDoublings); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((ageLimitSec == null) ? 0 : ageLimitSec.hashCode()); + result = prime * result + ((maxBackoffSec == null) ? 0 : maxBackoffSec.hashCode()); + result = prime * result + ((maxDoublings == null) ? 0 : maxDoublings.hashCode()); + result = prime * result + ((minBackoffSec == null) ? 0 : minBackoffSec.hashCode()); + result = prime * result + ((retryLimit == null) ? 0 : retryLimit.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + RetryParameters other = (RetryParameters) obj; + if (ageLimitSec == null) { + if (other.ageLimitSec != null) return false; + } else if (!ageLimitSec.equals(other.ageLimitSec)) return false; + if (maxBackoffSec == null) { + if (other.maxBackoffSec != null) return false; + } else if (!maxBackoffSec.equals(other.maxBackoffSec)) return false; + if (maxDoublings == null) { + if (other.maxDoublings != null) return false; + } else if (!maxDoublings.equals(other.maxDoublings)) return false; + if (minBackoffSec == null) { + if (other.minBackoffSec != null) return false; + } else if (!minBackoffSec.equals(other.minBackoffSec)) return false; + if (retryLimit == null) { + if (other.retryLimit != null) return false; + } else if (!retryLimit.equals(other.retryLimit)) return false; + return true; + } + } + + /** + * Describes a single queue entry. + */ + public static class Entry { + private String name; + private Double rate; + private RateUnit rateUnit; + private Integer bucketSize; + private Integer maxConcurrentRequests; + private RetryParameters retryParameters; + private String target; + private String mode; + private List acl; + + /** Create an empty queue entry. */ + public Entry() { + name = null; + rate = null; + rateUnit = RateUnit.SECOND; + bucketSize = null; + maxConcurrentRequests = null; + retryParameters = null; + target = null; + mode = null; + acl = null; + } + + public Entry(String name, double rate, RateUnit rateUnit, int bucketSize, + Integer maxConcurrentRequests, String target) { + this.name = name; + this.rate = rate; + this.rateUnit = rateUnit; + this.bucketSize = bucketSize; + this.maxConcurrentRequests = maxConcurrentRequests; + this.target = target; + } + + public String getName() { + return name; + } + + public void setName(String queueName) { + if (queueName == null || queueName.length() == 0 || + !QUEUE_NAME_PATTERN.matcher(queueName).matches()) { + throw new AppEngineConfigException( + "Queue name does not match expression " + QUEUE_NAME_PATTERN + + "; found '" + queueName + "'"); + } + this.name = queueName; + } + + public void setMode(String mode) { + if (mode == null || mode.length() == 0 || + !MODE_PATTERN.matcher(mode).matches()) { + throw new AppEngineConfigException( + "mode must be either 'push' or 'pull'"); + } + this.mode = mode; + } + + public String getMode() { + return mode; + } + + public List getAcl() { + return acl; + } + + public void setAcl(List acl) { + this.acl = acl; + } + + public void addAcl(AclEntry aclEntry) { + this.acl.add(aclEntry); + } + + public Double getRate() { + return rate; + } + + public void setRate(double rate) { + this.rate = rate; + } + + /** + * Set rate and units based on a "number/unit" formatted string. + * @param rateString My be "0" or "number/unit" where unit is 's|m|h|d'. + */ + public void setRate(String rateString) { + if (rateString.equals("0")) { + rate = 0.0; + rateUnit = RateUnit.SECOND; + return; + } + Matcher matcher = RATE_PATTERN.matcher(rateString); + if (!matcher.matches()) { + throw new AppEngineConfigException("Invalid queue rate was specified."); + } + String digits = matcher.group(1); + rateUnit = RateUnit.valueOf(matcher.group(3).charAt(0)); + rate = Double.valueOf(digits); + } + + public RateUnit getRateUnit() { + return rateUnit; + } + + public void setRateUnit(RateUnit rateUnit) { + this.rateUnit = rateUnit; + } + + public Integer getBucketSize() { + return bucketSize; + } + + public void setBucketSize(int bucketSize) { + this.bucketSize = bucketSize; + } + + public void setBucketSize(String bucketSize) { + try { + this.bucketSize = Integer.valueOf(bucketSize); + } catch (NumberFormatException exception) { + throw new AppEngineConfigException("Invalid bucket-size was specified.", exception); + } + } + + public Integer getMaxConcurrentRequests() { + return maxConcurrentRequests; + } + + public void setMaxConcurrentRequests(int maxConcurrentRequests) { + this.maxConcurrentRequests = maxConcurrentRequests; + } + + public void setMaxConcurrentRequests(String maxConcurrentRequests) { + try { + this.maxConcurrentRequests = Integer.valueOf(maxConcurrentRequests); + } catch (NumberFormatException exception) { + throw new AppEngineConfigException("Invalid max-concurrent-requests was specified: '" + + maxConcurrentRequests + "'", exception); + } + } + + public RetryParameters getRetryParameters() { + return retryParameters; + } + + public void setRetryParameters(RetryParameters retryParameters) { + this.retryParameters = retryParameters; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + Matcher matcher = TARGET_PATTERN.matcher(target); + if (!matcher.matches()) { + throw new AppEngineConfigException("Invalid queue target was specified. Target: '" + + target + "'"); + } + this.target = target; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((acl == null) ? 0 : acl.hashCode()); + result = prime * result + ((bucketSize == null) ? 0 : bucketSize.hashCode()); + result = + prime * result + ((maxConcurrentRequests == null) ? 0 : maxConcurrentRequests.hashCode()); + result = prime * result + ((mode == null) ? 0 : mode.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((rate == null) ? 0 : rate.hashCode()); + result = prime * result + ((rateUnit == null) ? 0 : rateUnit.hashCode()); + result = prime * result + ((target == null) ? 0 : target.hashCode()); + result = prime * result + ((retryParameters == null) ? 0 : retryParameters.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Entry other = (Entry) obj; + if (acl == null) { + if (other.acl != null) return false; + } else if (!acl.equals(other.acl)) return false; + if (bucketSize == null) { + if (other.bucketSize != null) return false; + } else if (!bucketSize.equals(other.bucketSize)) return false; + if (maxConcurrentRequests == null) { + if (other.maxConcurrentRequests != null) return false; + } else if (!maxConcurrentRequests.equals(other.maxConcurrentRequests)) return false; + if (mode == null) { + if (other.mode != null) return false; + } else if (!mode.equals(other.mode)) return false; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + if (rate == null) { + if (other.rate != null) return false; + } else if (!rate.equals(other.rate)) return false; + if (rateUnit == null) { + if (other.rateUnit != null) return false; + } else if (!rateUnit.equals(other.rateUnit)) return false; + if (target == null) { + if (other.target != null) return false; + } else if (!target.equals(other.target)) return false; + if (retryParameters == null) { + if (other.retryParameters != null) return false; + } else if (!retryParameters.equals(other.retryParameters)) return false; + return true; + } + } + + private final LinkedHashMap entries = new LinkedHashMap(); + private Entry lastEntry; + + private String totalStorageLimit = ""; + + /** + * Return a new {@link Entry} describing the default queue. + */ + public static Entry defaultEntry() { + return new Entry(DEFAULT_QUEUE, 5, RateUnit.SECOND, 5, null, null); + } + + /** + * Puts a new entry into the list defined by the queue.xml file. + * + * @throws AppEngineConfigException if the previously-last entry is still + * incomplete. + * @return the new entry + */ + public Entry addNewEntry() { + validateLastEntry(); + lastEntry = new Entry(); + return lastEntry; + } + + public void addEntry(Entry entry) { + validateLastEntry(); + lastEntry = entry; + validateLastEntry(); + } + + /** + * Get the entries. + */ + public Collection getEntries() { + validateLastEntry(); + return entries.values(); + } + + /** + * Check that the last entry defined is complete. + * @throws AppEngineConfigException if it is not. + */ + public void validateLastEntry() { + if (lastEntry == null) { + return; + } + if (lastEntry.getName() == null) { + throw new AppEngineConfigException("Queue entry must have a name."); + } + if (entries.containsKey(lastEntry.getName())) { + throw new AppEngineConfigException("Queue entry has duplicate name."); + } + if ("pull".equals(lastEntry.getMode())) { + if (lastEntry.getRate() != null) { + throw new AppEngineConfigException("Rate must not be specified for pull queue."); + } + if (lastEntry.getBucketSize() != null) { + throw new AppEngineConfigException("Bucket size must not be specified for pull queue."); + } + if (lastEntry.getMaxConcurrentRequests() != null) { + throw new AppEngineConfigException( + "MaxConcurrentRequests must not be specified for pull queue."); + } + RetryParameters retryParameters = lastEntry.getRetryParameters(); + if (retryParameters != null) { + if (retryParameters.getAgeLimitSec() != null) { + throw new AppEngineConfigException( + "Age limit must not be specified for pull queue."); + } + if (retryParameters.getMinBackoffSec() != null) { + throw new AppEngineConfigException( + "Min backoff must not be specified for pull queue."); + } + if (retryParameters.getMaxBackoffSec() != null) { + throw new AppEngineConfigException( + "Max backoff must not be specified for pull queue."); + } + if (retryParameters.getMaxDoublings() != null) { + throw new AppEngineConfigException( + "Max doublings must not be specified for pull queue."); + } + } + } else { + if (lastEntry.getRate() == null) { + throw new AppEngineConfigException("A queue rate is required for push queue."); + } + } + entries.put(lastEntry.getName(), lastEntry); + lastEntry = null; + } + + public void setTotalStorageLimit(String s) { + totalStorageLimit = s; + } + + public String getTotalStorageLimit() { + return totalStorageLimit; + } + + /** + * Get the YAML equivalent of this queue.xml file. + * + * @return contents of an equivalent {@code queue.yaml} file. + */ + public String toYaml() { + StringBuilder builder = new StringBuilder(); + if (getTotalStorageLimit().length() > 0) { + builder.append("total_storage_limit: " + getTotalStorageLimit() + "\n\n"); + } + builder.append("queue:\n"); + for (Entry ent : getEntries()) { + builder.append("- name: " + ent.getName() + "\n"); + Double rate = ent.getRate(); + if (rate != null) { + builder.append( + " rate: " + rate + '/' + ent.getRateUnit().getIdent() + "\n"); + } + Integer bucketSize = ent.getBucketSize(); + if (bucketSize != null) { + builder.append(" bucket_size: " + bucketSize + "\n"); + } + Integer maxConcurrentRequests = ent.getMaxConcurrentRequests(); + if (maxConcurrentRequests != null) { + builder.append(" max_concurrent_requests: " + maxConcurrentRequests + "\n"); + } + RetryParameters retryParameters = ent.getRetryParameters(); + if (retryParameters != null) { + builder.append(" retry_parameters:\n"); + if (retryParameters.getRetryLimit() != null) { + builder.append(" task_retry_limit: " + retryParameters.getRetryLimit() + "\n"); + } + if (retryParameters.getAgeLimitSec() != null) { + builder.append(" task_age_limit: " + retryParameters.getAgeLimitSec() + "s\n"); + } + if (retryParameters.getMinBackoffSec() != null) { + builder.append(" min_backoff_seconds: " + retryParameters.getMinBackoffSec() + "\n"); + } + if (retryParameters.getMaxBackoffSec() != null) { + builder.append(" max_backoff_seconds: " + retryParameters.getMaxBackoffSec() + "\n"); + } + if (retryParameters.getMaxDoublings() != null) { + builder.append(" max_doublings: " + retryParameters.getMaxDoublings() + "\n"); + } + } + String target = ent.getTarget(); + if (target != null) { + builder.append(" target: " + target + "\n"); + } + String mode = ent.getMode(); + if (mode != null) { + builder.append(" mode: " + mode + "\n"); + } + List acl = ent.getAcl(); + if (acl != null) { + builder.append(" acl:\n"); + for (AclEntry aclEntry : acl) { + if (aclEntry.getUserEmail() != null) { + builder.append(" - user_email: " + aclEntry.getUserEmail() + "\n"); + } else if (aclEntry.getWriterEmail() != null) { + builder.append(" - writer_email: " + aclEntry.getWriterEmail() + "\n"); + } + } + } + } + return builder.toString(); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/QueueXmlReader.java b/java/src/main/com/google/apphosting/utils/config/QueueXmlReader.java new file mode 100644 index 00000000..87a1480c --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/QueueXmlReader.java @@ -0,0 +1,243 @@ +// Copyright 2009 Google Inc. All Rights Reserved. +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser.Node; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Stack; + +/** + * Creates an {@link QueueXml} instance from + * WEB-INF/queue.xml. If you want to read the configuration + * from a different file, subclass and override {@link #getFilename()}. If you + * want to read the configuration from something that isn't a file, subclass + * and override {@link #getInputStream()}. + * + */ +public class QueueXmlReader extends AbstractConfigXmlReader { + + private static final String FILENAME = "WEB-INF/queue.xml"; + + private static final String TOTAL_STORAGE_LIMIT_TAG = "total-storage-limit"; + private static final String QUEUEENTRIES_TAG = "queue-entries"; + private static final String QUEUE_TAG = "queue"; + private static final String NAME_TAG = "name"; + private static final String RATE_TAG = "rate"; + private static final String BUCKET_SIZE = "bucket-size"; + private static final String MAX_CONCURRENT_REQUESTS = "max-concurrent-requests"; + private static final String MODE_TAG = "mode"; + + private static final String RETRY_PARAMETERS_TAG = "retry-parameters"; + private static final String TASK_RETRY_LIMIT_TAG = "task-retry-limit"; + private static final String TASK_AGE_LIMIT_TAG = "task-age-limit"; + private static final String MIN_BACKOFF_SECONDS_TAG = "min-backoff-seconds"; + private static final String MAX_BACKOFF_SECONDS_TAG = "max-backoff-seconds"; + private static final String MAX_DOUBLINGS_TAG = "max-doublings"; + private static final String TARGET_TAG = "target"; + + private static final String ACL_TAG = "acl"; + private static final String USER_EMAIL_TAG = "user-email"; + private static final String WRITER_EMAIL_TAG = "writer-email"; + + /** + * Constructs the reader for {@code queue.xml} in a given application directory. + * @param appDir the application directory + */ + public QueueXmlReader(String appDir) { + super(appDir, false); + } + + /** + * Parses the config file. + * @return A {@link QueueXml} object representing the parsed configuration. + */ + public QueueXml readQueueXml() { + return readConfigXml(); + } + + @Override + protected QueueXml processXml(InputStream is) { + final QueueXml queueXml = new QueueXml(); + parse(new ParserCallback() { + boolean firstQueueEntriesTag = true; + boolean firstTotalStorageLimitTag = true; + boolean insideRetryParametersTag = false; + boolean insideAclTag = false; + QueueXml.Entry entry; + + @Override + public void newNode(Node node, Stack ancestors) { + switch (ancestors.size()) { + case 0: + if (QUEUEENTRIES_TAG.equalsIgnoreCase(node.getTag())) { + if (!firstQueueEntriesTag) { + throw new AppEngineConfigException(getFilename() + " contains multiple <" + + QUEUEENTRIES_TAG + ">"); + } + firstQueueEntriesTag = false; + } + break; + + case 1: + if (firstQueueEntriesTag) { + throw new AppEngineConfigException(getFilename() + " does not contain <" + + QUEUEENTRIES_TAG + ">"); + } + if (TOTAL_STORAGE_LIMIT_TAG.equalsIgnoreCase(node.getTag())) { + if (!firstTotalStorageLimitTag) { + throw new AppEngineConfigException(getFilename() + " contains multiple <" + + TOTAL_STORAGE_LIMIT_TAG + ">"); + } + if (node.size() == 1 && node.get(0) instanceof String) { + queueXml.setTotalStorageLimit(getString(node)); + } else { + throw new AppEngineConfigException(getFilename() + "has invalid <" + +TOTAL_STORAGE_LIMIT_TAG + ">"); + } + firstTotalStorageLimitTag = false; + } else if (QUEUE_TAG.equalsIgnoreCase(node.getTag())) { + entry = queueXml.addNewEntry(); + } else { + throw new AppEngineConfigException(getFilename() + " contains <" + + node.getTag() + "> instead of <" + QUEUE_TAG + "/> or <" + + TOTAL_STORAGE_LIMIT_TAG + ">"); + } + break; + + case 2: + assert(entry != null); + if (NAME_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setName(getString(node)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + NAME_TAG + ">"); + } + } else if (BUCKET_SIZE.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setBucketSize(getString(node)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + BUCKET_SIZE + ">"); + } + } else if (RATE_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setRate(getString(node)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + RATE_TAG + ">"); + } + } else if (MAX_CONCURRENT_REQUESTS.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setMaxConcurrentRequests(getString(node)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + MAX_CONCURRENT_REQUESTS + ">"); + } + } else if (MODE_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setMode(getString(node)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + MODE_TAG + ">"); + } + } else if (TARGET_TAG.equalsIgnoreCase(node.getTag())) { + if (node.size() == 1 && node.get(0) instanceof String) { + entry.setTarget(getString(node)); + } else { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + TARGET_TAG + ">"); + } + } else if (RETRY_PARAMETERS_TAG.equalsIgnoreCase(node.getTag())) { + entry.setRetryParameters(new QueueXml.RetryParameters()); + insideRetryParametersTag = true; + insideAclTag = false; + } else if (ACL_TAG.equalsIgnoreCase(node.getTag())) { + entry.setAcl(new ArrayList()); + insideAclTag = true; + insideRetryParametersTag = false; + } else { + throw new AppEngineConfigException(getFilename() + " contains unknown <" + + node.getTag() + "> inside <" + QUEUE_TAG + "/>"); + } + break; + + case 3: + assert(insideRetryParametersTag ^ insideAclTag); + assert(entry != null); + boolean brokenTag = !(node.size() == 1 && node.get(0) instanceof String); + + if (insideRetryParametersTag) { + assert(entry.getRetryParameters() != null); + QueueXml.RetryParameters retryParameters = entry.getRetryParameters(); + + if (TASK_RETRY_LIMIT_TAG.equalsIgnoreCase(node.getTag())) { + if (!brokenTag) { + retryParameters.setRetryLimit(getString(node)); + } + } else if (TASK_AGE_LIMIT_TAG.equalsIgnoreCase(node.getTag())) { + if (!brokenTag) { + retryParameters.setAgeLimitSec(getString(node)); + } + } else if (MIN_BACKOFF_SECONDS_TAG.equalsIgnoreCase(node.getTag())) { + if (!brokenTag) { + retryParameters.setMinBackoffSec(getString(node)); + } + } else if (MAX_BACKOFF_SECONDS_TAG.equalsIgnoreCase(node.getTag())) { + if (!brokenTag) { + retryParameters.setMaxBackoffSec(getString(node)); + } + } else if (MAX_DOUBLINGS_TAG.equalsIgnoreCase(node.getTag())) { + if (!brokenTag) { + retryParameters.setMaxDoublings(getString(node)); + } + } else { + throw new AppEngineConfigException(getFilename() + " contains unknown <" + + node.getTag() + "> inside <" + RETRY_PARAMETERS_TAG + "/>"); + } + } + + if (insideAclTag) { + assert(entry.getAcl() != null); + if (USER_EMAIL_TAG.equalsIgnoreCase(node.getTag())) { + if (!brokenTag) { + QueueXml.AclEntry acl = new QueueXml.AclEntry(); + acl.setUserEmail(getString(node)); + entry.getAcl().add(acl); + } + } else if (WRITER_EMAIL_TAG.equalsIgnoreCase(node.getTag())) { + if (!brokenTag) { + QueueXml.AclEntry acl = new QueueXml.AclEntry(); + acl.setWriterEmail(getString(node)); + entry.getAcl().add(acl); + } + } else { + throw new AppEngineConfigException(getFilename() + " contains unknown <" + + node.getTag() + "> inside <" + ACL_TAG + "/>"); + } + } + + if (brokenTag) { + throw new AppEngineConfigException(getFilename() + " has bad contents in <" + + node.getTag() + ">"); + } + break; + + default: + throw new AppEngineConfigException(getFilename() + + " has a syntax error; node <" + + node.getTag() + "> is too deeply nested to be valid."); + } + } + }, is); + queueXml.validateLastEntry(); + return queueXml; + } + + @Override + protected String getRelativeFilename() { + return FILENAME; + } + +} diff --git a/java/src/main/com/google/apphosting/utils/config/QueueYamlReader.java b/java/src/main/com/google/apphosting/utils/config/QueueYamlReader.java new file mode 100644 index 00000000..717f5059 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/QueueYamlReader.java @@ -0,0 +1,241 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.io.File; +import java.io.FileReader; +import java.io.FileNotFoundException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import net.sourceforge.yamlbeans.YamlException; +import net.sourceforge.yamlbeans.YamlReader; + +/** + * Class to parse queue.yaml into a QueueXml object. + * + */ +public class QueueYamlReader { + + /** + * Wrapper around QueueXml to match JavaBean properties to + * the yaml file syntax. + */ + public static class QueueYaml { + + /** + * Wrapper around QueueXml.RetryParameters to match the JavaBean + * properties to the yaml file syntax. + */ + public static class RetryParameters { + private QueueXml.RetryParameters retryParameters = new QueueXml.RetryParameters(); + + public RetryParameters() {} + + public void setTask_retry_limit(int limit) { + retryParameters.setRetryLimit(limit); + } + public int getTask_retry_limit() { + return retryParameters.getRetryLimit(); + } + public void setTask_age_limit(String ageLimit) { + retryParameters.setAgeLimitSec(ageLimit); + } + public String getTask_age_limit() { + return retryParameters.getAgeLimitSec().toString() + "s"; + } + public void setMin_backoff_seconds(double backoff) { + retryParameters.setMinBackoffSec(backoff); + } + public double getMin_backoff_seconds() { + return retryParameters.getMinBackoffSec(); + } + public void setMax_backoff_seconds(double backoff) { + retryParameters.setMaxBackoffSec(backoff); + } + public double getMax_backoff_seconds() { + return retryParameters.getMaxBackoffSec(); + } + public void setMax_doublings(int doublings) { + retryParameters.setMaxDoublings(doublings); + } + public int getMax_doublings() { + return retryParameters.getMaxDoublings(); + } + public QueueXml.RetryParameters toXml() { + return retryParameters; + } + } + + public static class AclEntry { + QueueXml.AclEntry acl = new QueueXml.AclEntry(); + + public void setUser_email(String userEmail) { + acl.setUserEmail(userEmail); + } + + public String getUser_email() { + return acl.getUserEmail(); + } + + public void setWriter_email(String writerEmail) { + acl.setWriterEmail(writerEmail); + } + + public String getWriter_email() { + return acl.getWriterEmail(); + } + + public QueueXml.AclEntry toXml() { + return acl; + } + } + + /** + * Wrapper around QueueXml.Entry to match the JavaBean properties to + * the yaml file syntax. + */ + public static class Entry { + private QueueXml.Entry entry = new QueueXml.Entry(); + private RetryParameters retryParameters; + private List acl; + public void setName(String name) { + entry.setName(name); + } + public String getName() { + return entry.getName(); + } + public void setRate(String rate) { + entry.setRate(rate); + } + public String getRate() { + return entry.getRate() + "/" + entry.getRateUnit().getIdent(); + } + public void setBucket_size(int size) { + entry.setBucketSize(size); + } + public int getBucket_size() { + return entry.getBucketSize(); + } + public void setMax_concurrent_requests(int size) { + entry.setMaxConcurrentRequests(size); + } + public int getMax_concurrent_requests() { + return entry.getMaxConcurrentRequests(); + } + public void setRetry_parameters(RetryParameters retryParameters) { + this.retryParameters = retryParameters; + if (retryParameters != null) { + entry.setRetryParameters(retryParameters.toXml()); + } else { + entry.setRetryParameters(new QueueXml.RetryParameters()); + } + } + public RetryParameters getRetry_parameters() { + return retryParameters; + } + public void setTarget(String target) { + entry.setTarget(target); + } + public String getTarget() { + return entry.getTarget(); + } + public void setMode(String mode) { + entry.setMode(mode); + } + public String getMode() { + return entry.getMode(); + } + public void setAcl(List acl) { + this.acl = acl; + entry.setAcl(new ArrayList()); + if (acl != null) { + for (int i = 0; i < acl.size(); ++i) { + AclEntry aclEntry = acl.get(i); + entry.addAcl(aclEntry.toXml()); + } + } + } + public List getAcl() { + return acl; + } + public QueueXml.Entry toXml() { + return entry; + } + } + + private List entries; + public String total_storage_limit; + + public List getQueue() { + return entries; + } + + public void setQueue(List entries) { + this.entries = entries; + } + + public QueueXml toXml() { + QueueXml xml = new QueueXml(); + if (total_storage_limit != null) { + xml.setTotalStorageLimit(total_storage_limit); + } + if (entries != null) { + for (Entry entry : entries) { + xml.addEntry(entry.toXml()); + } + } + return xml; + } + } + + private static final String FILENAME = "queue.yaml"; + private String appDir; + + public QueueYamlReader(String appDir) { + if (appDir.length() > 0 && appDir.charAt(appDir.length() - 1) != File.separatorChar) { + appDir += File.separatorChar; + } + this.appDir = appDir; + } + + public String getFilename() { + return appDir + QueueYamlReader.FILENAME; + } + + public QueueXml parse() { + if (new File(getFilename()).exists()) { + try { + return parse(new FileReader(getFilename())); + } catch (FileNotFoundException ex) { + throw new AppEngineConfigException("Cannot find file " + getFilename(), ex); + } + } + return null; + } + + public static QueueXml parse(Reader yaml) { + YamlReader reader = new YamlReader(yaml); + reader.getConfig().setPropertyElementType(QueueYaml.class, + "queue", + QueueYaml.Entry.class); + + reader.getConfig().setPropertyElementType(QueueYaml.Entry.class, + "acl", + QueueYaml.AclEntry.class); + try { + QueueYaml queueYaml = reader.read(QueueYaml.class); + if (queueYaml == null) { + throw new AppEngineConfigException("Empty queue configuration."); + } + return queueYaml.toXml(); + } catch (YamlException ex) { + throw new AppEngineConfigException(ex.getMessage(), ex); + } + } + + public static QueueXml parse(String yaml) { + return parse(new StringReader(yaml)); + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/WebModule.java b/java/src/main/com/google/apphosting/utils/config/WebModule.java new file mode 100644 index 00000000..3c164d7f --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/WebModule.java @@ -0,0 +1,156 @@ +package com.google.apphosting.utils.config; + +import com.google.common.base.StringUtil; + +import java.io.File; + +/** + * Holder for information for a web module extracted from the module's + * on disk application directory. + * + */ +public class WebModule { + /** + * Default value for a module name. + */ + public static final String DEFAULT_MODULE_NAME = "default"; + + private final File applicationDirectory; + private final AppEngineWebXml appEngineWebXml; + private final File appEngineWebXmlFile; + private final WebXml webXml; + private final File webXmlFile; + private final String contextRoot; + + /** + * Returns the server name specified in {@link #getAppEngineWebXml()} or + * {@link #DEFAULT_SERVER_NAME} if none is specified. + */ + public static String getModuleName(AppEngineWebXml appEngineWebXml) { + return StringUtil.isEmptyOrWhitespace(appEngineWebXml.getModule()) ? + "default" : appEngineWebXml.getModule().trim(); + } + + WebModule(File applicationDirectory, AppEngineWebXml appEngineWebXml, File appEngineWebXmlFile, + WebXml webXml, File webXmlFile, String contextRoot) { + this.applicationDirectory = applicationDirectory; + this.appEngineWebXml = appEngineWebXml; + this.appEngineWebXmlFile = appEngineWebXmlFile; + this.webXml = webXml; + this.webXmlFile = webXmlFile; + this.contextRoot = contextRoot; + } + + public File getApplicationDirectory() { + return applicationDirectory; + } + + public AppEngineWebXml getAppEngineWebXml() { + return appEngineWebXml; + } + + public File getAppEngineWebXmlFile() { + return appEngineWebXmlFile; + } + + public WebXml getWebXml() { + return webXml; + } + + public File getWebXmlFile() { + return webXmlFile; + } + + public String getContextRoot() { + return contextRoot; + } + + /** + * Returns the module name specified in {@link #getAppEngineWebXml()} or + * {@link #DEFAULT_MODULE_NAME} if none is specified. + */ + public String getModuleName() { + return getModuleName(appEngineWebXml); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((appEngineWebXml == null) ? 0 : appEngineWebXml.hashCode()); + result = prime * result + ((appEngineWebXmlFile == null) ? 0 : appEngineWebXmlFile.hashCode()); + result = + prime * result + ((applicationDirectory == null) ? 0 : applicationDirectory.hashCode()); + result = prime * result + ((contextRoot == null) ? 0 : contextRoot.hashCode()); + result = prime * result + ((webXml == null) ? 0 : webXml.hashCode()); + result = prime * result + ((webXmlFile == null) ? 0 : webXmlFile.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + WebModule other = (WebModule) obj; + if (appEngineWebXml == null) { + if (other.appEngineWebXml != null) { + return false; + } + } else if (!appEngineWebXml.equals(other.appEngineWebXml)) { + return false; + } + if (appEngineWebXmlFile == null) { + if (other.appEngineWebXmlFile != null) { + return false; + } + } else if (!appEngineWebXmlFile.equals(other.appEngineWebXmlFile)) { + return false; + } + if (applicationDirectory == null) { + if (other.applicationDirectory != null) { + return false; + } + } else if (!applicationDirectory.equals(other.applicationDirectory)) { + return false; + } + if (contextRoot == null) { + if (other.contextRoot != null) { + return false; + } + } else if (!contextRoot.equals(other.contextRoot)) { + return false; + } + if (webXml == null) { + if (other.webXml != null) { + return false; + } + } else if (!webXml.equals(other.webXml)) { + return false; + } + if (webXmlFile == null) { + if (other.webXmlFile != null) { + return false; + } + } else if (!webXmlFile.equals(other.webXmlFile)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "WebModule: applicationDirectory=" + applicationDirectory + + " appEngineWebXml=" + appEngineWebXml + + " appEngineWebXmlFile=" + appEngineWebXmlFile + + " webXml=" + webXml + + " webXmlFile=" + webXmlFile + + " contextRoot=" + contextRoot; + } +} \ No newline at end of file diff --git a/java/src/main/com/google/apphosting/utils/config/WebXml.java b/java/src/main/com/google/apphosting/utils/config/WebXml.java new file mode 100644 index 00000000..9e1d3412 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/WebXml.java @@ -0,0 +1,163 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * + * + */ +public class WebXml { + private final List servletPatterns; + private final List securityConstraints; + private final List welcomeFiles; + private final Map mimeMappings; + private final Map patternToId; + private boolean fallThroughToRuntime; + + public WebXml() { + servletPatterns = new ArrayList(); + securityConstraints = new ArrayList(); + welcomeFiles = new ArrayList(); + mimeMappings = new HashMap(); + patternToId = new HashMap(); + } + + /** + * Returns true if {@code url} matches one of the servlets or + * servlet filters listed in this web.xml. + */ + public boolean matches(String url) { + for (String pattern : servletPatterns) { + if (pattern.length() == 0) { + continue; + } + if (pattern.startsWith("*") && url.endsWith(pattern.substring(1))) { + return true; + } else if (pattern.endsWith("*") && + url.startsWith(pattern.substring(0, pattern.length() - 1))) { + return true; + } else if (url.equals(pattern)) { + return true; + } + } + return false; + } + + public String getHandlerIdForPattern(String pattern) { + return patternToId.get(pattern); + } + + public void addServletPattern(String urlPattern, String id) { + YamlUtils.validateUrl(urlPattern); + servletPatterns.add(urlPattern); + if (id != null) { + patternToId.put(urlPattern, id); + } + } + + public List getServletPatterns() { + return servletPatterns; + } + + public List getSecurityConstraints() { + return securityConstraints; + } + + public SecurityConstraint addSecurityConstraint() { + SecurityConstraint context = new SecurityConstraint(); + securityConstraints.add(context); + return context; + } + + public void addWelcomeFile(String welcomeFile) { + welcomeFiles.add(welcomeFile); + } + + public List getWelcomeFiles() { + return welcomeFiles; + } + + public void addMimeMapping(String extension, String mimeType) { + mimeMappings.put(extension, mimeType); + } + + public Map getMimeMappings() { + return mimeMappings; + } + + public String getMimeTypeForPath(String path) { + int dot = path.lastIndexOf("."); + if (dot != -1) { + return mimeMappings.get(path.substring(dot + 1)); + } else { + return null; + } + } + + public boolean getFallThroughToRuntime() { + return fallThroughToRuntime; + } + + public void setFallThroughToRuntime(boolean fallThroughToRuntime) { + this.fallThroughToRuntime = fallThroughToRuntime; + } + + /** + * Performs some optional validation on this {@code WebXml}. + * + * @throws AppEngineConfigException If any errors are found. + */ + public void validate() { + for (String welcomeFile : welcomeFiles) { + if (welcomeFile.startsWith("/")) { + throw new AppEngineConfigException("Welcome files must be relative paths: " + welcomeFile); + } + } + } + + /** + * Information about a security context, requiring SSL and/or authentication. + * Effectively, this is a tuple of { urlpatterns..., ssl-guarantee, auth-role }. + */ + public static class SecurityConstraint { + public enum RequiredRole { NONE, ANY_USER, ADMIN } + public enum TransportGuarantee { NONE, INTEGRAL, CONFIDENTIAL } + + private final List patterns; + private TransportGuarantee transportGuarantee = TransportGuarantee.NONE; + private RequiredRole requiredRole = RequiredRole.NONE; + + private SecurityConstraint() { + patterns = new ArrayList(); + } + + public List getUrlPatterns() { + return patterns; + } + + public void addUrlPattern(String pattern) { + patterns.add(pattern); + } + + public TransportGuarantee getTransportGuarantee() { + return transportGuarantee; + } + + public void setTransportGuarantee(TransportGuarantee transportGuarantee) { + this.transportGuarantee = transportGuarantee; + } + + public RequiredRole getRequiredRole() { + return requiredRole; + } + + public void setRequiredRole(RequiredRole requiredRole) { + this.requiredRole = requiredRole; + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/WebXmlReader.java b/java/src/main/com/google/apphosting/utils/config/WebXmlReader.java new file mode 100644 index 00000000..e45e276f --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/WebXmlReader.java @@ -0,0 +1,233 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import com.google.apphosting.utils.config.WebXml.SecurityConstraint; + +import org.mortbay.xml.XmlParser; +import org.mortbay.xml.XmlParser.Node; + +import java.io.InputStream; +import java.net.URL; +import java.util.Stack; + +/** + * This reads {@code web.xml}. + * + * + */ +public class WebXmlReader extends AbstractConfigXmlReader { + + private static final String[] DEFAULT_WELCOME_FILES = new String[] { + "index.html", + "index.jsp", + }; + + public static final String DEFAULT_RELATIVE_FILENAME = "WEB-INF/web.xml"; + + private static final String URLPATTERN_TAG = "url-pattern"; + private static final String SERVLETMAP_TAG = "servlet-mapping"; + private static final String FILTERMAP_TAG = "filter-mapping"; + private static final String SECURITYCONST_TAG = "security-constraint"; + private static final String AUTHCONST_TAG = "auth-constraint"; + private static final String ROLENAME_TAG = "role-name"; + private static final String USERDATACONST_TAG = "user-data-constraint"; + private static final String TRANSGUARANTEE_TAG = "transport-guarantee"; + private static final String WELCOME_FILE_LIST_TAG = "welcome-file-list"; + private static final String WELCOME_FILE_TAG = "welcome-file"; + private static final String EXTENSION_TAG = "extension"; + private static final String MIME_TYPE_TAG = "mime-type"; + private static final String ERROR_CODE_TAG = "error-code"; + + private final String relativeFilename; + + /** + * Creates a reader for web.xml. + * + * @param appDir The directory in which the config file resides. + * @param relativeFilename The path to the config file, relative to + * {@code appDir}. + */ + public WebXmlReader(String appDir, String relativeFilename) { + super(appDir, true); + this.relativeFilename = relativeFilename; + } + + /** + * Creates a reader for web.xml. + * + * @param appDir The directory in which the web.xml config file resides. The + * path to the config file relative to the directory is assumed to be + * {@link #DEFAULT_RELATIVE_FILENAME}. + */ + public WebXmlReader(String appDir) { + this(appDir, DEFAULT_RELATIVE_FILENAME); + } + + @Override + protected String getRelativeFilename() { + return relativeFilename; + } + + /** + * Parses the config file. + * @return A {@link WebXml} object representing the parsed configuration. + */ + public WebXml readWebXml() { + return readConfigXml(); + } + + /** + * Instead of creating a default {@link XmlParser}, use the same + * logic as Jetty's {@code WebXmlConfiguration} to create one that + * is aware of web.xml files. Specifically, this method registers + * static versions of all known web.xml DTD's and schemas to avoid + * URL retrieval at parse time. + */ + @Override + protected XmlParser createXmlParser() { + + XmlParser xmlParser = new XmlParser(); + URL dtd22 = getClass().getResource("/javax/servlet/resources/web-app_2_2.dtd"); + URL dtd23 = getClass().getResource("/javax/servlet/resources/web-app_2_3.dtd"); + URL jsp20xsd = getClass().getResource("/javax/servlet/resources/jsp_2_0.xsd"); + URL jsp21xsd = getClass().getResource("/javax/servlet/resources/jsp_2_1.xsd"); + URL j2ee14xsd = getClass().getResource("/javax/servlet/resources/j2ee_1_4.xsd"); + URL webapp24xsd = getClass().getResource("/javax/servlet/resources/web-app_2_4.xsd"); + URL webapp25xsd = getClass().getResource("/javax/servlet/resources/web-app_2_5.xsd"); + URL schemadtd = getClass().getResource("/javax/servlet/resources/XMLSchema.dtd"); + URL xmlxsd = getClass().getResource("/javax/servlet/resources/xml.xsd"); + URL webservice11xsd = + getClass().getResource("/javax/servlet/resources/j2ee_web_services_client_1_1.xsd"); + URL webservice12xsd = + getClass().getResource("/javax/servlet/resources/javaee_web_services_client_1_2.xsd"); + URL datatypesdtd = getClass().getResource("/javax/servlet/resources/datatypes.dtd"); + xmlParser.redirectEntity("web-app_2_2.dtd",dtd22); + xmlParser.redirectEntity("-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN",dtd22); + xmlParser.redirectEntity("web.dtd",dtd23); + xmlParser.redirectEntity("web-app_2_3.dtd",dtd23); + xmlParser.redirectEntity("-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN",dtd23); + xmlParser.redirectEntity("XMLSchema.dtd",schemadtd); + xmlParser.redirectEntity("http://www.w3.org/2001/XMLSchema.dtd",schemadtd); + xmlParser.redirectEntity("-//W3C//DTD XMLSCHEMA 200102//EN",schemadtd); + xmlParser.redirectEntity("jsp_2_0.xsd",jsp20xsd); + xmlParser.redirectEntity("http://java.sun.com/xml/ns/j2ee/jsp_2_0.xsd",jsp20xsd); + xmlParser.redirectEntity("jsp_2_1.xsd",jsp21xsd); + xmlParser.redirectEntity("http://java.sun.com/xml/ns/javaee/jsp_2_1.xsd",jsp21xsd); + xmlParser.redirectEntity("j2ee_1_4.xsd",j2ee14xsd); + xmlParser.redirectEntity("http://java.sun.com/xml/ns/j2ee/j2ee_1_4.xsd",j2ee14xsd); + xmlParser.redirectEntity("web-app_2_4.xsd",webapp24xsd); + xmlParser.redirectEntity("http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd",webapp24xsd); + xmlParser.redirectEntity("web-app_2_5.xsd",webapp25xsd); + xmlParser.redirectEntity("http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd",webapp25xsd); + xmlParser.redirectEntity("xml.xsd",xmlxsd); + xmlParser.redirectEntity("http://www.w3.org/2001/xml.xsd",xmlxsd); + xmlParser.redirectEntity("datatypes.dtd",datatypesdtd); + xmlParser.redirectEntity("http://www.w3.org/2001/datatypes.dtd",datatypesdtd); + xmlParser.redirectEntity("j2ee_web_services_client_1_1.xsd",webservice11xsd); + xmlParser.redirectEntity("http://www.ibm.com/webservices/xsd/j2ee_web_services_client_1_1.xsd", + webservice11xsd); + xmlParser.redirectEntity("javaee_web_services_client_1_2.xsd",webservice12xsd); + xmlParser.redirectEntity("http://www.ibm.com/webservices/xsd/javaee_web_services_client_1_2.xsd", + webservice12xsd); + + return xmlParser; + } + + @Override + protected WebXml processXml(InputStream is) { + final WebXml webXml = new WebXml(); + parse(new ParserCallback() { + + private WebXml.SecurityConstraint security; + private String extension; + + @Override + public void newNode(Node node, Stack ancestors) { + String thisTag = node.getTag().toLowerCase(); + String parentTag = null; + if (ancestors.size() > 0) { + parentTag = ancestors.get(ancestors.size() - 1).getTag().toLowerCase(); + } + + if (URLPATTERN_TAG.equals(thisTag)) { + String pattern = getString(node); + if (SERVLETMAP_TAG.equals(parentTag) || FILTERMAP_TAG.equals(parentTag)) { + String id = node.getAttribute("id"); + webXml.addServletPattern(pattern, id); + } else if (security != null) { + security.addUrlPattern(pattern); + } + } else if (ROLENAME_TAG.equals(thisTag) && AUTHCONST_TAG.equals(parentTag)) { + if (security == null) { + throw new AppEngineConfigException(getFilename() + ": <" + ROLENAME_TAG + + "> in <" + AUTHCONST_TAG + "> in unrecognized context"); + } + security.setRequiredRole(parseRequiredRole(getString(node))); + } else if (TRANSGUARANTEE_TAG.equals(thisTag) && USERDATACONST_TAG.equals(parentTag)) { + if (security == null) { + throw new AppEngineConfigException(getFilename() + ": <" + TRANSGUARANTEE_TAG + + "> in <" + USERDATACONST_TAG + "> in unrecognized context"); + } + security.setTransportGuarantee(parseTransportGuarantee(getString(node))); + } else if (SECURITYCONST_TAG.equals(thisTag)) { + security = webXml.addSecurityConstraint(); + } else if (WELCOME_FILE_TAG.equals(thisTag)) { + if (!WELCOME_FILE_LIST_TAG.equals(parentTag)) { + throw new AppEngineConfigException(getFilename() + ": <" + WELCOME_FILE_TAG + + "> in unrecognized context"); + } + webXml.addWelcomeFile(getString(node)); + } else if (EXTENSION_TAG.equals(thisTag)) { + extension = getString(node); + } else if (MIME_TYPE_TAG.equals(thisTag)) { + if (extension == null) { + throw new AppEngineConfigException(getFilename() + ": <" + MIME_TYPE_TAG + + "> without ."); + } + String mimeType = getString(node); + webXml.addMimeMapping(extension, mimeType); + extension = null; + } else if (ERROR_CODE_TAG.equals(thisTag)) { + String code = getString(node); + if ("404".equals(code)) { + webXml.setFallThroughToRuntime(true); + } + } + } + }, is); + + if (webXml.getWelcomeFiles().isEmpty()) { + for (String welcomeFile : DEFAULT_WELCOME_FILES) { + webXml.addWelcomeFile(welcomeFile); + } + } + + return webXml; + } + + private SecurityConstraint.RequiredRole parseRequiredRole(String role) { + if ("*".equals(role)) { + return SecurityConstraint.RequiredRole.ANY_USER; + } else if ("admin".equals(role)) { + return SecurityConstraint.RequiredRole.ADMIN; + } else { + throw new AppEngineConfigException(getFilename() + ": " + + "Unknown role-name: must be '*' or 'admin'"); + } + } + + private SecurityConstraint.TransportGuarantee parseTransportGuarantee(String transportGuarantee) { + if ("NONE".equalsIgnoreCase(transportGuarantee)) { + return SecurityConstraint.TransportGuarantee.NONE; + } else if ("INTEGRAL".equalsIgnoreCase(transportGuarantee)) { + return SecurityConstraint.TransportGuarantee.INTEGRAL; + } else if ("CONFIDENTIAL".equalsIgnoreCase(transportGuarantee)) { + return SecurityConstraint.TransportGuarantee.CONFIDENTIAL; + } else { + throw new AppEngineConfigException(getFilename() + ": " + + "Unknown transport-guarantee: must be " + + "NONE, INTEGRAL, or CONFIDENTIAL."); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/XmlUtils.java b/java/src/main/com/google/apphosting/utils/config/XmlUtils.java new file mode 100644 index 00000000..8dee8910 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/XmlUtils.java @@ -0,0 +1,54 @@ +package com.google.apphosting.utils.config; + +import org.mortbay.xml.XmlParser; +import org.mortbay.xml.XmlParser.Node; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility functions for processing XML. + */ +public class XmlUtils { + private static final Logger logger = Logger.getLogger(XmlUtils.class.getName()); + + static String getText(XmlParser.Node node) throws AppEngineConfigException{ + Object child = node.get(0); + String value; + if (child == null) { + value = ""; + } else { + if (!(child instanceof String)) { + String msg = "Invalid XML: String content expected in node '" + node.getTag() + "'."; + logger.log(Level.SEVERE, msg); + throw new AppEngineConfigException(msg); + } + value = (String) child; + } + + return value.trim(); + } + + /** + * Parses the input stream and returns the {@link Node} for the root element. + * + * @throws AppEngineConfigException If the input stream cannot be parsed. + */ + static Node parse(InputStream is) { + XmlParser xmlParser = new XmlParser(); + try { + return xmlParser.parse(is); + } catch (IOException e) { + String msg = "Received IOException parsing the input stream."; + logger.log(Level.SEVERE, msg, e); + throw new AppEngineConfigException(msg, e); + } catch (SAXException e) { + String msg = "Received SAXException parsing the input stream."; + logger.log(Level.SEVERE, msg, e); + throw new AppEngineConfigException(msg, e); + } + } +} diff --git a/java/src/main/com/google/apphosting/utils/config/YamlUtils.java b/java/src/main/com/google/apphosting/utils/config/YamlUtils.java new file mode 100644 index 00000000..35c69905 --- /dev/null +++ b/java/src/main/com/google/apphosting/utils/config/YamlUtils.java @@ -0,0 +1,106 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.apphosting.utils.config; + +import net.sourceforge.yamlbeans.YamlException; +import net.sourceforge.yamlbeans.YamlReader; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +/** + * Helper methods for parsing YAML files. + * + */ +public class YamlUtils { + + static final Pattern TRUE_PATTERN = + Pattern.compile("y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON"); + static final Pattern FALSE_PATTERN = + Pattern.compile("n|N|no|No|NO|false|False|FALSE|off|Off|OFF"); + + private static final String RESERVED_URL = + "The URL '%s' is reserved and cannot be used."; + + private YamlUtils() { } + + /** + * Parse a YAML !!bool type. YamlBeans only supports "true" or "false". + * + * @throws AppEngineConfigException + */ + static boolean parseBoolean(String value) { + if (TRUE_PATTERN.matcher(value).matches()) { + return true; + } else if (FALSE_PATTERN.matcher(value).matches()) { + return false; + } + throw new AppEngineConfigException("Invalid boolean value '" + value + "'."); + } + + /** + * Check that a URL is not one of the reserved URLs according to + * http://code.google.com/appengine/docs/java/configyaml/appconfig_yaml.html#Reserved_URLs + * + * @throws AppEngineConfigException + */ + static void validateUrl(String url) { + if (url.equals("/form")) { + throw new AppEngineConfigException(String.format(RESERVED_URL, url)); + } + } + + /** + * Parse the data into YAML. + * + * @param data The text to parse as YAML. + * @return The YAML data as a Map. + * @throws AppEngineConfigException Throws when the underlying Yaml library + * detects bad data of when the data does not parse to a Map. + */ + @SuppressWarnings("unchecked") + public static Map genericParse(String data) + throws AppEngineConfigException { + try { + YamlReader reader = new YamlReader(data); + return reader.read(Map.class); + } catch (YamlException exc) { + throw new AppEngineConfigException("Invalid YAML data: " + data, exc); + } + } + + /** + * A utility class that encapsulates conversion between different + * types of objects. + */ + public static interface ObjectConverter { + T convert(Object obj) throws Exception; + } + + /** + * Parses the data into YAML and converts the map values. + * + * @param data The text to parse as YAML. + * @param converter A converter for updating the values of the Map from + * the type returned from the underlying Yaml library to the expected type. + * @return The YAML data as a Map. + * @throws AppEngineConfigException Throws when the underlying Yaml library + * detects bad data or when the data does not parse to a Map. Also all + * Exceptions from ObjectConverted are encapsulated in an + * AppEngineConfigException. + */ + public static Map genericParse(String data, + ObjectConverter converter) throws AppEngineConfigException { + try { + Map yaml = new HashMap(); + for (Entry entry : genericParse(data).entrySet()) { + yaml.put(entry.getKey(), converter.convert(entry.getValue())); + } + return yaml; + } catch (Exception exc) { + throw new AppEngineConfigException(exc); + } + } +} -- 2.11.4.GIT