> rpcResponseTransformer =
new GetAllRpcResponseTransformer<>(byteStringToKey, responseTransformer);
return makeAsyncCall(
diff --git a/api/src/main/java/com/google/appengine/api/memcache/MemcacheService.java b/api/src/main/java/com/google/appengine/api/memcache/MemcacheService.java
index d60938f0a..e9ce9fff9 100644
--- a/api/src/main/java/com/google/appengine/api/memcache/MemcacheService.java
+++ b/api/src/main/java/com/google/appengine/api/memcache/MemcacheService.java
@@ -104,6 +104,40 @@ interface IdentifiableValue {
Object getValue();
}
+ /**
+ * ...
+ */
+ interface ItemForPeek {
+ /**
+ * @return the encapsulated value object.
+ */
+ Object getValue();
+ /**
+ *
+ * @return the expiration timestamp of the item in unix epoch seconds, or null if
+ * this item has no expiration timestamp.
+ *
+ */
+ Long getExpirationTimeSec();
+
+ /**
+ *
+ * @return the last accessed timestamp of the item in unix epoch seconds, or null if this item has no
+ * last access timestamp.
+ *
+ *
+ */
+ Long getLastAccessTimeSec();
+
+ /**
+ *
+ * @return the delete_time timestamp in unix epoch seconds resulting from setting the millisNoReAdd
+ * parameter of the item, or null if this item is not delete locked.
+ *
+ */
+ Long getDeleteLockTimeSec();
+ }
+
/**
* A holder for compare and set values.
* {@link Expiration expiration} and {@code newValue} can be null.
@@ -201,6 +235,26 @@ public int hashCode() {
*/
IdentifiableValue getIdentifiable(Object key);
+ /**
+ * Similar to {@link #get}, but returns an object that can provide extra timestamp
+ * metadata.
+ *
+ * If an error deserializing the value occurs, this passes
+ * an {@link InvalidValueException} to the service's {@link ErrorHandler}.
+ * If a service error occurs, this passes a {@link MemcacheServiceException}.
+ * See {@link BaseMemcacheService#setErrorHandler(ErrorHandler)}.
+ *
+ * @param key the key object used to store the cache entry
+ * @return an {@link ItemForPeek} object that wraps the
+ * value object stored as well as extra meta data timestamps.
+ * {@code null} is returned if {@code key} is not present in the cache.
+ * @throws IllegalArgumentException if {@code key} is not
+ * {@link Serializable} and is not {@code null}
+ */
+ default ItemForPeek getItemForPeek(Object key) {
+ throw new UnsupportedOperationException();
+ };
+
/**
* Performs a getIdentifiable for multiple keys at once.
* This is more efficient than multiple separate calls to
@@ -219,6 +273,26 @@ public int hashCode() {
*/
Map getIdentifiables(Collection keys);
+ /**
+ * Performs a getItemForPeek for multiple keys at once.
+ * This is more efficient than multiple separate calls to
+ * {@link #getItemForPeek(Object)}.
+ *
+ * If an error deserializing the value occurs, this passes
+ * an {@link InvalidValueException} to the service's {@link ErrorHandler}.
+ * If a service error occurs, this passes a {@link MemcacheServiceException}.
+ * See {@link BaseMemcacheService#setErrorHandler(ErrorHandler)}.
+ *
+ * @param keys a collection of keys for which values should be retrieved
+ * @return a mapping from keys to {@link ItemForPeek} of any entries found. If a requested
+ * key is not found in the cache, the key will not be in the returned Map.
+ * @throws IllegalArgumentException if any element of {@code keys} is not
+ * {@link Serializable} and is not {@code null}
+ */
+ default Map getItemsForPeek(Collection keys) {
+ throw new UnsupportedOperationException();
+ };
+
/**
* Tests whether a given value is in cache, even if its value is {@code null}.
*
diff --git a/api/src/main/java/com/google/appengine/api/memcache/MemcacheServiceImpl.java b/api/src/main/java/com/google/appengine/api/memcache/MemcacheServiceImpl.java
index 15120f81f..fe7ee3bb3 100644
--- a/api/src/main/java/com/google/appengine/api/memcache/MemcacheServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/memcache/MemcacheServiceImpl.java
@@ -77,6 +77,16 @@ public Map getIdentifiables(Collection keys) {
public Map getAll(Collection keys) {
return quietGet(async.getAll(keys));
}
+
+ @Override
+ public ItemForPeek getItemForPeek(Object key) {
+ return quietGet(async.getItemForPeek(key));
+ }
+
+ @Override
+ public Map getItemsForPeek(Collection keys) {
+ return quietGet(async.getItemsForPeek(keys));
+ }
@Override
public boolean put(Object key, Object value, Expiration expires, SetPolicy policy) {
diff --git a/api_dev/pom.xml b/api_dev/pom.xml
index 3a9a2aa91..f26e31800 100644
--- a/api_dev/pom.xml
+++ b/api_dev/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
diff --git a/api_dev/src/test/java/com/google/appengine/api/memcache/MemcacheServiceImplTest.java b/api_dev/src/test/java/com/google/appengine/api/memcache/MemcacheServiceImplTest.java
index d851aadb4..c7eb64160 100644
--- a/api_dev/src/test/java/com/google/appengine/api/memcache/MemcacheServiceImplTest.java
+++ b/api_dev/src/test/java/com/google/appengine/api/memcache/MemcacheServiceImplTest.java
@@ -34,10 +34,12 @@
import com.google.appengine.api.NamespaceManager;
import com.google.appengine.api.memcache.AsyncMemcacheServiceImpl.IdentifiableValueImpl;
+import com.google.appengine.api.memcache.AsyncMemcacheServiceImpl.ItemForPeekImpl;
import com.google.appengine.api.memcache.MemcacheSerialization.Flag;
import com.google.appengine.api.memcache.MemcacheSerialization.ValueAndFlags;
import com.google.appengine.api.memcache.MemcacheService.CasValues;
import com.google.appengine.api.memcache.MemcacheService.IdentifiableValue;
+import com.google.appengine.api.memcache.MemcacheService.ItemForPeek;
import com.google.appengine.api.memcache.MemcacheService.SetPolicy;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheBatchIncrementRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheBatchIncrementResponse;
@@ -1976,6 +1978,140 @@ public void testMultiGetIdentifiable() {
multiGetIdentifiableTest(memcache, "ns2");
}
+ @Test
+ public void testGetForPeek() {
+ MemcacheServiceImpl memcache = new MemcacheServiceImpl(null);
+ String namespace = "";
+ String[] keys = {ONE, TWO, null, null};
+ String[] values = {ONE, null, TWO, null};
+ for (int i = 0; i < keys.length; i++) {
+ String key = keys[i];
+ String value = values[i];
+ byte[] pbKey = makePbKey(key);
+ MemcacheGetRequest request =
+ MemcacheGetRequest.newBuilder()
+ .setNameSpace(namespace)
+ .setForPeek(true)
+ .addKey(ByteString.copyFrom(pbKey))
+ .build();
+ MemcacheGetResponse response =
+ MemcacheGetResponse.newBuilder()
+ .addItem(
+ MemcacheGetResponse.Item.newBuilder()
+ .setFlags(value == null ? Flag.OBJECT.ordinal() : Flag.UTF8.ordinal())
+ .setKey(ByteString.copyFrom(pbKey))
+ .setValue(ByteString.copyFrom(serialize(value).value))
+ .setTimestamps(
+ MemcacheServicePb.ItemTimestamps.newBuilder()
+ .setExpirationTimeSec(1)
+ .setLastAccessTimeSec(2)
+ .setDeleteLockTimeSec(3)
+ .build()))
+ .build();
+ expectAsyncCall("Get", request, response);
+ ItemForPeek v = memcache.getItemForPeek(key);
+ assertThat(v.getExpirationTimeSec()).isEqualTo(Long.valueOf(1));
+ assertThat(v.getLastAccessTimeSec()).isEqualTo(Long.valueOf(2));
+ assertThat(v.getDeleteLockTimeSec()).isEqualTo(Long.valueOf(3));
+ assertThat(v.getValue()).isEqualTo(value);
+ verifyAsyncCall("Get", request);
+ }
+ }
+
+ private void multiItemsForPeekTest(MemcacheService memcache, String namespace) {
+ MemcacheGetRequest request =
+ MemcacheGetRequest.newBuilder()
+ .setNameSpace(namespace)
+ .setForPeek(true)
+ .addKey(ByteString.copyFrom(makePbKey(ONE)))
+ .addKey(ByteString.copyFrom(makePbKey(null)))
+ .addKey(ByteString.copyFrom(makePbKey("Missing")))
+ .build();
+ MemcacheGetResponse response =
+ MemcacheGetResponse.newBuilder()
+ .addItem(
+ MemcacheGetResponse.Item.newBuilder()
+ .setKey(ByteString.copyFrom(makePbKey(ONE)))
+ .setTimestamps(
+ MemcacheServicePb.ItemTimestamps.newBuilder()
+ .setExpirationTimeSec(1)
+ .setLastAccessTimeSec(2)
+ .setDeleteLockTimeSec(3)
+ .build())
+ .setFlags(Flag.OBJECT.ordinal())
+ .setValue(ByteString.copyFrom(serialize(null).value)))
+ .addItem(
+ MemcacheGetResponse.Item.newBuilder()
+ .setKey(ByteString.copyFrom(makePbKey(null)))
+ .setTimestamps(
+ MemcacheServicePb.ItemTimestamps.newBuilder()
+ .setExpirationTimeSec(10)
+ .setLastAccessTimeSec(20)
+ .setDeleteLockTimeSec(30)
+ .build())
+ .setFlags(Flag.INTEGER.ordinal())
+ .setValue(ByteString.copyFrom(serialize(456).value)))
+ .build();
+ expectAsyncCall("Get", request, response);
+ ArrayList collection = new ArrayList<>();
+ collection.add(ONE);
+ collection.add(null);
+ collection.add("Missing");
+ Map result = memcache.getItemsForPeek(collection);
+ assertThat(result).hasSize(2);
+ assertThat(result.get(ONE).getValue()).isEqualTo(null);
+ assertThat(((ItemForPeekImpl) result.get(ONE)).getExpirationTimeSec()).isEqualTo(1);
+ assertThat(((ItemForPeekImpl) result.get(ONE)).getLastAccessTimeSec()).isEqualTo(2);
+ assertThat(((ItemForPeekImpl) result.get(ONE)).getDeleteLockTimeSec()).isEqualTo(3);
+ assertThat(result.get(null).getValue()).isEqualTo(456);
+ assertThat(((ItemForPeekImpl) result.get(null)).getExpirationTimeSec()).isEqualTo(10);
+ assertThat(((ItemForPeekImpl) result.get(null)).getLastAccessTimeSec()).isEqualTo(20);
+ assertThat(((ItemForPeekImpl) result.get(null)).getDeleteLockTimeSec()).isEqualTo(30);
+ verifyAsyncCall("Get", request);
+ }
+
+ @Test
+ public void testMultiItemForPeek() {
+ MemcacheService memcache = new MemcacheServiceImpl(null);
+ NamespaceManager.set("");
+ multiItemsForPeekTest(memcache, "");
+ NamespaceManager.set("ns");
+ multiItemsForPeekTest(memcache, "ns");
+ memcache = new MemcacheServiceImpl("ns2");
+ multiItemsForPeekTest(memcache, "ns2");
+ }
+
+ @Test
+ public void testGetWithPeekAfterDelete() {
+ MemcacheService memcache = new MemcacheServiceImpl(null);
+ NamespaceManager.set("");
+ byte[] pbKey = makePbKey(ONE);
+ MemcacheGetRequest request =
+ MemcacheGetRequest.newBuilder()
+ .setNameSpace("")
+ .setForPeek(true)
+ .addKey(ByteString.copyFrom(pbKey))
+ .build();
+ MemcacheGetResponse response =
+ MemcacheGetResponse.newBuilder()
+ .addItem(
+ MemcacheGetResponse.Item.newBuilder()
+ .setFlags(Flag.BOOLEAN.ordinal())
+ .setKey(ByteString.copyFrom(pbKey))
+ .setValue(ByteString.copyFromUtf8(""))
+ .setIsDeleteLocked(true)
+ .setTimestamps(
+ MemcacheServicePb.ItemTimestamps.newBuilder()
+ .setDeleteLockTimeSec(3)
+ .build()))
+ .build();
+ expectAsyncCall("Get", request, response);
+ ItemForPeek v = memcache.getItemForPeek(ONE);
+ assertThat(v.getDeleteLockTimeSec()).isEqualTo(Long.valueOf(3));
+ assertThat(v.getValue()).isEqualTo(null);
+ verifyAsyncCall("Get", request);
+ }
+
@Test
public void testPutIfUntouched() {
MemcacheServiceImpl memcache = new MemcacheServiceImpl(null);
diff --git a/api_legacy/pom.xml b/api_legacy/pom.xml
index f914597a6..02245185c 100644
--- a/api_legacy/pom.xml
+++ b/api_legacy/pom.xml
@@ -22,7 +22,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
diff --git a/appengine-api-1.0-sdk/pom.xml b/appengine-api-1.0-sdk/pom.xml
index 0ca39bea9..04e3e24a5 100644
--- a/appengine-api-1.0-sdk/pom.xml
+++ b/appengine-api-1.0-sdk/pom.xml
@@ -20,7 +20,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
AppEngine :: appengine-api-1.0-sdk
diff --git a/appengine-api-stubs/pom.xml b/appengine-api-stubs/pom.xml
index e98d36b91..2247931ee 100644
--- a/appengine-api-stubs/pom.xml
+++ b/appengine-api-stubs/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
@@ -285,7 +285,7 @@
- com.esotericsoftware.yamlbeans:yamlbeans
+ com.contrastsecurity:yamlbeans
com/esotericsoftware/yamlbeans/**
@@ -362,7 +362,7 @@
- com.esotericsoftware.yamlbeans:yamlbeans
+ com.contrastsecurity:yamlbeans
com.google.appengine:appengine-apis-dev
com.google.appengine:mediautil
com.google.appengine:shared-sdk
diff --git a/appengine_jsr107/pom.xml b/appengine_jsr107/pom.xml
index 3a0adb302..92cd06bd5 100644
--- a/appengine_jsr107/pom.xml
+++ b/appengine_jsr107/pom.xml
@@ -24,7 +24,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
diff --git a/appengine_resources/pom.xml b/appengine_resources/pom.xml
index 2bbab1f58..c0e5a6d7e 100644
--- a/appengine_resources/pom.xml
+++ b/appengine_resources/pom.xml
@@ -21,7 +21,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
AppEngine :: appengine-resources
diff --git a/appengine_setup/test/pom.xml b/appengine_setup/test/pom.xml
index 4bd646628..6434eb69f 100644
--- a/appengine_setup/test/pom.xml
+++ b/appengine_setup/test/pom.xml
@@ -37,7 +37,7 @@
com.google.guava
guava
- 23.0
+ 32.1.2-jre
org.projectlombok
diff --git a/appengine_testing/pom.xml b/appengine_testing/pom.xml
index e5288bdfb..cf40ea245 100644
--- a/appengine_testing/pom.xml
+++ b/appengine_testing/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
diff --git a/appengine_testing_tests/pom.xml b/appengine_testing_tests/pom.xml
index 850ca85e2..14a830bce 100644
--- a/appengine_testing_tests/pom.xml
+++ b/appengine_testing_tests/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
diff --git a/applications/pom.xml b/applications/pom.xml
index 35affda0c..e2a89e2bd 100644
--- a/applications/pom.xml
+++ b/applications/pom.xml
@@ -22,7 +22,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
pom
diff --git a/applications/proberapp/pom.xml b/applications/proberapp/pom.xml
index b737ffd5c..54a928a0f 100644
--- a/applications/proberapp/pom.xml
+++ b/applications/proberapp/pom.xml
@@ -14,9 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
4.0.0
war
@@ -27,7 +25,7 @@
com.google.appengine
applications
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
diff --git a/applications/springboot/pom.xml b/applications/springboot/pom.xml
index d35ad917d..0f6cbf554 100644
--- a/applications/springboot/pom.xml
+++ b/applications/springboot/pom.xml
@@ -15,13 +15,12 @@
limitations under the License.
-->
-
+
4.0.0
com.google.appengine.demos
springboot
- 0.0.1-SNAPSHOT
+ 0.0.2-SNAPSHOT
war
AppEngine :: springboot
diff --git a/e2etests/pom.xml b/e2etests/pom.xml
new file mode 100644
index 000000000..de83404b5
--- /dev/null
+++ b/e2etests/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine
+ e2etests
+
+ com.google.appengine
+ parent
+ 2.0.20-SNAPSHOT
+
+ AppEngine :: e2e tests
+ pom
+
+
+ testlocalapps
+ stagingtests
+
+
diff --git a/e2etests/stagingtests/pom.xml b/e2etests/stagingtests/pom.xml
new file mode 100644
index 000000000..dc46b382a
--- /dev/null
+++ b/e2etests/stagingtests/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+
+ 4.0.0
+
+ stagingtests
+
+ com.google.appengine
+ e2etests
+ 2.0.20-SNAPSHOT
+
+
+ jar
+ AppEngine :: e2e staging tests
+
+
+ true
+
+
+
+
+
+ com.google.appengine
+ runtime-testapps
+ jar-with-dependencies
+ test
+ ${project.version}
+
+
+
+ com.google.guava
+ guava
+
+
+
+ com.google.truth
+ truth
+ test
+
+
+ junit
+ junit
+ test
+
+
+
+ com.google.appengine
+ appengine-utils
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ test
+
+
+
diff --git a/e2etests/stagingtests/src/test/java/com/google/appengine/tools/admin/ApplicationTest.java b/e2etests/stagingtests/src/test/java/com/google/appengine/tools/admin/ApplicationTest.java
new file mode 100644
index 000000000..85cf18917
--- /dev/null
+++ b/e2etests/stagingtests/src/test/java/com/google/appengine/tools/admin/ApplicationTest.java
@@ -0,0 +1,1797 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.appengine.tools.admin;
+
+
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Maps;
+import com.google.common.io.Files;
+import com.google.common.io.MoreFiles;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.apphosting.utils.config.AppEngineConfigException;
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.apphosting.utils.config.CronXml;
+import com.google.apphosting.utils.config.GenerationDirectory;
+import com.google.apphosting.utils.config.RetryParametersXml;
+import com.google.apphosting.utils.config.StagingOptions;
+
+import java.io.BufferedWriter;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * Tests for the Application functionality
+ *
+ * @author fabbott@google.com (Freeland Abbott)
+ */
+@RunWith(JUnit4.class)
+public class ApplicationTest {
+ private static final String SDK_ROOT_PROPERTY = "appengine.sdk.root";
+
+ private static final String TEST_FILES = getWarPath("sampleapp");
+ private static final String TEST_FILES_RUNTIME_DEFINED = getWarPath("sampleapp-runtime");
+ private static final String TEST_FILES_AUTOMATIC_MODULE =
+ getWarPath("sampleapp-automatic-module");
+ private static final String TEST_FILES_MANUAL_MODULE = getWarPath("sampleapp-manual-module");
+ private static final String TEST_FILES_BASIC_MODULE = getWarPath("sampleapp-basic-module");
+ private static final String TEST_FILES_BACKENDS = getWarPath("sampleapp-backends");
+ private static final String TEST_JSPX_FILES = getWarPath("sample-jspx");
+ private static final String TEST_TAGS_FILES = getWarPath("sample-jsptaglibrary");
+ private static final String TEST_ERROR_IN_TAGS_FILES = getWarPath("sample-error-in-tag-file");
+ private static final String NOJSP_TEST_FILES = getWarPath("sample-nojsps");
+ private static final String BAD_WEBXML_TEST_FILES = getWarPath("sample-badweb");
+ private static final String BAD_RUNTIME_CHANNEL = getWarPath("sample-badruntimechannel");
+ private static final String TEST_JAVA11 = getWarPath("sample-java11");
+ private static final String TEST_JAVA17 = getWarPath("sample-java17");
+ private static final String BAD_ENTRYPOINT = getWarPath("sample-badentrypoint");
+ private static final String BAD_APPENGINEWEBXML_TEST_FILES = getWarPath("sample-badaeweb");
+ private static final String BAD_INDEXESXML_TEST_FILES = getWarPath("sample-badindexes");
+ private static final String MISSING_APP_ID_TEST_FILES = getWarPath("sample-missingappid");
+ private static final String INCLUDE_HTTP_HEADERS_TEST_FILES = getWarPath("http-headers");
+ private static final String XMLORDERING_TEST_FILES = getWarPath("xmlorder");
+ private static final String BAD_CRONXML_TEST_FILES = getWarPath("badcron");
+ private static final String JAVA8_NO_WEBXML = getWarPath("java8-no-webxml");
+ private static final String CRON_RETRY_PARAMETERS_TEST_FILES =
+ getWarPath("cron-good-retry-parameters");
+ private static final String CRON_NEGATIVE_RETRY_LIMIT_TEST_FILES =
+ getWarPath("cron-negative-retry-limit");
+ private static final String CRON_NEGATIVE_MAX_BACKOFF_TEST_FILES =
+ getWarPath("cron-negative-max-backoff");
+ private static final String CRON_TWO_MAX_DOUBLINGS_TEST_FILES =
+ getWarPath("cron-two-max-doublings");
+ private static final String CRON_BAD_AGE_LIMIT_TEST_FILES = getWarPath("cron-bad-job-age-limit");
+
+ private static final String LEGACY_AUTO_ID_POLICY_TEST_FILES =
+ getWarPath("sample-legacy-auto-ids");
+ private static final String UNSPECIFIED_AUTO_ID_POLICY_TEST_FILES =
+ getWarPath("sample-unspecified-auto-ids");
+ private static final String DEFAULT_AUTO_ID_POLICY_TEST_FILES =
+ getWarPath("sample-default-auto-ids");
+ private static final String JAVA8_JAR_TEST_FILES = getWarPath("java8-jar");
+ private static final String CLASSES_TEST_FILES = getWarPath("sample-with-classes");
+
+ private static final String SDK_ROOT =getSDKRoot();
+
+ private static final String SERVLET3_STANDARD_APP_ROOT =getWarPath("bundle_standard");
+ private static final String SERVLET3_STANDARD_APP_NO_JSP_ROOT =getWarPath("bundle_standard_with_no_jsp");
+ private static final String SERVLET3_STANDARD_WEBLISTENER_MEMCACHE =getWarPath("bundle_standard_with_weblistener_memcache");
+ private static final String SERVLET3_STANDARD_APP_WITH_CONTAINER_INIT =getWarPath("bundle_standard_with_container_initializer");
+ private static final String SERVLET3_APP_ID = "servlet3-demo";
+
+ private static final String STAGE_TEST_APP = getWarPath("stage-sampleapp");
+ private static final String STAGE_WITH_APPID_AND_VERSION_TEST_APP =
+ getWarPath("stage-with-appid-and-version");
+ private static final String STAGE_WITH_STAGING_OPTIONS = getWarPath("stage-with-staging-options");
+
+ private static final int RANDOM_HTML_SIZE = 704;
+ private static final String APPID = "sampleapp";
+ private static final String MODULE_ID = "stan";
+ private static final String APPVER = "1";
+ private String oldSdkRoot;
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private static final String APPENGINE_WEB_XML_CONTENT =
+ ""
+ + "sampleapp1"
+ + "truetrue";
+
+ private static String getWarPath(String directoryName) {
+ File currentDirectory = new File("").getAbsoluteFile();
+
+ String appRoot = new File(currentDirectory, "../testlocalapps/"+directoryName+"/target/"+directoryName+"-0.0.1-SNAPSHOT").getAbsolutePath();
+ System.out.println(appRoot);
+
+// assertThat(appRoot.isDirectory()).isTrue();
+return appRoot;
+
+
+ }
+ private static String getSDKRoot() {
+ File currentDirectory = new File("").getAbsoluteFile();
+ String sdkRoot= null;
+ try {
+ sdkRoot = new File(currentDirectory, "../../sdk_assembly/target/appengine-java-sdk").getCanonicalPath();
+ } catch (IOException ex) {
+ Logger.getLogger(ApplicationTest.class.getName()).log(Level.SEVERE, null, ex);
+ }
+return sdkRoot;
+
+
+ }
+ /** Set the appengine.sdk.root system property to make SdkInfo happy. */
+ @Before
+ public void setUp() {
+ oldSdkRoot = System.setProperty(SDK_ROOT_PROPERTY, SDK_ROOT);
+ }
+
+ @After
+ public void tearDown() {
+ if (oldSdkRoot != null) {
+ System.setProperty(SDK_ROOT_PROPERTY, oldSdkRoot);
+ } else {
+ System.clearProperty(SDK_ROOT_PROPERTY);
+ }
+ }
+
+ private static void checkIfEntryIsInJarFile(File jarFile, String entryName) {
+ assertThat(jarFile.exists()).isTrue();
+ try {
+ JarFile jar = new JarFile(jarFile);
+ try {
+ Enumeration jarEntries = jar.entries();
+ while (jarEntries.hasMoreElements()) {
+ JarEntry jarEntry = jarEntries.nextElement();
+ if (jarEntry.getName().equals(entryName)) {
+ return;
+ }
+ }
+ } finally {
+ jar.close();
+ }
+ } catch (Exception e) {
+ throw new AssertionError("Cannot read jar file: " + jarFile.getAbsolutePath(), e);
+ }
+ fail("Jar does not contain entry: " + entryName);
+ }
+
+ @Test
+ public void testModuleOverride() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES_AUTOMATIC_MODULE);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getModule()).isEqualTo(MODULE_ID);
+ assertThat(testApp.getVersion()).isEqualTo(APPVER);
+
+ testApp = Application.readApplication(TEST_FILES_AUTOMATIC_MODULE, null, "newModule", null);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getModule()).isEqualTo("newModule");
+ assertThat(testApp.getVersion()).isEqualTo(APPVER);
+
+ testApp =
+ Application.readApplication(
+ TEST_FILES_AUTOMATIC_MODULE, "newId", "newModule", "newVersion");
+ assertThat(testApp.getAppId()).isEqualTo("newId");
+ assertThat(testApp.getModule()).isEqualTo("newModule");
+ assertThat(testApp.getVersion()).isEqualTo("newVersion");
+ }
+
+ @Test
+ public void testAppIdAppVerOverride() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getModule()).isNull();
+ assertThat(testApp.getVersion()).isEqualTo(APPVER);
+
+ testApp = Application.readApplication(TEST_FILES, null, null, null);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getModule()).isNull();
+ assertThat(testApp.getVersion()).isEqualTo(APPVER);
+
+ testApp = Application.readApplication(TEST_FILES, null, null, "newVersion");
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getModule()).isNull();
+ assertThat(testApp.getVersion()).isEqualTo("newVersion");
+
+ testApp = Application.readApplication(TEST_FILES, "newId", null, null);
+ assertThat(testApp.getAppId()).isEqualTo("newId");
+ assertThat(testApp.getModule()).isNull();
+ assertThat(testApp.getVersion()).isEqualTo(APPVER);
+
+ testApp = Application.readApplication(TEST_FILES, null, "newModule", null);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getModule()).isEqualTo("newModule");
+ assertThat(testApp.getVersion()).isEqualTo(APPVER);
+
+ testApp = Application.readApplication(TEST_FILES, "newId", "newModule", "newVersion");
+ assertThat(testApp.getAppId()).isEqualTo("newId");
+ assertThat(testApp.getModule()).isEqualTo("newModule");
+ assertThat(testApp.getVersion()).isEqualTo("newVersion");
+ }
+
+ @Test
+ public void testMissingAppIdNoOverride() throws Exception {
+ Application testApp =
+ Application.readApplication(MISSING_APP_ID_TEST_FILES, null, null, "ignoredVersion");
+ assertThrows(AppEngineConfigException.class, () -> testApp.validate());
+ }
+
+ @Test
+ public void testMissingAppIdAndVersionWithOverride() throws Exception {
+ Application testApp =
+ Application.readApplication(MISSING_APP_ID_TEST_FILES, "newId", null, "newVersion");
+ assertThat(testApp.getAppId()).isEqualTo("newId");
+ assertThat(testApp.getVersion()).isEqualTo("newVersion");
+ }
+
+ @Test
+ public void testLegacyAutoIdPolicy() throws IOException {
+ Application testApp = Application.readApplication(LEGACY_AUTO_ID_POLICY_TEST_FILES);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getAppEngineWebXml().getAutoIdPolicy()).isEqualTo("legacy");
+ }
+
+ @Test
+ public void testDefaultAutoIdPolicy() throws IOException {
+ Application testApp = Application.readApplication(DEFAULT_AUTO_ID_POLICY_TEST_FILES);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getAppEngineWebXml().getAutoIdPolicy()).isEqualTo("default");
+ }
+
+ @Test
+ public void testUnspecifiedAutoIdPolicy() throws IOException {
+ Application testApp = Application.readApplication(UNSPECIFIED_AUTO_ID_POLICY_TEST_FILES);
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ assertThat(testApp.getAppEngineWebXml().getAutoIdPolicy()).isNull();
+ }
+
+ @Test
+ public void testReadApplicationForStaging() throws IOException {
+ Application testApp = Application.readApplication(STAGE_TEST_APP, null, null, null);
+ assertThat(testApp.getAppId()).isNull();
+ assertThat(testApp.getVersion()).isNull();
+
+ // should not throw exception
+ testApp.validateForStaging();
+ }
+
+ @Test
+ public void testReadApplicationForStagingWithAppIdAndVersionFromCommandLine() throws IOException {
+ Application testApp =
+ Application.readApplication(STAGE_TEST_APP, "a-override", null, "v-override");
+
+ // should not throw exception
+ testApp.validateForStaging();
+ }
+
+ //TODO(ludo) @Test
+ public void testReadApplicationForStagingWithAppIdAndVersionFromFile() throws IOException {
+ Application testApp =
+ Application.readApplication(STAGE_WITH_APPID_AND_VERSION_TEST_APP, null, null, null);
+
+ // should not throw exception
+ testApp.validateForStaging();
+ }
+
+ @Test
+ public void testStagingWithFiles() throws Exception {
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(StagingOptions.builder().setSplitJarFiles(Optional.of(true)).build());
+
+ testApp.createStagingDirectory(opts);
+ testStagedFiles(testApp);
+ File stage = testApp.getStagingDir();
+ // Does not work when you are on github automation
+ // assertThat(new File(stage, "WEB-INF/classes/source-context.json").canRead()).isFalse();
+ File appYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "application: 'sampleapp'");
+ assertFileContains(appYaml, "\nversion: '1'");
+ }
+
+ @Test
+ public void testStagingWithRelativeFiles() throws Exception {
+ // If this assumption is false, presumably we're on Windows somehow, and our fabricated
+ // relative path probably won't work because of those stupid C: drive letter things.
+ assumeTrue(File.separatorChar == '/');
+ String currentDirAbsolute = Paths.get("").toAbsolutePath().toString();
+ assertThat(currentDirAbsolute).startsWith("/");
+ String currentDirToRoot = currentDirAbsolute.substring(1).replaceAll("([^/]+)", "..");
+ assertThat(TEST_FILES).startsWith("/");
+ String testFilesRelative = currentDirToRoot + TEST_FILES;
+ assertThat(new File(testFilesRelative).isDirectory()).isTrue();
+ Application testApp = Application.readApplication(testFilesRelative);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(StagingOptions.builder().setSplitJarFiles(Optional.of(true)).build());
+
+ testApp.createStagingDirectory(opts);
+ testStagedFiles(testApp);
+ File stage = testApp.getStagingDir();
+ // Does not work when you are on github automation
+ // assertThat(new File(stage, "WEB-INF/classes/source-context.json").canRead()).isFalse();
+ File appYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "application: 'sampleapp'");
+ assertFileContains(appYaml, "\nversion: '1'");
+ }
+
+ @Test
+ public void testZipClassesDirectoryWithoutClasses() throws Exception {
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(
+ StagingOptions.builder()
+ .setSplitJarFiles(Optional.of(true))
+ .setJarClasses(Optional.of(true))
+ .build());
+
+ testApp.createStagingDirectory(opts);
+ testStagedFiles(testApp);
+ File stage = testApp.getStagingDir();
+ File appYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "application: 'sampleapp'");
+ }
+
+ /**
+ * The sane staging defaults split jars and jars classes, hence re-using logic from the above test
+ * (testZipClassesDirectoryWithoutClasses).
+ */
+ @Test
+ public void testSaneStagingDefaults() throws Exception {
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setDefaultStagingOptions(StagingOptions.SANE_DEFAULTS);
+
+ testApp.createStagingDirectory(opts);
+ testStagedFiles(testApp);
+ File stage = testApp.getStagingDir();
+ File appYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "application: 'sampleapp'");
+ }
+
+ @Test
+ public void testStagingWithSourceContext() throws Exception {
+ RepoInfo.SourceContext testSourceContext =
+ RepoInfo.SourceContext.createFromUrl(
+ "https://source.developers.google.com/p/testing/r/default", "dummyrevision");
+ String testSourceContextJson =
+ "{\"cloudRepo\": {\"repoId\": {\"projectRepoId\":"
+ + " {\"projectId\": \"testing\", \"repoName\": \"default\"}},"
+ + " \"revisionId\": \"dummyrevision\"}}";
+ Application testApp = Application.readApplication(TEST_FILES, testSourceContext);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(StagingOptions.builder().setSplitJarFiles(Optional.of(true)).build());
+
+ testApp.createStagingDirectory(opts);
+ testStagedFiles(testApp);
+ File stage = testApp.getStagingDir();
+ File jsonFile = new File(stage, "WEB-INF/classes/source-context.json");
+ assertThat(jsonFile.isFile()).isTrue();
+ String contents = Files.asCharSource(jsonFile, StandardCharsets.UTF_8).read();
+ assertThat(testSourceContextJson).isEqualTo(contents);
+ File appYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "application: 'sampleapp'");
+ assertFileContains(appYaml, "\nversion: '1'");
+ }
+
+ @Test
+ public void testStagingForGcloudWithFilesAndConfigErasure() throws Exception {
+ Application testApp = Application.readApplication(TEST_FILES, null, null, null);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ testApp.validateForStaging();
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(StagingOptions.builder().setSplitJarFiles(Optional.of(true)).build());
+
+ File stagingDir = testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+ testStagedFiles(testApp);
+ // user staging copies yaml files from appengine-generated into the root
+ File copiedAppYaml = new File(stagingDir, "app.yaml");
+ File originalAppYaml = new File(stagingDir, "WEB-INF/appengine-generated/app.yaml");
+ assertThat(copiedAppYaml.canRead()).isTrue();
+
+ // the copied app.yaml is the original with a precautionary new line and an
+ // extra 'skip_files' line.
+ List expectedCopiedAppYaml = Files.readLines(originalAppYaml, UTF_8);
+ expectedCopiedAppYaml.add("");
+ expectedCopiedAppYaml.add("skip_files: app.yaml");
+ assertThat(Files.readLines(copiedAppYaml, UTF_8)).isEqualTo(expectedCopiedAppYaml);
+
+ String yamlString = Files.asCharSource(copiedAppYaml, UTF_8).read();
+ assertThat(yamlString.startsWith("application:")).isFalse();
+ assertThat(yamlString).doesNotContain("\napplication:");
+ assertThat(yamlString.startsWith("version:")).isFalse();
+ assertThat(yamlString).doesNotContain("\nversion:");
+ assertFileContains(copiedAppYaml, "\nskip_files: app.yaml");
+ }
+
+ private static void testStagedFiles(Application testApp) throws Exception {
+ File stage = testApp.getStagingDir();
+ File html = new File(stage, "__static__/random.html");
+ assertWithMessage("Can read " + html).that(html.canRead()).isTrue();
+ assertThat(html.length()).isEqualTo(RANDOM_HTML_SIZE);
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ File jspJar = new File(stage, "WEB-INF/lib/_ah_compiled_jsps-0000.jar");
+ assertThat(jspJar.exists()).isTrue();
+ // Test that the classes in the generated jar are for Java8.
+ assertThat(getJavaJarVersion(jspJar)).isEqualTo(8);
+ checkIfEntryIsInJarFile(jspJar, "org/apache/jsp/nested/testing_jsp.class");
+ assertThat(new File(stage, "nested/dukebanner.html").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/lib").isDirectory()).isTrue();
+ File appYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertThat(Files.asCharSource(appYaml, UTF_8).read()).contains("api_version: 'user_defined'");
+ }
+
+ //TODO(ludo) @Test
+ public void testStageForGcloudOnlyCopyAppYamlToRoot() throws IOException {
+ Application testApp =
+ Application.readApplication(getWarPath("stage-with-all-xmls"), null, null, null);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ File stagingDir = testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+ File generationDir = GenerationDirectory.getGenerationDirectory(stagingDir);
+
+ assertThat(new File(generationDir, "app.yaml").exists()).isTrue();
+ assertThat(new File(stagingDir, "app.yaml").exists()).isTrue();
+
+ assertThat(new File(generationDir, "cron.yaml").exists()).isTrue();
+ assertThat(new File(stagingDir, "cron.yaml").exists()).isFalse();
+
+ assertThat(new File(generationDir, "dispatch.yaml").exists()).isTrue();
+ assertThat(new File(stagingDir, "dispatch.yaml").exists()).isFalse();
+
+ assertThat(new File(generationDir, "dos.yaml").exists()).isTrue();
+ assertThat(new File(stagingDir, "dos.yaml").exists()).isFalse();
+
+ assertThat(new File(generationDir, "index.yaml").exists()).isTrue();
+ assertThat(new File(stagingDir, "index.yaml").exists()).isFalse();
+
+ assertThat(new File(generationDir, "queue.yaml").exists()).isTrue();
+ assertThat(new File(stagingDir, "queue.yaml").exists()).isFalse();
+ }
+
+ //TODO(ludo) @Test
+ public void testDoNotStageDispatchForUpdate() throws IOException {
+ Application testApp =
+ Application.readApplication(getWarPath("sample-dispatch"), null, null, null);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ File stagingDir = testApp.createStagingDirectory(opts);
+
+ File dispatchYaml = new File(stagingDir, "dispatch.yaml");
+ assertThat(dispatchYaml.exists()).isFalse();
+ }
+
+ @Test
+ public void testAppEngineApiJarIncluded() throws Exception {
+ File tmpDir = Files.createTempDir();
+ try {
+ doTestAppEngineApiJarIncluded(tmpDir, "impl", "lib/impl/appengine-api.jar");
+ } finally {
+ MoreFiles.deleteRecursively(tmpDir.toPath());
+ }
+ }
+
+ private static void doTestAppEngineApiJarIncluded(File tmpDir, String testName, String apiJarPath)
+ throws Exception {
+ File sdkRoot = new File(SDK_ROOT);
+ File apiJar = new File(sdkRoot, apiJarPath);
+ assertWithMessage(apiJar.toString()).that(apiJar.exists()).isTrue();
+ //TODO(ludo) File remoteApiJar = new File(sdkRoot, "lib/appengine-remote-api.jar");
+ //TODO(ludo) assertWithMessage(remoteApiJar.toString()).that(remoteApiJar.exists()).isTrue();
+ File testDir = new File(tmpDir, testName);
+ File webInf = new File(testDir, "WEB-INF");
+ File webInfLib = new File(webInf, "lib");
+ boolean madeWebInfLib = webInfLib.mkdirs();
+ assertThat(madeWebInfLib).isTrue();
+ Files.copy(apiJar, new File(webInfLib, "appengine-api.jar"));
+ //TODO(ludo) Files.copy(remoteApiJar, new File(webInfLib, "appengine-remote-api.jar"));
+ File testAppRoot = new File(TEST_FILES);
+ Files.copy(new File(testAppRoot, "WEB-INF/web.xml"), new File(webInf, "web.xml"));
+ Files.copy(
+ new File(testAppRoot, "WEB-INF/appengine-web.xml"), new File(webInf, "appengine-web.xml"));
+
+ Application application = Application.readApplication(testDir.getAbsolutePath());
+ application.setDetailsWriter(new PrintWriter(new OutputStreamWriter(System.out, UTF_8)));
+
+ assertThat(application.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ application.createStagingDirectory(opts);
+ File stage = application.getStagingDir();
+ File appYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "api_version: 'user_defined'");
+ File stagedApiJar = new File(stage, "WEB-INF/lib/appengine-api.jar");
+ assertWithMessage(stagedApiJar.toString()).that(stagedApiJar.exists()).isTrue();
+ byte[] originalApiJarBytes = Files.toByteArray(apiJar);
+ byte[] stagedApiJarBytes = Files.toByteArray(stagedApiJar);
+ assertThat(originalApiJarBytes).isEqualTo(stagedApiJarBytes);
+ }
+
+ @Test
+ public void testStagingJava8() throws Exception {
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ testApp.createStagingDirectory(opts);
+ File stage = testApp.getStagingDir();
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-generated/app.yaml").canRead()).isTrue();
+ assertFileContains(new File(stage, "WEB-INF/appengine-generated/app.yaml"), "runtime: java8\n");
+ }
+
+ @Test
+ public void testStagingJava11() throws Exception {
+ Application testApp = Application.readApplication(TEST_JAVA11);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ testApp.createStagingDirectory(opts);
+ File stage = testApp.getStagingDir();
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-generated/app.yaml").canRead()).isTrue();
+ File generatedAppYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(generatedAppYaml, "runtime: java11\n");
+ assertFileContains(generatedAppYaml, "runtime_channel: canary\n");
+ assertFileContains(generatedAppYaml, "entrypoint: 'java -jar foo.jar'\n");
+ }
+
+ @Test
+ public void testStagingJava17() throws Exception {
+ Application testApp = Application.readApplication(TEST_JAVA17);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ testApp.createStagingDirectory(opts);
+ File stage = testApp.getStagingDir();
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-generated/app.yaml").canRead()).isTrue();
+ File generatedAppYaml = new File(stage, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(generatedAppYaml, "runtime: java17\n");
+ assertFileContains(generatedAppYaml, "runtime_channel: canary\n");
+ assertFileContains(generatedAppYaml, "entrypoint: 'java -jar foo.jar'\n");
+ }
+
+ @Test
+ public void testJspCompilerJava8() throws Exception {
+ Application testApp = Application.readApplication(SERVLET3_STANDARD_APP_ROOT);
+ assertThat(testApp.getJSPCClassName())
+ .contains("com.google.appengine.tools.development.jetty");
+ assertThat(testApp.getJSPCClassName())
+ .contains("LocalJspC");
+ }
+
+ @Test
+ public void testStagingJava8RuntimeJava8JarFile() throws Exception {
+ Application testApp = Application.readApplication(JAVA8_JAR_TEST_FILES);
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setAllowAnyRuntime(true);
+ opts.setRuntime("java8");
+ File stage = testApp.createStagingDirectory(opts);
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-generated/app.yaml").canRead()).isTrue();
+ assertFileContains(new File(stage, "WEB-INF/appengine-generated/app.yaml"), "runtime: java8\n");
+ }
+
+ @Test
+ public void testStagingManuallySetUnsupportedRuntime() throws IOException {
+ try {
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setRuntime("foo");
+
+ testApp.createStagingDirectory(opts);
+ fail("Did not get expected AppEngineConfigException");
+ } catch (AppEngineConfigException expected) {
+ // Expect an exception flagging the runtime id as invalid.
+ assertThat(expected).hasMessageThat().contains("Invalid runtime id: foo");
+ }
+ }
+
+ @Test
+ public void testStagingManuallySetUnsupportedRuntimeAllowAnyRuntime() throws Exception {
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setRuntime("foo");
+ opts.setAllowAnyRuntime(true);
+
+ testApp.createStagingDirectory(opts);
+ File stage = testApp.getStagingDir();
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-generated/app.yaml").canRead()).isTrue();
+ assertFileContains(new File(stage, "WEB-INF/appengine-generated/app.yaml"), "runtime: foo\n");
+ }
+
+ private static void assertFileContains(File file, String expectedSubstring) throws Exception {
+ String contentsAsString = Files.asCharSource(file, UTF_8).read();
+ assertThat(contentsAsString).contains(expectedSubstring);
+ }
+
+ @Test
+ public void testWithJspx() throws IOException {
+ Application testApp = Application.readApplication(TEST_JSPX_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(StagingOptions.builder().setSplitJarFiles(Optional.of(true)).build());
+
+ testApp.createStagingDirectory(opts);
+ File stage = testApp.getStagingDir();
+ File html = new File(stage, "__static__/random.html");
+ assertThat(html.canRead()).isTrue();
+ assertThat(html.length()).isEqualTo(RANDOM_HTML_SIZE);
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ checkIfEntryIsInJarFile(
+ new File(stage, "WEB-INF/lib/_ah_compiled_jsps-0000.jar"),
+ "org/apache/jsp/nested/testing_jspx.class");
+ assertThat(new File(stage, "nested/dukebanner.html").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/lib").isDirectory()).isTrue();
+ }
+
+ /* @Test
+ public void testWithBigJarWithTlds() throws Exception {
+ Application testApp =
+ Application.readApplication(
+ TestUtil.getSrcDir()
+ + "/google3/javatests/com/google/appengine/tools/admin/deploy_fileset_big_jar_tld");
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(StagingOptions.builder().setSplitJarFiles(Optional.of(true)).build());
+
+ testApp.createStagingDirectory(opts);
+ File stage = testApp.getStagingDir();
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/lib").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/lib/bigjar_with_tlds-0000.jar").canRead()).isTrue();
+ // checkIfEntryIsInJarFile(new File(stage, "WEB-INF/lib/bigjar_with_tlds-0000.jar"),
+ // "META-INF/fn.tl");
+
+ File quickstartXml = new File(stage, "WEB-INF/quickstart-web.xml");
+
+ assertThat(quickstartXml.canRead()).isTrue();
+
+ Document quickstartDoc =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(quickstartXml);
+ // We want to verify that the list of tlds defined by:
+ //
+ // testApp.createStagingDirectory(opts));
+ assertThat(exception).hasMessageThat().isEqualTo("Failed to compile jsp files.");
+ }
+
+ @Test
+ public void testZipClassesWithoutJSPs() throws IOException {
+ Application testApp = Application.readApplication(CLASSES_TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+ assertThat(testApp.getAppId()).isEqualTo(APPID);
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ opts.setStagingOptions(StagingOptions.builder().setJarClasses(Optional.of(true)).build());
+
+ testApp.createStagingDirectory(opts);
+ File stage = testApp.getStagingDir();
+ assertThat(new File(stage, "WEB-INF").isDirectory()).isTrue();
+ assertThat(new File(stage, "WEB-INF/web.xml").canRead()).isTrue();
+ assertThat(new File(stage, "WEB-INF/appengine-web.xml").canRead()).isTrue();
+ checkIfEntryIsInJarFile(
+ new File(stage, "WEB-INF/lib/_ah_webinf_classes-0000.jar"), "foo/AClass.class");
+ assertThat(new File(stage, "WEB-INF/classes/foo/AClass.class").exists()).isFalse();
+ }
+
+ /**
+ * Check that precedence is respected, where defaults, appengine-web.xml and flags are applied in
+ * order. Flags have the highest precedence.
+ *
+ *
+ * Defaults are:
+ * - splitJarFiles: false
+ * - splitJarFilesExcludes: {} (empty set)
+ * - jarJsps: true
+ * - jarClasses: false
+ * - deleteJsps: false
+ * - compileEncoding: "UTF-8"
+ *
+ * Sample app's appengine-web.xml:
+ * - splitJarFilesExcludes: {"foo", "bar"}
+ * - deleteJsps: true
+ * - compileEncoding: "UTF-16"
+ *
+ * Flags (populated through ApplicationProcessingOptions):
+ * - splitJarFiles: true
+ * - splitJarFilesExcludes: {"baz"}
+ * - deleteJsps: false
+ *
+ * Expected result:
+ * - splitJarFiles: true
+ * - splitJarFilesExcludes: {"baz"}
+ * - jarJsps: true
+ * - jarClasses: false
+ * - deleteJsps: false
+ * - compileEncoding: "UTF-16"
+ *
+ */
+ @Test
+ public void testStagingOptionPrecedence() throws IOException {
+ Application testApp = Application.readApplication(STAGE_WITH_STAGING_OPTIONS);
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ // These correspond to the flags passed in to AppCfg
+ opts.setStagingOptions(
+ StagingOptions.builder()
+ .setSplitJarFiles(Optional.of(true))
+ .setSplitJarFilesExcludes(Optional.of(ImmutableSortedSet.of("baz")))
+ .setDeleteJsps(Optional.of(false))
+ .build());
+
+ StagingOptions expected =
+ StagingOptions.builder()
+ .setSplitJarFiles(Optional.of(true))
+ .setSplitJarFilesExcludes(Optional.of(ImmutableSortedSet.of("baz")))
+ .setJarJsps(Optional.of(true))
+ .setJarClasses(Optional.of(false))
+ .setDeleteJsps(Optional.of(false))
+ .setCompileEncoding(Optional.of("UTF-16"))
+ .build();
+
+ assertThat(testApp.getStagingOptions(opts)).isEqualTo(expected);
+ }
+
+ @Test
+ public void testWriteDefaultWebXml() throws IOException {
+ File tempAppDir = temporaryFolder.newFolder();
+ File webinfFile = new File(tempAppDir, "WEB-INF");
+ AppEngineConfigException exception =
+ assertThrows(
+ AppEngineConfigException.class,
+ () -> Application.readApplication(tempAppDir.toString()));
+ String expectedMessage =
+ "Could not find the WEB-INF directory in "
+ + webinfFile
+ + ". The given application file path must point to an exploded WAR directory that"
+ + " contains a WEB-INF directory.";
+ assertThat(exception).hasMessageThat().isEqualTo(expectedMessage);
+ webinfFile.mkdir();
+ Files.asCharSink(new File(webinfFile, "appengine-web.xml"), UTF_8)
+ .write(APPENGINE_WEB_XML_CONTENT);
+ File webXmlFile = new File(webinfFile, "web.xml");
+ assertThat(webXmlFile.exists()).isFalse();
+ try {
+ Application.readApplication(tempAppDir.toString());
+ assertThat(webXmlFile.canRead()).isTrue();
+ assertThat(Files.asCharSource(webXmlFile, UTF_8).read())
+ .isEqualTo(Application.DEFAULT_WEB_XML_CONTENT);
+ } finally {
+ deleteRecursively(tempAppDir.toPath());
+ }
+ }
+
+ private static void deleteRecursively(Path path) throws IOException {
+ java.nio.file.Files.walkFileTree(
+ path,
+ new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult postVisitDirectory(Path directory, IOException exc)
+ throws IOException {
+ java.nio.file.Files.delete(directory);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException {
+ java.nio.file.Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+
+ @Test
+ public void testXmlValidation() throws IOException {
+ Application testApp = Application.readApplication(XMLORDERING_TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ try {
+ testApp = Application.readApplication(BAD_WEBXML_TEST_FILES);
+ fail("Bad web.xml wasn't detected");
+ } catch (AppEngineConfigException ex) {
+ // good
+ }
+ try {
+ testApp = Application.readApplication(BAD_APPENGINEWEBXML_TEST_FILES);
+ fail("Bad appengine-web.xml wasn't detected");
+ } catch (AppEngineConfigException ex) {
+ // good
+ }
+ try {
+ testApp = Application.readApplication(BAD_INDEXESXML_TEST_FILES);
+ fail("Bad datastore-indexes.xml wasn't detected");
+ } catch (AppEngineConfigException ex) {
+ // good
+ }
+ try {
+ testApp = Application.readApplication(BAD_CRONXML_TEST_FILES);
+ fail("Bad datastore-indexes.xml wasn't detected");
+ } catch (AppEngineConfigException ex) {
+ // good
+ }
+ }
+
+ @Test
+ public void testBadRuntimeChannel() throws IOException {
+ try {
+ Application.readApplication(BAD_RUNTIME_CHANNEL);
+ fail("iappengine-web.xml content was incorrectly accepted with a runtime-channel.");
+ } catch (AppEngineConfigException ex) {
+ assertThat(ex)
+ .hasMessageThat()
+ .isEqualTo("'runtime-channel' is not valid with this runtime.");
+ }
+ }
+
+ @Test
+ public void testBadEntrypoint() throws IOException {
+ try {
+ Application.readApplication(BAD_ENTRYPOINT);
+ fail("appengine-web.xml content was incorrectly accepted with an entrypoint.");
+ } catch (AppEngineConfigException ex) {
+ assertThat(ex).hasMessageThat().isEqualTo("'entrypoint' is not valid with this runtime.");
+ }
+ }
+
+ @Test
+ public void testCronEntries() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+ CronXml cron = testApp.getCronXml();
+ assertWithMessage("App without cron entries should have null getCronXml()").that(cron).isNull();
+
+ testApp = Application.readApplication(NOJSP_TEST_FILES);
+ cron = testApp.getCronXml();
+ assertThat(cron.getEntries()).isEmpty();
+
+ testApp = Application.readApplication(XMLORDERING_TEST_FILES);
+ cron = testApp.getCronXml();
+ List entries = cron.getEntries();
+ assertThat(entries).hasSize(2);
+ assertThat(entries.get(0).getTimezone()).isEqualTo("HST");
+ assertThat(entries.get(0).getSchedule()).isEqualTo("every 2 hours");
+ assertThat(entries.get(0).getUrl()).isEqualTo("http://nowhere/fast");
+ assertThat(entries.get(0).getDescription()).isEmpty();
+ assertThat(entries.get(1).getTimezone()).isEqualTo("UTC");
+ assertThat(entries.get(1).getSchedule()).isEqualTo("every 3 hours");
+ assertThat(entries.get(1).getUrl()).isEqualTo("http://nowhere");
+ assertThat(entries.get(1).getDescription()).isEqualTo("Why it's here");
+ }
+
+ @Test
+ public void testCronRetryParameterEntries() throws IOException {
+ Application testApp = Application.readApplication(CRON_RETRY_PARAMETERS_TEST_FILES);
+ CronXml cron = testApp.getCronXml();
+ assertWithMessage("Application should contain cron entries.").that(cron).isNotNull();
+ List entries = cron.getEntries();
+ assertThat(entries).hasSize(4);
+
+ // all entries should exist for entry 0
+ RetryParametersXml retryParameters = entries.get(0).getRetryParameters();
+ assertThat(retryParameters.getRetryLimit().intValue()).isEqualTo(3);
+ assertThat(retryParameters.getAgeLimitSec().intValue()).isEqualTo(60 * 60 * 24 * 2);
+ assertThat(retryParameters.getMinBackoffSec()).isEqualTo(2.4);
+ assertThat(retryParameters.getMaxBackoffSec()).isEqualTo(10.5);
+ assertThat(retryParameters.getMaxDoublings().intValue()).isEqualTo(4);
+
+ // only retry limit should be set for entry 1
+ retryParameters = entries.get(1).getRetryParameters();
+ assertThat(retryParameters.getRetryLimit().intValue()).isEqualTo(2);
+ assertThat(retryParameters.getAgeLimitSec()).isNull();
+ assertThat(retryParameters.getMinBackoffSec()).isNull();
+ assertThat(retryParameters.getMaxBackoffSec()).isNull();
+ assertThat(retryParameters.getMaxDoublings()).isNull();
+
+ // no parameters should be set for entry 2
+ retryParameters = entries.get(2).getRetryParameters();
+ assertThat(retryParameters.getRetryLimit()).isNull();
+ assertThat(retryParameters.getAgeLimitSec()).isNull();
+ assertThat(retryParameters.getMinBackoffSec()).isNull();
+ assertThat(retryParameters.getMaxBackoffSec()).isNull();
+ assertThat(retryParameters.getMaxDoublings()).isNull();
+
+ // no retry parameters should exist for entry 3
+ retryParameters = entries.get(3).getRetryParameters();
+ assertThat(retryParameters).isNull();
+ }
+
+ @Test
+ public void testCronRetryParameterNegativeRetryLimit() throws IOException {
+ AppEngineConfigException aece =
+ assertThrows(
+ AppEngineConfigException.class,
+ () -> Application.readApplication(CRON_NEGATIVE_RETRY_LIMIT_TEST_FILES));
+ assertThat(aece).hasMessageThat().contains("XML error");
+ assertThat(aece).hasCauseThat().hasMessageThat().contains("'-3'");
+ }
+
+ @Test
+ public void testCronRetryParameterNegativeMaxBackoff() throws IOException {
+ AppEngineConfigException aece =
+ assertThrows(
+ AppEngineConfigException.class,
+ () -> Application.readApplication(CRON_NEGATIVE_MAX_BACKOFF_TEST_FILES));
+ assertThat(aece).hasMessageThat().contains("XML error");
+ assertThat(aece).hasCauseThat().hasMessageThat().contains("'-10.5'");
+ }
+
+ @Test
+ public void testCronRetryParameterBadAgeLimit() throws IOException {
+ AppEngineConfigException aece =
+ assertThrows(
+ AppEngineConfigException.class,
+ () -> Application.readApplication(CRON_BAD_AGE_LIMIT_TEST_FILES));
+ assertThat(aece).hasMessageThat().contains("XML error");
+ assertThat(aece).hasCauseThat().hasMessageThat().contains("'2x'");
+ }
+
+ @Test
+ public void testCronRetryParameterTwoMaxDoublings() throws IOException {
+ AppEngineConfigException aece =
+ assertThrows(
+ AppEngineConfigException.class,
+ () -> Application.readApplication(CRON_TWO_MAX_DOUBLINGS_TEST_FILES));
+ assertThat(aece).hasMessageThat().contains("XML error");
+ assertThat(aece).hasCauseThat().hasMessageThat().contains("'max-doublings'");
+ }
+
+ @Test
+ public void testRuntime() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES_RUNTIME_DEFINED);
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ testApp.createStagingDirectory(opts);
+ assertThat(testApp.getRuntime()).isEqualTo("foo-bar");
+ }
+
+ @Test
+ public void testServer() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES_AUTOMATIC_MODULE);
+ assertThat(testApp.getModule()).isEqualTo("stan");
+ assertThat(testApp.getInstanceClass()).isEqualTo("F8");
+ AppEngineWebXml.AutomaticScaling settings = testApp.getAppEngineWebXml().getAutomaticScaling();
+ assertThat(settings.isEmpty()).isFalse();
+ assertThat(settings.getMinPendingLatency()).isEqualTo("10.5s");
+ assertThat(settings.getMaxPendingLatency()).isEqualTo("10900ms");
+ assertThat(settings.getMinIdleInstances()).isEqualTo("automatic");
+ assertThat(settings.getMaxIdleInstances()).isEqualTo("10");
+ assertThat(settings.getMaxConcurrentRequests()).isEqualTo("20");
+ }
+
+ @Test
+ public void testManualServer() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES_MANUAL_MODULE);
+ assertThat(testApp.getModule()).isEqualTo("stan");
+ assertThat(testApp.getInstanceClass()).isEqualTo("B8");
+ AppEngineWebXml.ManualScaling settings = testApp.getAppEngineWebXml().getManualScaling();
+ assertThat(settings.isEmpty()).isFalse();
+ assertThat(settings.getInstances()).isEqualTo("10");
+ }
+
+ @Test
+ public void testBasicServer() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES_BASIC_MODULE);
+ assertThat(testApp.getModule()).isEqualTo("stan");
+ assertThat(testApp.getInstanceClass()).isEqualTo("B8");
+ AppEngineWebXml.BasicScaling settings = testApp.getAppEngineWebXml().getBasicScaling();
+ assertThat(settings.isEmpty()).isFalse();
+ assertThat(settings.getMaxInstances()).isEqualTo("11");
+ assertThat(settings.getIdleTimeout()).isEqualTo("10m");
+
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ File stagingDir = testApp.createStagingDirectory(opts);
+ }
+
+ @Test
+ public void testIncludeHttpHeaders() throws IOException {
+ Application testApp = Application.readApplication(INCLUDE_HTTP_HEADERS_TEST_FILES);
+ List includes =
+ testApp.getAppEngineWebXml().getStaticFileIncludes();
+
+ assertThat(includes).hasSize(1);
+
+ Map httpHeaders = includes.get(0).getHttpHeaders();
+ assertThat(httpHeaders).hasSize(2);
+ assertThat(httpHeaders.get("P3P")).isEqualTo("P3P header value");
+ assertThat(httpHeaders.get("Access-Control-Allow-Origin")).isEqualTo("http://example.org");
+ }
+
+ //TODO(ludo ) @Test
+ public void testDispatch() throws IOException {
+ Application testApp = Application.readApplication(getWarPath("sample-dispatch"));
+ String expectYaml = "dispatch:\n" + "- url: '*/userapp/*'\n" + " module: web\n";
+ assertThat(testApp.getDispatchXml().toYaml()).isEqualTo(expectYaml);
+ }
+
+ //TODO(ludo ) @Test
+ public void testDispatch_yaml() throws IOException {
+ Application testApp = Application.readApplication(getWarPath("sample-dispatch-yaml"));
+ String expectYaml = "dispatch:\n" + "- url: '*/*'\n" + " module: web\n";
+ assertThat(testApp.getDispatchXml().toYaml()).isEqualTo(expectYaml);
+ }
+
+ //TODO(ludo) @Test
+ public void testDispatch_xmlAndYaml() throws IOException {
+ Application testApp = Application.readApplication(getWarPath("sample-dispatch-xml-and-yaml"));
+ String expectYaml = "dispatch:\n" + "- url: '*/userapp/*'\n" + " module: web\n";
+ assertThat(testApp.getDispatchXml().toYaml()).isEqualTo(expectYaml);
+ }
+
+ @Test
+ public void testDispatch_invalidXml() throws IOException {
+ AppEngineConfigException aece =
+ assertThrows(
+ AppEngineConfigException.class,
+ () -> Application.readApplication(getWarPath("sample-baddispatch")));
+ assertThat(aece).hasMessageThat().contains("error validating");
+ }
+
+ @Test
+ public void testDispatch_invalidYaml() throws IOException {
+ AppEngineConfigException aece =
+ assertThrows(
+ AppEngineConfigException.class,
+ () -> Application.readApplication(getWarPath("sample-baddispatch-yaml")));
+ String expect =
+ "Unable to find property 'not-url' in "
+ + "com.google.apphosting.utils.config.DispatchYamlReader$DispatchYamlEntry";
+ assertThat(aece).hasMessageThat().isEqualTo(expect);
+ }
+
+ @Test
+ public void testDispatch_missing() throws IOException {
+ Application testApp = Application.readApplication(TEST_FILES_BACKENDS);
+ assertThat(testApp.getDispatchXml()).isNull();
+ }
+
+
+ @Test
+ public void testUseJava8Standard() throws Exception {
+ Application testApp = Application.readApplication(SERVLET3_STANDARD_APP_ROOT);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(SERVLET3_APP_ID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+
+ File stageDir = testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+ File appYaml = new File(stageDir, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "runtime: java8");
+ // See if the index.jsp file has been pre-compiled:
+ File jspJar = new File(stageDir, "WEB-INF/lib/_ah_compiled_jsps-0000.jar");
+ assertThat(jspJar.exists()).isTrue();
+ // Test that the classes in the generated jar are for Java8.
+ assertThat(getJavaJarVersion(jspJar)).isEqualTo(8);
+
+ // See if one of the Jetty9 or Jetty12 JSP jars is being added:
+ assertThat(
+ new File(stageDir, "WEB-INF/lib/org.apache.taglibs.taglibs-standard-impl-1.2.5.jar")
+ .exists()
+ // TODO need to extend for Jarkata APIs!
+ || new File(stageDir, "WEB-INF/lib/org.glassfish.web.javax.servlet.jsp.jstl-1.2.5.jar")
+ .exists())
+
+ .isTrue();
+ }
+
+ @Test
+ public void testStageGaeStandardJava8Servlet31QuickstartWebListenerMemcache()
+ throws IOException, ParserConfigurationException, SAXException {
+ Application testApp = Application.readApplication(SERVLET3_STANDARD_WEBLISTENER_MEMCACHE);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(SERVLET3_APP_ID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ File stageDir = testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+
+ File quickstartXml = new File(stageDir, "WEB-INF/quickstart-web.xml");
+ File minQuickstartXml = new File(stageDir, "WEB-INF/min-quickstart-web.xml");
+
+ assertThat(quickstartXml.canRead()).isTrue();
+ assertThat(minQuickstartXml.canRead()).isTrue();
+
+ Document quickstartDoc =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(quickstartXml);
+ // No more CloudSqlConnectionCleanupFilter in 9.4 (defined as a RequestListener)
+ assertThat(
+ quickstartDoc.getDocumentElement().getElementsByTagName("filter-mapping").getLength())
+ .isEqualTo(0);
+ // No more default servlets in webdefault.xml, all defined inside our jetty 9.4 init code.
+ assertThat(
+ quickstartDoc.getDocumentElement().getElementsByTagName("servlet-mapping").getLength())
+ .isEqualTo(0);
+
+ Document minQuickstartDoc =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(minQuickstartXml);
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("filter-mapping")
+ .getLength())
+ .isEqualTo(0);
+
+ // 0 user defined annotated servlet in the SERVLET3_STANDARD_WEBLISTENER_MEMCACHE application.
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("servlet-mapping")
+ .getLength())
+ .isEqualTo(0);
+
+ // 1 user defined annotated weblistener in the SERVLET3_STANDARD_WEBLISTENER_MEMCACHE
+ // application.
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("listener-class")
+ .getLength())
+ .isEqualTo(1);
+
+ // We should not see any welcome-file entries:
+ assertThat(
+ minQuickstartDoc.getDocumentElement().getElementsByTagName("welcome-file").getLength())
+ .isEqualTo(0);
+
+ // We should not see any security-constraint entries:
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("security-constraint")
+ .getLength())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testStageGaeStandardJava8Servlet31QuickstartWithoutJSP()
+ throws IOException, ParserConfigurationException, SAXException {
+ Application testApp = Application.readApplication(SERVLET3_STANDARD_APP_NO_JSP_ROOT);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(SERVLET3_APP_ID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ File stageDir = testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+
+ File quickstartXml = new File(stageDir, "WEB-INF/quickstart-web.xml");
+ File minQuickstartXml = new File(stageDir, "WEB-INF/min-quickstart-web.xml");
+
+ assertThat(quickstartXml.canRead()).isTrue();
+ assertThat(minQuickstartXml.canRead()).isTrue();
+
+ Document quickstartDoc =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(quickstartXml);
+ assertThat(
+ quickstartDoc.getDocumentElement().getElementsByTagName("filter-mapping").getLength())
+ .isEqualTo(0);
+ assertThat(
+ quickstartDoc.getDocumentElement().getElementsByTagName("servlet-mapping").getLength())
+ .isEqualTo(1);
+
+ Document minQuickstartDoc =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(minQuickstartXml);
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("filter-mapping")
+ .getLength())
+ .isEqualTo(0);
+ // 1 user defined annotated servlet in the SERVLET3_STANDARD_APP_NO_JSP_ROOT application.
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("servlet-mapping")
+ .getLength())
+ .isEqualTo(1);
+
+ // We should not see any welcome-file entries:
+ assertThat(
+ minQuickstartDoc.getDocumentElement().getElementsByTagName("welcome-file").getLength())
+ .isEqualTo(0);
+ // We should not see any security-constraint entries:
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("security-constraint")
+ .getLength())
+ .isEqualTo(0);
+
+ // We want to verify we do not have any TLDs and resourcesdefined from useless JSP jars:
+ //
+ // org.eclipse.jetty.tlds
+ //
+ //
+ //
+ // org.eclipse.jetty.resources
+ //
+ //
+ //
+ // org.eclipse.jetty.originAttribute
+ // origin
+ //
+
+ NodeList nodeList = quickstartDoc.getElementsByTagName("context-param");
+ assertThat(nodeList.getLength()).isEqualTo(3);
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ Node contextParam = nodeList.item(i).getFirstChild();
+ int nbParamValue = 0;
+ do {
+ String nodeName = contextParam.getNodeName();
+ if (nodeName.equals("param-value")) {
+ nbParamValue++;
+ String content = contextParam.getFirstChild().getTextContent();
+ assertThat(content).isIn(Arrays.asList("", "origin"));
+ }
+ } while ((contextParam = contextParam.getNextSibling()) != null);
+ assertThat(nbParamValue).isEqualTo(1);
+ }
+
+ List patterns = testApp.getWebXml().getServletPatterns();
+ assertThat(patterns).contains("/test/*");
+ assertThat(patterns).doesNotContain("/_ah/queue/__deferred__");
+ assertThat(patterns).doesNotContain("/_ah/sessioncleanup");
+ assertThat(patterns).doesNotContain("/");
+ assertThat(patterns).doesNotContain("/*");
+ }
+
+ @Test
+ public void testStageGaeStandardJava8Servlet31QuickstartWithJSP()
+ throws IOException, ParserConfigurationException, SAXException {
+
+ Application testApp = Application.readApplication(SERVLET3_STANDARD_APP_ROOT);
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ assertThat(testApp.getAppId()).isEqualTo(SERVLET3_APP_ID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ // We want JSP compilation for Standard Java8.
+ opts.setCompileJsps(true);
+ File stageDir = testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+
+ File quickstartXml = new File(stageDir, "WEB-INF/quickstart-web.xml");
+ File minQuickstartXml = new File(stageDir, "WEB-INF/min-quickstart-web.xml");
+
+ assertThat(quickstartXml.canRead()).isTrue();
+ assertThat(minQuickstartXml.canRead()).isTrue();
+
+ Document quickstartDoc =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(quickstartXml);
+ assertThat(
+ quickstartDoc.getDocumentElement().getElementsByTagName("filter-mapping").getLength())
+ .isEqualTo(0);
+ assertThat(
+ quickstartDoc.getDocumentElement().getElementsByTagName("servlet-mapping").getLength())
+ .isEqualTo(2);
+
+ Document minQuickstartDoc =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(minQuickstartXml);
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("filter-mapping")
+ .getLength())
+ .isEqualTo(0);
+ // 2 user defined servlets in the SERVLET3_APP_ID application, 1 by annotation and 1 by
+ // a pre-compiled JSP.
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("servlet-mapping")
+ .getLength())
+ .isEqualTo(2);
+
+ // We should not see any welcome-file entries:
+ assertThat(
+ minQuickstartDoc.getDocumentElement().getElementsByTagName("welcome-file").getLength())
+ .isEqualTo(0);
+ // We should not see any security-constraint entries:
+ assertThat(
+ minQuickstartDoc
+ .getDocumentElement()
+ .getElementsByTagName("security-constraint")
+ .getLength())
+ .isEqualTo(0);
+
+ // We want to verify that the list of tlds defined by:
+ //
+ // org.eclipse.jetty.tlds
+ // patterns = testApp.getWebXml().getServletPatterns();
+ assertThat(patterns).contains("/test/*");
+ assertThat(patterns).contains("/index.jsp");
+ assertThat(patterns).doesNotContain("/_ah/queue/__deferred__");
+ assertThat(patterns).doesNotContain("/_ah/sessioncleanup");
+ assertThat(patterns).doesNotContain("/");
+ assertThat(patterns).doesNotContain("/*");
+ }
+
+ @Test
+ public void testStageGaeStandardJava8WithOnlyJasperContextInitializer()
+ throws IOException, ParserConfigurationException, SAXException {
+
+ Application testApp = Application.readApplication(SERVLET3_STANDARD_APP_ROOT);
+ testApp.setDetailsWriter(new PrintWriter(new OutputStreamWriter(System.out, UTF_8)));
+
+ assertThat(testApp.getAppId()).isEqualTo(SERVLET3_APP_ID);
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ // We want JSP compilation for Standard Java8.
+ opts.setCompileJsps(true);
+
+ testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+ assertThat(testApp.getWebXml().getFallThroughToRuntime()).isFalse();
+ String expectedJasperInitializer =
+ "\"ContainerInitializer"
+ + "{org.eclipse.jetty.apache.jsp.JettyJasperInitializer"
+ + ",interested=[],applicable=[],annotated=[]}\"";
+ Map trimmedContextParams =
+ Maps.transformValues(testApp.getWebXml().getContextParams(), String::trim);
+ assertThat(trimmedContextParams)
+ .containsEntry("org.eclipse.jetty.containerInitializers", expectedJasperInitializer);
+ }
+
+ //TODO(ludo) @Test
+ public void testStageGaeStandardJava8WithContextInitializers()
+ throws IOException, ParserConfigurationException, SAXException {
+ Application testApp = Application.readApplication(SERVLET3_STANDARD_APP_WITH_CONTAINER_INIT);
+ testApp.setDetailsWriter(new PrintWriter(new OutputStreamWriter(System.out, UTF_8)));
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+
+ // We want JSP compilation for Standard Java8.
+ opts.setCompileJsps(true);
+ testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+ assertThat(testApp.getWebXml().getFallThroughToRuntime()).isTrue();
+ String expectedJasperInitializer =
+ "\"ContainerInitializer"
+ + "{servletthree.Servlet3ContainerInitializer"
+ + ",interested=[],applicable=[],annotated=[]}\"";
+ Map trimmedContextParams =
+ Maps.transformValues(testApp.getWebXml().getContextParams(), String::trim);
+ assertThat(trimmedContextParams)
+ .containsEntry("org.eclipse.jetty.containerInitializers", expectedJasperInitializer);
+ }
+
+
+ @Test
+ public void testCountClasses() throws IOException {
+ assertThat(Application.countClasses(new File(CLASSES_TEST_FILES, "/WEB-INF/classes")))
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void testCountClasses_noClassesDir() throws IOException {
+ // Test that we don't explode when WEB-INF/classes directory doesn't exist.
+ Application testApp = Application.readApplication(TEST_FILES);
+ testApp.createStagingDirectory(new ApplicationProcessingOptions());
+ }
+
+ @Test
+ public void testMimetypes() throws Exception {
+ assertThat(Application.guessContentTypeFromName("foo.class")).isEqualTo("application/java-vm");
+ assertThat(Application.guessContentTypeFromName("foo.css")).isEqualTo("text/css");
+ assertThat(Application.guessContentTypeFromName("foo.gif")).isEqualTo("image/gif");
+ assertThat(Application.guessContentTypeFromName("foo.ico")).isEqualTo("image/x-icon");
+ assertThat(Application.guessContentTypeFromName("foo.java")).isEqualTo("text/plain");
+ assertThat(Application.guessContentTypeFromName("foo.jar"))
+ .isEqualTo("application/java-archive");
+ assertThat(Application.guessContentTypeFromName("foo.jpe")).isEqualTo("image/jpeg");
+ assertThat(Application.guessContentTypeFromName("foo.jpeg")).isEqualTo("image/jpeg");
+ assertThat(Application.guessContentTypeFromName("foo.json")).isEqualTo("application/json");
+ assertThat(Application.guessContentTypeFromName("foo.htm")).isEqualTo("text/html");
+ assertThat(Application.guessContentTypeFromName("foo.html")).isEqualTo("text/html");
+ assertThat(Application.guessContentTypeFromName("foo.zip")).isEqualTo("application/zip");
+ assertThat(Application.guessContentTypeFromName("foo.ludo"))
+ .isEqualTo("application/octet-stream");
+ assertThat(Application.guessContentTypeFromName("foo.wasm")).isEqualTo("application/wasm");
+ }
+
+ @Test
+ public void testJava8NoWebXmlNoApiJar() throws Exception {
+
+ Path temp = CopyDirVisitor.createTempDirectoryFrom(Paths.get(JAVA8_NO_WEBXML));
+ Application testApp = Application.readApplication(temp.toFile().getAbsolutePath());
+ testApp.setDetailsWriter(
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
+
+ ApplicationProcessingOptions opts = new ApplicationProcessingOptions();
+ // No JSP compilation for this sample.
+ opts.setCompileJsps(false);
+ File stageDir = testApp.createStagingDirectory(opts, temporaryFolder.newFolder());
+
+ File appYaml = new File(stageDir, "WEB-INF/appengine-generated/app.yaml");
+ assertFileContains(appYaml, "runtime: java8");
+ assertFileContains(appYaml, "threadsafe: True");
+ assertFileContains(appYaml, "api_version: 'user_defined'");
+ }
+
+ /** returns the Java version of the first class in the jar, or -1 if error. */
+ private static int getJavaJarVersion(File jarFile) throws Exception {
+ try (JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jarFile))) {
+ JarEntry jarEntry = jarInputStream.getNextJarEntry();
+ while (jarEntry != null) {
+ if (jarEntry.getName().endsWith(".class")) {
+ // We only check the first class file in the jar.
+ DataInputStream in = new DataInputStream(jarInputStream);
+ if (in.readInt() == 0xcafebabe) {
+ in.readShort(); // discard minor version
+ int majorVersion = in.readShort();
+ int actualVersion = majorVersion - 44;
+ return actualVersion;
+ }
+ jarEntry = jarInputStream.getNextJarEntry();
+ }
+ }
+ return -1;
+ }
+ }
+
+ private static class CopyDirVisitor extends SimpleFileVisitor {
+
+ private final Path fromPath;
+ private final Path toPath;
+
+ CopyDirVisitor(Path fromPath, Path toPath) {
+ this.fromPath = fromPath;
+ this.toPath = toPath;
+ }
+ // Return a temp directory that contains the from directory
+ static Path createTempDirectoryFrom(Path from) throws IOException {
+ Path to = java.nio.file.Files.createTempDirectory("staging");
+ java.nio.file.Files.walkFileTree(
+ from,
+ EnumSet.of(FileVisitOption.FOLLOW_LINKS),
+ Integer.MAX_VALUE,
+ new CopyDirVisitor(from, to));
+ deleteOnExit(to.toFile());
+ return to;
+ }
+
+ private static void deleteOnExit(File folder) {
+ folder.deleteOnExit();
+ for (File file : folder.listFiles()) {
+ if (file.isDirectory()) {
+ deleteOnExit(file);
+ } else {
+ file.deleteOnExit();
+ }
+ }
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+ throws IOException {
+ Path targetPath = toPath.resolve(fromPath.relativize(dir));
+ if (!java.nio.file.Files.exists(targetPath)) {
+ java.nio.file.Files.createDirectory(targetPath);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ java.nio.file.Files.copy(
+ file, toPath.resolve(fromPath.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
+ return FileVisitResult.CONTINUE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/e2etests/testlocalapps/badcron/pom.xml b/e2etests/testlocalapps/badcron/pom.xml
new file mode 100644
index 000000000..33038201e
--- /dev/null
+++ b/e2etests/testlocalapps/badcron/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ badcron
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: badcron
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..4474f35a8
--- /dev/null
+++ b/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..431c840df
--- /dev/null
+++ b/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,37 @@
+
+
+
+<
+
+cronentries>
+ http://nowhere
+ GMQ
+
diff --git a/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d3b5d4b3b
--- /dev/null
+++ b/e2etests/testlocalapps/badcron/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/bundle_standard/pom.xml b/e2etests/testlocalapps/bundle_standard/pom.xml
new file mode 100644
index 000000000..b3e918607
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ bundle_standard
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: bundle_standard
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard/src/main/java/servletthree/Servlet3Test.java b/e2etests/testlocalapps/bundle_standard/src/main/java/servletthree/Servlet3Test.java
new file mode 100644
index 000000000..e7a8fb82e
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard/src/main/java/servletthree/Servlet3Test.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package servletthree;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import javax.servlet.annotation.WebInitParam;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ */
+@WebServlet(
+ name = "servlet3test",
+ urlPatterns = {"/test/*"},
+ initParams = {
+ @WebInitParam(name = "prefix", value = "<<<"),
+ @WebInitParam(name = "suffix", value = ">>>")
+ })
+public class Servlet3Test extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ resp.setStatus(200);
+ try (PrintWriter writer =
+ new PrintWriter(
+ new BufferedWriter(new OutputStreamWriter(resp.getOutputStream(), UTF_8)))) {
+ String prefix = getInitParameter("prefix");
+ String suffix = getInitParameter("suffix");
+ writer.println(prefix + req.getRequestURI() + suffix);
+ // Check we are not running with a security manager:
+ SecurityManager security = System.getSecurityManager();
+ if (security != null) {
+ throw new RuntimeException("Security manager detected.");
+ }
+ }
+ }
+}
diff --git a/e2etests/testlocalapps/bundle_standard/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/bundle_standard/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..a0244565b
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,23 @@
+
+
+
+ servlet3-demo
+ 1
+ true
+ true
+ java8
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/bundle_standard/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d30021931
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,22 @@
+
+
+
+ Servlet3 Test
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard/src/main/webapp/index.jsp b/e2etests/testlocalapps/bundle_standard/src/main/webapp/index.jsp
new file mode 100644
index 000000000..740dee8f7
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard/src/main/webapp/index.jsp
@@ -0,0 +1,27 @@
+
+
+ <%@page contentType="text/html" pageEncoding="UTF-8"%>
+
+
+
+
+ JSP Page
+
+
+ Hello World!
+
+
diff --git a/e2etests/testlocalapps/bundle_standard_with_container_initializer/pom.xml b/e2etests/testlocalapps/bundle_standard_with_container_initializer/pom.xml
new file mode 100644
index 000000000..1f182bb4f
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_container_initializer/pom.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ bundle_standard_with_container_initializer
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: bundle_standard_with_container_initializer
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+ com.google.auto.value
+ auto-value
+ 1.10.2
+
+
+ com.google.auto.service
+ auto-service-annotations
+ 1.0.1
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/java/servletthree/Servlet3ContainerInitializer.java b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/java/servletthree/Servlet3ContainerInitializer.java
new file mode 100644
index 000000000..46ace6fc8
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/java/servletthree/Servlet3ContainerInitializer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package servletthree;
+
+import com.google.auto.service.AutoService;
+import java.util.Set;
+import javax.servlet.ServletContainerInitializer;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+/** Simple ServletContainerInitializer */
+@AutoService(ServletContainerInitializer.class)
+public class Servlet3ContainerInitializer implements ServletContainerInitializer {
+
+ @Override
+ public void onStartup(Set> set, ServletContext servletContext) throws ServletException {
+ System.out.println("test");
+ }
+}
diff --git a/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..a0244565b
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,23 @@
+
+
+
+ servlet3-demo
+ 1
+ true
+ true
+ java8
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d30021931
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,22 @@
+
+
+
+ Servlet3 Test
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/index.jsp b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/index.jsp
new file mode 100644
index 000000000..740dee8f7
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_container_initializer/src/main/webapp/index.jsp
@@ -0,0 +1,27 @@
+
+
+ <%@page contentType="text/html" pageEncoding="UTF-8"%>
+
+
+
+
+ JSP Page
+
+
+ Hello World!
+
+
diff --git a/e2etests/testlocalapps/bundle_standard_with_no_jsp/pom.xml b/e2etests/testlocalapps/bundle_standard_with_no_jsp/pom.xml
new file mode 100644
index 000000000..13aec81de
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_no_jsp/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ bundle_standard_with_no_jsp
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: bundle_standard_with_no_jsp
+
+
+ UTF-8
+ true
+ UTF-8
+ 2.7.2
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/java/servletthree/Servlet3Test.java b/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/java/servletthree/Servlet3Test.java
new file mode 100644
index 000000000..e7a8fb82e
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/java/servletthree/Servlet3Test.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package servletthree;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import javax.servlet.annotation.WebInitParam;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ */
+@WebServlet(
+ name = "servlet3test",
+ urlPatterns = {"/test/*"},
+ initParams = {
+ @WebInitParam(name = "prefix", value = "<<<"),
+ @WebInitParam(name = "suffix", value = ">>>")
+ })
+public class Servlet3Test extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ resp.setStatus(200);
+ try (PrintWriter writer =
+ new PrintWriter(
+ new BufferedWriter(new OutputStreamWriter(resp.getOutputStream(), UTF_8)))) {
+ String prefix = getInitParameter("prefix");
+ String suffix = getInitParameter("suffix");
+ writer.println(prefix + req.getRequestURI() + suffix);
+ // Check we are not running with a security manager:
+ SecurityManager security = System.getSecurityManager();
+ if (security != null) {
+ throw new RuntimeException("Security manager detected.");
+ }
+ }
+ }
+}
diff --git a/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..3d7088694
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,23 @@
+
+
+
+ servlet3-demo
+ 1
+ true
+ true
+ java8
+
diff --git a/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d30021931
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_no_jsp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,22 @@
+
+
+
+ Servlet3 Test
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/pom.xml b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/pom.xml
new file mode 100644
index 000000000..c12db4018
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/pom.xml
@@ -0,0 +1,75 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ bundle_standard_with_weblistener_memcache
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: bundle_standard_with_weblistener_memcache
+
+
+ UTF-8
+ true
+ UTF-8
+ 2.7.2
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ 2.0.17
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/java/servletthree/WebListenerWithMemcache.java b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/java/servletthree/WebListenerWithMemcache.java
new file mode 100644
index 000000000..70db2c0e8
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/java/servletthree/WebListenerWithMemcache.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package servletthree;
+
+import com.google.appengine.api.memcache.BaseMemcacheService;
+import com.google.appengine.api.memcache.ErrorHandler;
+import com.google.appengine.api.memcache.InvalidValueException;
+import com.google.appengine.api.memcache.MemcacheServiceException;
+import com.google.appengine.api.memcache.MemcacheServiceFactory;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.annotation.WebListener;
+
+/** Simple WebListener that depends on some GAE API (memcache) to reproduce b/120480580. */
+@WebListener
+public class WebListenerWithMemcache implements ServletContextListener {
+ @Override
+ public void contextInitialized(ServletContextEvent servletContextEvent) {
+ BaseMemcacheService bms = MemcacheServiceFactory.getMemcacheService();
+ bms.setErrorHandler(
+ new ErrorHandler() {
+ @Override
+ public void handleDeserializationError(InvalidValueException e) {}
+
+ @Override
+ public void handleServiceError(MemcacheServiceException e) {}
+ });
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent servletContextEvent) {}
+}
diff --git a/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..a0244565b
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,23 @@
+
+
+
+ servlet3-demo
+ 1
+ true
+ true
+ java8
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d30021931
--- /dev/null
+++ b/e2etests/testlocalapps/bundle_standard_with_weblistener_memcache/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,22 @@
+
+
+
+ Servlet3 Test
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/cron-bad-job-age-limit/pom.xml b/e2etests/testlocalapps/cron-bad-job-age-limit/pom.xml
new file mode 100644
index 000000000..48cd15119
--- /dev/null
+++ b/e2etests/testlocalapps/cron-bad-job-age-limit/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ cron-bad-job-age-limit
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: cron-bad-job-age-limit
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..6d6e6ebbd
--- /dev/null
+++ b/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ /nowhere_1
+ every 5 hours
+
+ 3
+ 2x
+ 2.4
+ 10.5
+ 4
+
+ GMT
+ all retry parameters
+
+
+
diff --git a/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/cron-bad-job-age-limit/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/cron-good-retry-parameters/pom.xml b/e2etests/testlocalapps/cron-good-retry-parameters/pom.xml
new file mode 100644
index 000000000..13977cfb4
--- /dev/null
+++ b/e2etests/testlocalapps/cron-good-retry-parameters/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ cron-good-retry-parameters
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: cron-good-retry-parameters
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..8622d38c5
--- /dev/null
+++ b/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+ /nowhere_1
+ every 5 hours
+
+ 3
+ 2d
+ 2.4
+ 10.5
+ 4
+
+ GMT
+ all retry parameters
+
+
+
+ /nowhere_2
+ every 6 hours
+
+ 2
+
+ GMT
+ one retry parameter
+
+
+
+ /nowhere_3
+ every 7 hours
+
+
+ GMT
+ empty retry parameters
+
+
+
+ /nowhere_4
+ every 8 hours
+ GMT
+ no retry parameters
+
+
+
diff --git a/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/cron-good-retry-parameters/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/cron-negative-max-backoff/pom.xml b/e2etests/testlocalapps/cron-negative-max-backoff/pom.xml
new file mode 100644
index 000000000..24430eaeb
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-max-backoff/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ cron-negative-max-backoff
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: cron-negative-max-backoff
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..8e2419659
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ /nowhere_1
+ every 5 hours
+
+ 3
+ 2d
+ 2.4
+ -10.5
+ 4
+
+ GMT
+ all retry parameters
+
+
+
diff --git a/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-max-backoff/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/cron-negative-retry-limit/pom.xml b/e2etests/testlocalapps/cron-negative-retry-limit/pom.xml
new file mode 100644
index 000000000..6757ac160
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-retry-limit/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ cron-negative-retry-limit
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: cron-negative-retry-limit
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..2e9931a9b
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ /nowhere_1
+ every 5 hours
+
+ -3
+ 2d
+ 2.4
+ 10.5
+ 4
+
+ GMT
+ all retry parameters
+
+
+
diff --git a/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/cron-negative-retry-limit/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/cron-two-max-doublings/pom.xml b/e2etests/testlocalapps/cron-two-max-doublings/pom.xml
new file mode 100644
index 000000000..82999ef1d
--- /dev/null
+++ b/e2etests/testlocalapps/cron-two-max-doublings/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ cron-two-max-doublings
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: cron-two-max-doublings
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..1dc30acea
--- /dev/null
+++ b/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ /nowhere_1
+ every 5 hours
+
+ 3
+ 2d
+ 2.4
+ 10.5
+ 4
+ 3
+
+ GMT
+ all retry parameters
+
+
+
diff --git a/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/cron-two-max-doublings/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/http-headers/pom.xml b/e2etests/testlocalapps/http-headers/pom.xml
new file mode 100644
index 000000000..7fb86db76
--- /dev/null
+++ b/e2etests/testlocalapps/http-headers/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ http-headers
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: http-headers
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/http-headers/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/http-headers/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..8b1a665c6
--- /dev/null
+++ b/e2etests/testlocalapps/http-headers/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/http-headers/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/http-headers/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/http-headers/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/java8-jar/pom.xml b/e2etests/testlocalapps/java8-jar/pom.xml
new file mode 100644
index 000000000..9889c0820
--- /dev/null
+++ b/e2etests/testlocalapps/java8-jar/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ java8-jar
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: java8-jar
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/java8-jar/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/java8-jar/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..9b3be4c69
--- /dev/null
+++ b/e2etests/testlocalapps/java8-jar/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ sampleapp
+ 1
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/java8-jar/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/java8-jar/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a3f85d4f0
--- /dev/null
+++ b/e2etests/testlocalapps/java8-jar/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/e2etests/testlocalapps/java8-no-webxml/pom.xml b/e2etests/testlocalapps/java8-no-webxml/pom.xml
new file mode 100644
index 000000000..3a27f2ee2
--- /dev/null
+++ b/e2etests/testlocalapps/java8-no-webxml/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ java8-no-webxml
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: java8-no-webxml
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/java8-no-webxml/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/java8-no-webxml/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..a3aa6e449
--- /dev/null
+++ b/e2etests/testlocalapps/java8-no-webxml/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ sampleapp
+ java8
+ true
+
diff --git a/e2etests/testlocalapps/java8-static b/e2etests/testlocalapps/java8-static
new file mode 100644
index 000000000..52a278aa7
--- /dev/null
+++ b/e2etests/testlocalapps/java8-static
@@ -0,0 +1,7 @@
+
+
+Nonsense
+
+This is just a test file.
+
+
diff --git a/e2etests/testlocalapps/pom.xml b/e2etests/testlocalapps/pom.xml
new file mode 100644
index 000000000..1272d7045
--- /dev/null
+++ b/e2etests/testlocalapps/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+
+ 4.0.0
+ testlocalapss
+ AppEngine :: Test local applications
+
+ com.google.appengine
+ e2etests
+ 2.0.20-SNAPSHOT
+
+ pom
+
+ true
+
+
+ badcron
+ cron-bad-job-age-limit
+ cron-good-retry-parameters
+ cron-negative-max-backoff
+ cron-negative-retry-limit
+ cron-two-max-doublings
+ http-headers
+ java8-jar
+ java8-no-webxml
+ sampleapp
+ sampleapp-automatic-module
+ sampleapp-backends
+ sampleapp-basic-module
+ sampleapp-manual-module
+ sampleapp-runtime
+ sample-badaeweb
+ sample-badentrypoint
+ sample-badindexes
+ sample-badruntimechannel
+ sample-badweb
+ sample-baddispatch
+ sample-baddispatch-yaml
+ sample-default-auto-ids
+ sample-error-in-tag-file
+ sample-java11
+ sample-java17
+ sample-jsptaglibrary
+ sample-jspx
+ sample-legacy-auto-ids
+ sample-missingappid
+ sample-nojsps
+ sample-unspecified-auto-ids
+ sample-with-classes
+ stage-sampleapp
+ stage-with-staging-options
+ xmlorder
+ bundle_standard
+ bundle_standard_with_container_initializer
+ bundle_standard_with_no_jsp
+ bundle_standard_with_weblistener_memcache
+
+
diff --git a/e2etests/testlocalapps/sample-badaeweb/pom.xml b/e2etests/testlocalapps/sample-badaeweb/pom.xml
new file mode 100644
index 000000000..e73fd55e4
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badaeweb/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-badaeweb
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-badaeweb
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-badaeweb/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-badaeweb/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..67b21e208
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badaeweb/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-badaeweb/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-badaeweb/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badaeweb/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-baddispatch-yaml/pom.xml b/e2etests/testlocalapps/sample-baddispatch-yaml/pom.xml
new file mode 100644
index 000000000..945c6b8b2
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch-yaml/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-baddispatch-yaml
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-baddispatch-yaml
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..1a5660871
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,29 @@
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/dispatch.yaml b/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/dispatch.yaml
new file mode 100644
index 000000000..eec9d0100
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/dispatch.yaml
@@ -0,0 +1,18 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+dispatch:
+ - not-url: a.b.com/c
+ service: web
+
diff --git a/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d3b5d4b3b
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch-yaml/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-baddispatch/pom.xml b/e2etests/testlocalapps/sample-baddispatch/pom.xml
new file mode 100644
index 000000000..9e15fe380
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-baddispatch
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-baddispatch
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..1a5660871
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,29 @@
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/dispatch.xml b/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/dispatch.xml
new file mode 100644
index 000000000..e598679cf
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/dispatch.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ */userapp/*
+ web
+
+
diff --git a/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d3b5d4b3b
--- /dev/null
+++ b/e2etests/testlocalapps/sample-baddispatch/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-badentrypoint/pom.xml b/e2etests/testlocalapps/sample-badentrypoint/pom.xml
new file mode 100644
index 000000000..816462415
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badentrypoint/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-badentrypoint
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-badentrypoint
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-badentrypoint/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-badentrypoint/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..9e457edba
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badentrypoint/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ true
+ java8
+ java -jar foo.jar
+
diff --git a/e2etests/testlocalapps/sample-badentrypoint/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-badentrypoint/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..974d1619f
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badentrypoint/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-badindexes/pom.xml b/e2etests/testlocalapps/sample-badindexes/pom.xml
new file mode 100644
index 000000000..7e2ba05ac
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badindexes/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-badindexes
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-badindexes
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..c8005e891
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/datastore-indexes.xml b/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 000000000..f5a364cf4
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badindexes/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-badruntimechannel/pom.xml b/e2etests/testlocalapps/sample-badruntimechannel/pom.xml
new file mode 100644
index 000000000..237bf284f
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badruntimechannel/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-badruntimechannel
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-badruntimechannel
+
+
+ UTF-8
+ true
+ UTF-8
+ 2.7.2
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-badruntimechannel/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-badruntimechannel/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..b33f19cb1
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badruntimechannel/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ true
+ java8
+ canary
+
diff --git a/e2etests/testlocalapps/sample-badruntimechannel/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-badruntimechannel/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..974d1619f
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badruntimechannel/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-badweb/pom.xml b/e2etests/testlocalapps/sample-badweb/pom.xml
new file mode 100644
index 000000000..e62bc92a0
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badweb/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-badweb
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-badweb
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-badweb/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-badweb/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..c8005e891
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badweb/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-badweb/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-badweb/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..8b58c09d6
--- /dev/null
+++ b/e2etests/testlocalapps/sample-badweb/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ /invalid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-default-auto-ids/pom.xml b/e2etests/testlocalapps/sample-default-auto-ids/pom.xml
new file mode 100644
index 000000000..2aab7c5cd
--- /dev/null
+++ b/e2etests/testlocalapps/sample-default-auto-ids/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-default-auto-ids
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-default-auto-ids
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..bd9c963ac
--- /dev/null
+++ b/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+ default
+
+
diff --git a/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/random.html b/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/random.html
new file mode 100644
index 000000000..259193564
--- /dev/null
+++ b/e2etests/testlocalapps/sample-default-auto-ids/src/main/webapp/random.html
@@ -0,0 +1,23 @@
+
+
+
+
+Nonsense
+
+This is just a test file.
+
+
diff --git a/e2etests/testlocalapps/sample-error-in-tag-file/pom.xml b/e2etests/testlocalapps/sample-error-in-tag-file/pom.xml
new file mode 100644
index 000000000..c6982601d
--- /dev/null
+++ b/e2etests/testlocalapps/sample-error-in-tag-file/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-error-in-tag-file
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-error-in-tag-file
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..7612ffc8d
--- /dev/null
+++ b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ sampleapp
+ 1
+ true
+ java8
+
diff --git a/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/nested/message.jsp b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/nested/message.jsp
new file mode 100644
index 000000000..9b4a070ab
--- /dev/null
+++ b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/nested/message.jsp
@@ -0,0 +1,23 @@
+
+
+<%@ page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/tags/ui/page.tag b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/tags/ui/page.tag
new file mode 100644
index 000000000..cd07ebf8c
--- /dev/null
+++ b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/tags/ui/page.tag
@@ -0,0 +1,25 @@
+
+
+
+<%@ tag description="App Engine Tag Testing" pageEncoding="UTF-8" %>
+<%@ attribute name="title" required="true" %>
+
+
+
App Engine rocks: ${title}
+
+
+
diff --git a/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..90e90bf12
--- /dev/null
+++ b/e2etests/testlocalapps/sample-error-in-tag-file/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-java11/pom.xml b/e2etests/testlocalapps/sample-java11/pom.xml
new file mode 100644
index 000000000..9bb21db6c
--- /dev/null
+++ b/e2etests/testlocalapps/sample-java11/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-java11
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-java11
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-java11/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-java11/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..cd9b60e0b
--- /dev/null
+++ b/e2etests/testlocalapps/sample-java11/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ true
+ java11
+ canary
+ java -jar foo.jar
+ true
+
diff --git a/e2etests/testlocalapps/sample-java11/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-java11/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..974d1619f
--- /dev/null
+++ b/e2etests/testlocalapps/sample-java11/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-java17/pom.xml b/e2etests/testlocalapps/sample-java17/pom.xml
new file mode 100644
index 000000000..c0355b3bf
--- /dev/null
+++ b/e2etests/testlocalapps/sample-java17/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-java17
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-java17
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-java17/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-java17/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..a6e4f36ae
--- /dev/null
+++ b/e2etests/testlocalapps/sample-java17/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ true
+ java17
+ canary
+ java -jar foo.jar
+ true
+
diff --git a/e2etests/testlocalapps/sample-java17/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-java17/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..974d1619f
--- /dev/null
+++ b/e2etests/testlocalapps/sample-java17/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-jsptaglibrary/pom.xml b/e2etests/testlocalapps/sample-jsptaglibrary/pom.xml
new file mode 100644
index 000000000..3c22d9c9c
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jsptaglibrary/pom.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-jsptaglibrary
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-jsptaglibrary
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..7612ffc8d
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ sampleapp
+ 1
+ true
+ java8
+
diff --git a/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/tags/ui/page.tag b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/tags/ui/page.tag
new file mode 100644
index 000000000..a75565901
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/tags/ui/page.tag
@@ -0,0 +1,26 @@
+
+
+
+<%@ tag description="App Engine Tag Testing" pageEncoding="UTF-8" %>
+<%@ attribute name="title" required="true" %>
+
+
+
App Engine rocks: ${title}
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..6762f0f23
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/nested/message.jsp b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/nested/message.jsp
new file mode 100644
index 000000000..b0076b353
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jsptaglibrary/src/main/webapp/nested/message.jsp
@@ -0,0 +1,23 @@
+
+
+<%@ page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-jspx/pom.xml b/e2etests/testlocalapps/sample-jspx/pom.xml
new file mode 100644
index 000000000..9bdf2062f
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jspx/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-jspx
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-jspx
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-jspx/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-jspx/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..c8005e891
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jspx/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-jspx/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-jspx/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jspx/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-jspx/src/main/webapp/nested/dukebanner.html b/e2etests/testlocalapps/sample-jspx/src/main/webapp/nested/dukebanner.html
new file mode 100644
index 000000000..154c4464e
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jspx/src/main/webapp/nested/dukebanner.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+ |
+ |
+
+
+
+ |
+
+I'm lazy, this should be an image.
+ |
+
+
+
diff --git a/e2etests/testlocalapps/sample-jspx/src/main/webapp/nested/testing.jspx b/e2etests/testlocalapps/sample-jspx/src/main/webapp/nested/testing.jspx
new file mode 100644
index 000000000..0d4317f07
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jspx/src/main/webapp/nested/testing.jspx
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+Hello, World
+
+
+
+
+
+
+ |
+ Hello, World!
+It is now System.currentTimeMillis()
+milliseconds past the epoch. |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-jspx/src/main/webapp/random.html b/e2etests/testlocalapps/sample-jspx/src/main/webapp/random.html
new file mode 100644
index 000000000..259193564
--- /dev/null
+++ b/e2etests/testlocalapps/sample-jspx/src/main/webapp/random.html
@@ -0,0 +1,23 @@
+
+
+
+
+Nonsense
+
+This is just a test file.
+
+
diff --git a/e2etests/testlocalapps/sample-legacy-auto-ids/pom.xml b/e2etests/testlocalapps/sample-legacy-auto-ids/pom.xml
new file mode 100644
index 000000000..86d2ca6ec
--- /dev/null
+++ b/e2etests/testlocalapps/sample-legacy-auto-ids/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-legacy-auto-ids
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-legacy-auto-ids
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..5bbb61e1e
--- /dev/null
+++ b/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ sampleapp
+ 1
+
+
+
+
+
+
+
+ true
+ legacy
+ java8
+
+
diff --git a/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/random.html b/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/random.html
new file mode 100644
index 000000000..259193564
--- /dev/null
+++ b/e2etests/testlocalapps/sample-legacy-auto-ids/src/main/webapp/random.html
@@ -0,0 +1,23 @@
+
+
+
+
+Nonsense
+
+This is just a test file.
+
+
diff --git a/e2etests/testlocalapps/sample-missingappid/pom.xml b/e2etests/testlocalapps/sample-missingappid/pom.xml
new file mode 100644
index 000000000..99273bc27
--- /dev/null
+++ b/e2etests/testlocalapps/sample-missingappid/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-missingappid
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-missingappid
+
+
+ UTF-8
+ true
+ UTF-8
+ 2.7.2
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-missingappid/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-missingappid/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..bce905528
--- /dev/null
+++ b/e2etests/testlocalapps/sample-missingappid/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ java8
+
+
diff --git a/e2etests/testlocalapps/sample-missingappid/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-missingappid/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sample-missingappid/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-nojsps/pom.xml b/e2etests/testlocalapps/sample-nojsps/pom.xml
new file mode 100644
index 000000000..4db77be8b
--- /dev/null
+++ b/e2etests/testlocalapps/sample-nojsps/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-nojsps
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-nojsps
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..c8005e891
--- /dev/null
+++ b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..8af78157b
--- /dev/null
+++ b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/dukebanner.html b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/dukebanner.html
new file mode 100644
index 000000000..c0758a5bb
--- /dev/null
+++ b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/dukebanner.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+ |
+ |
+
+
+
+ |
+
+I'm lazy, this should be an image.
+ |
+
+
+
diff --git a/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-nojsps/src/main/webapp/random.html b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/random.html
new file mode 100644
index 000000000..259193564
--- /dev/null
+++ b/e2etests/testlocalapps/sample-nojsps/src/main/webapp/random.html
@@ -0,0 +1,23 @@
+
+
+
+
+Nonsense
+
+This is just a test file.
+
+
diff --git a/e2etests/testlocalapps/sample-unspecified-auto-ids/pom.xml b/e2etests/testlocalapps/sample-unspecified-auto-ids/pom.xml
new file mode 100644
index 000000000..cf21905c5
--- /dev/null
+++ b/e2etests/testlocalapps/sample-unspecified-auto-ids/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-unspecified-auto-ids
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-unspecified-auto-ids
+
+
+ UTF-8
+ true
+ UTF-8
+ 2.7.2
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/random.html b/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/random.html
new file mode 100644
index 000000000..259193564
--- /dev/null
+++ b/e2etests/testlocalapps/sample-unspecified-auto-ids/src/main/webapp/random.html
@@ -0,0 +1,23 @@
+
+
+
+
+Nonsense
+
+This is just a test file.
+
+
diff --git a/e2etests/testlocalapps/sample-with-classes/pom.xml b/e2etests/testlocalapps/sample-with-classes/pom.xml
new file mode 100644
index 000000000..862e89f34
--- /dev/null
+++ b/e2etests/testlocalapps/sample-with-classes/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sample-with-classes
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sample-with-classes
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sample-with-classes/src/main/java/foo/AClass.java b/e2etests/testlocalapps/sample-with-classes/src/main/java/foo/AClass.java
new file mode 100644
index 000000000..ca9a9fa6f
--- /dev/null
+++ b/e2etests/testlocalapps/sample-with-classes/src/main/java/foo/AClass.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package foo;
+
+/**
+ *
+ */
+final class AClass {
+
+ private AClass() {
+ }
+}
diff --git a/e2etests/testlocalapps/sample-with-classes/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sample-with-classes/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/sample-with-classes/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/sample-with-classes/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sample-with-classes/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sample-with-classes/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-automatic-module/pom.xml b/e2etests/testlocalapps/sampleapp-automatic-module/pom.xml
new file mode 100644
index 000000000..d10f1ced0
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-automatic-module/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sampleapp-automatic-module
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sampleapp-automatic-module
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sampleapp-automatic-module/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sampleapp-automatic-module/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..1dba29c62
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-automatic-module/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ sampleapp
+ 1
+ stan
+ java8
+ F8
+
+ 10.5s
+ 10900ms
+ automatic
+ 10
+ 20
+
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-automatic-module/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sampleapp-automatic-module/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-automatic-module/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-backends/pom.xml b/e2etests/testlocalapps/sampleapp-backends/pom.xml
new file mode 100644
index 000000000..55e718e66
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-backends/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sampleapp-backends
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sampleapp-backends
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sampleapp-backends/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sampleapp-backends/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..c8005e891
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-backends/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-backends/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sampleapp-backends/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-backends/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-basic-module/pom.xml b/e2etests/testlocalapps/sampleapp-basic-module/pom.xml
new file mode 100644
index 000000000..21512eff8
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-basic-module/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sampleapp-basic-module
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sampleapp-basic-module
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sampleapp-basic-module/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sampleapp-basic-module/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..83e5ded73
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-basic-module/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+ stan
+ B8
+
+ 11
+ 10m
+
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-basic-module/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sampleapp-basic-module/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-basic-module/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-manual-module/pom.xml b/e2etests/testlocalapps/sampleapp-manual-module/pom.xml
new file mode 100644
index 000000000..7a6f36a84
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-manual-module/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sampleapp-manual-module
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sampleapp-manual-module
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sampleapp-manual-module/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sampleapp-manual-module/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..64906d057
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-manual-module/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,35 @@
+
+
+
+
+ sampleapp
+ 1
+ stan
+ B8
+ java8
+
+ 10
+
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-manual-module/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sampleapp-manual-module/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-manual-module/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-runtime/pom.xml b/e2etests/testlocalapps/sampleapp-runtime/pom.xml
new file mode 100644
index 000000000..70974bba4
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-runtime/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sampleapp-runtime
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sampleapp-runtime
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sampleapp-runtime/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sampleapp-runtime/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..b25e28c14
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-runtime/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ sampleapp
+ 1
+ foo-bar
+
+
+
+
+ true
+
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp-runtime/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sampleapp-runtime/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp-runtime/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp/pom.xml b/e2etests/testlocalapps/sampleapp/pom.xml
new file mode 100644
index 000000000..4d628d01e
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ sampleapp
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: sampleapp
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sampleapp/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/sampleapp/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..f0576a126
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ sampleapp
+ 1
+ java8
+
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/sampleapp/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/sampleapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp/src/main/webapp/foo.jsp b/e2etests/testlocalapps/sampleapp/src/main/webapp/foo.jsp
new file mode 100644
index 000000000..312e1c50f
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp/src/main/webapp/foo.jsp
@@ -0,0 +1,27 @@
+
+
+ <%@page contentType="text/html" pageEncoding="UTF-8"%>
+
+
+
+
+ JSP Page
+
+
+ Hello World!
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/sampleapp/src/main/webapp/nested/dukebanner.html b/e2etests/testlocalapps/sampleapp/src/main/webapp/nested/dukebanner.html
new file mode 100644
index 000000000..a2223c074
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp/src/main/webapp/nested/dukebanner.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+ |
+ |
+
+
+
+ |
+
+I'm lazy, this should be an image.
+ |
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp/src/main/webapp/nested/testing.jsp b/e2etests/testlocalapps/sampleapp/src/main/webapp/nested/testing.jsp
new file mode 100644
index 000000000..3a857cafb
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp/src/main/webapp/nested/testing.jsp
@@ -0,0 +1,34 @@
+
+
+<%@ page info="a hello world example" %>
+
+
+Hello, World
+
+
+<%@ include file="dukebanner.html" %>
+
+
+
+
+
+
diff --git a/e2etests/testlocalapps/sampleapp/src/main/webapp/random.html b/e2etests/testlocalapps/sampleapp/src/main/webapp/random.html
new file mode 100644
index 000000000..259193564
--- /dev/null
+++ b/e2etests/testlocalapps/sampleapp/src/main/webapp/random.html
@@ -0,0 +1,23 @@
+
+
+
+
+Nonsense
+
+This is just a test file.
+
+
diff --git a/e2etests/testlocalapps/stage-sampleapp/pom.xml b/e2etests/testlocalapps/stage-sampleapp/pom.xml
new file mode 100644
index 000000000..44c427c60
--- /dev/null
+++ b/e2etests/testlocalapps/stage-sampleapp/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ stage-sampleapp
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: stage-sampleapp
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/stage-sampleapp/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/stage-sampleapp/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..b2baac44f
--- /dev/null
+++ b/e2etests/testlocalapps/stage-sampleapp/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ java8
+
+
+
+
+
+
+ true
+
+
diff --git a/e2etests/testlocalapps/stage-sampleapp/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/stage-sampleapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..a8d770886
--- /dev/null
+++ b/e2etests/testlocalapps/stage-sampleapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/stage-with-staging-options/pom.xml b/e2etests/testlocalapps/stage-with-staging-options/pom.xml
new file mode 100644
index 000000000..c9c39b290
--- /dev/null
+++ b/e2etests/testlocalapps/stage-with-staging-options/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ stage-with-staging-options
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: stage-with-staging-options
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/stage-with-staging-options/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/stage-with-staging-options/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..519a7c332
--- /dev/null
+++ b/e2etests/testlocalapps/stage-with-staging-options/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+ true
+ java8
+
+
+ foo,bar
+ true
+ UTF-16
+
+
diff --git a/e2etests/testlocalapps/stage-with-staging-options/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/stage-with-staging-options/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..b7465d323
--- /dev/null
+++ b/e2etests/testlocalapps/stage-with-staging-options/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ valid
+
+
+
+ 300
+
+
+
diff --git a/e2etests/testlocalapps/xmlorder/pom.xml b/e2etests/testlocalapps/xmlorder/pom.xml
new file mode 100644
index 000000000..bc0d8186e
--- /dev/null
+++ b/e2etests/testlocalapps/xmlorder/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ com.google.appengine.demos
+ xmlorder
+ 0.0.2-SNAPSHOT
+ war
+
+ AppEngine :: xmlorder
+
+
+ UTF-8
+ true
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+
+ ludo-in-in
+
+ demo
+
+
+
+
+ maven-war-plugin
+ 3.3.1
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/appengine-web.xml b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..68f87c78a
--- /dev/null
+++ b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+ true
+ java8
+
+ 1
+ sampleapp
+
diff --git a/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/cron.xml b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 000000000..d087a5ebc
--- /dev/null
+++ b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ HST
+ every 2 hours
+ http://nowhere/fast
+
+
+ every 3 hours
+ http://nowhere
+ Why it's here
+
+
diff --git a/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/queue.xml b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/queue.xml
new file mode 100644
index 000000000..35b17fadf
--- /dev/null
+++ b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/queue.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+ a-name
+ 5/s
+ 3
+
+
+ b-name
+ 0
+
+
+ c-name
+ 10/m
+
+
+ d-name
+
+ 100
+
+ pull
+
+ prometheus-eng@googl.com
+
+
+
+ e-name
+ pull
+
+ prometheus-eng@googl.com
+ prometheus-eng@googl.com
+ prometheus-eng-2@googl.com
+ prometheus-eng-2@googl.com
+
+
+
diff --git a/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/web.xml b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..917096f25
--- /dev/null
+++ b/e2etests/testlocalapps/xmlorder/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ userapp
+
+ com.google.apphosting.securitycontest.sampleapp.SampleAppServlet
+
+
+
+
+ userapp
+ /
+
+
+
+ 300
+
+
+
diff --git a/external/geronimo_javamail/pom.xml b/external/geronimo_javamail/pom.xml
index e716ea7c1..9e16b6755 100644
--- a/external/geronimo_javamail/pom.xml
+++ b/external/geronimo_javamail/pom.xml
@@ -22,14 +22,14 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
../../pom.xml
geronimo-javamail_1.4_spec
jar
AppEngine :: JavaMail 1.4
- 1.4.4-${project.parent.version}
+ 1.4.4-3.0.19-SNAPSHOT
Javamail 1.4 Specification with AppEngine updates.
diff --git a/google3/third_party/java_src/appengine_standard/api_compatibility_tests/pom.xml b/google3/third_party/java_src/appengine_standard/api_compatibility_tests/pom.xml
index c8630c110..71b7274b9 100644
--- a/google3/third_party/java_src/appengine_standard/api_compatibility_tests/pom.xml
+++ b/google3/third_party/java_src/appengine_standard/api_compatibility_tests/pom.xml
@@ -22,7 +22,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.19-SNAPSHOT
jar
diff --git a/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/NoSerializeImmutableTest.java b/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/NoSerializeImmutableTest.java
index 4254dcb95..f3bc6ef19 100644
--- a/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/NoSerializeImmutableTest.java
+++ b/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/NoSerializeImmutableTest.java
@@ -86,7 +86,7 @@ public class NoSerializeImmutableTest {
public void serializableCollectionFieldsAreNotGuavaImmutable() throws Exception {
File appengineApiJar =
new File(
- "/tmp/check_build/appengine-api-1.0-sdk/target/appengine-api-1.0-sdk-2.0.17-SNAPSHOT.jar");
+ "/tmp/check_build/appengine-api-1.0-sdk/target/appengine-api-1.0-sdk-2.0.19-SNAPSHOT.jar");
assertThat(appengineApiJar.exists()).isTrue();
ClassLoader apiJarClassLoader = new URLClassLoader(new URL[] {appengineApiJar.toURI().toURL()});
Class> messageLite =
diff --git a/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/usage/ApiExhaustiveUsageTestCase.java b/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/usage/ApiExhaustiveUsageTestCase.java
index 0a522f7ef..055546196 100644
--- a/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/usage/ApiExhaustiveUsageTestCase.java
+++ b/google3/third_party/java_src/appengine_standard/api_compatibility_tests/src/test/java/com/google/appengine/apicompat/usage/ApiExhaustiveUsageTestCase.java
@@ -54,7 +54,7 @@ public abstract class ApiExhaustiveUsageTestCase {
/** The path to the sdk api jar. */
private static final String API_JAR_PATH =
- "/tmp/check_build/appengine-api-1.0-sdk/target/appengine-api-1.0-sdk-2.0.17-SNAPSHOT.jar";
+ "/tmp/check_build/appengine-api-1.0-sdk/target/appengine-api-1.0-sdk-2.0.19-SNAPSHOT.jar";
private boolean isExhaustiveUsageClass(String clsName) {
return clsName.startsWith("com.google.appengine.apicompat.usage");
diff --git a/lib/pom.xml b/lib/pom.xml
index 015b157af..dff76bcd2 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -22,7 +22,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
pom
diff --git a/lib/tools_api/pom.xml b/lib/tools_api/pom.xml
index 2451fa917..ed879dd28 100644
--- a/lib/tools_api/pom.xml
+++ b/lib/tools_api/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
lib-parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
@@ -268,7 +268,7 @@
- com.esotericsoftware.yamlbeans:yamlbeans
+ com.contrastsecurity:yamlbeans
com.google.appengine:appengine-apis
com.google.appengine:appengine-apis-dev
com.google.appengine:protos
diff --git a/lib/tools_api/src/test/java/com/google/appengine/tools/admin/AppAdminImplTest.java b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/AppAdminImplTest.java
new file mode 100644
index 000000000..d35399cb3
--- /dev/null
+++ b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/AppAdminImplTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.appengine.tools.admin;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.rules.TemporaryFolder;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Unit tests for {@link AppAdminImpl}. */
+@RunWith(JUnit4.class)
+public class AppAdminImplTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock private GenericApplication app;
+ @Mock private PrintWriter errorWriter;
+ @Mock private ApplicationProcessingOptions appOptions;
+
+ private AppAdminImpl appAdmin;
+
+ @Before
+ public void setUp() {
+ when(app.getAppId()).thenReturn("app1");
+ when(app.getModule()).thenReturn("module1");
+ when(app.getVersion()).thenReturn("v1");
+
+ appAdmin = new AppAdminImpl(app, errorWriter, appOptions);
+ }
+
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void testStageApplicationWithDefaultResourceLimits() throws Exception {
+ temporaryFolder.create();
+ File stagingDir = temporaryFolder.getRoot();
+ when(app.createStagingDirectory(any(ApplicationProcessingOptions.class), eq(stagingDir)))
+ .thenReturn(new File("a/b"));
+
+ appAdmin.stageApplicationWithDefaultResourceLimits(stagingDir);
+
+ verify(app).resetProgress();
+ verify(app).setDetailsWriter(any(PrintWriter.class));
+ }
+
+ @Test
+ public void testStageApplicationNonEmptyDir() throws IOException {
+ temporaryFolder.create();
+ File stagingDir = temporaryFolder.getRoot();
+ File newFile = new File(stagingDir, "test.file");
+
+ if (!newFile.createNewFile()) {
+ throw new IOException("Failed when setting up test - could not create file");
+ }
+
+ assertThrows(
+ stagingDir.getPath() + " is not a valid staging directory (it is not empty)",
+ AdminException.class,
+ () -> appAdmin.stageApplication(stagingDir, /* useRemoteResourceLimits= */ false));
+ }
+
+ @Test
+ public void testStageApplicationRegularFile() throws IOException {
+ temporaryFolder.create();
+ File stagingDir = temporaryFolder.getRoot();
+ File badStageDir = new File(stagingDir, "test.file");
+
+ if (!badStageDir.createNewFile()) {
+ throw new IOException("Failed when setting up test - could not create file");
+ }
+
+ assertThrows(
+ badStageDir.getPath() + " is not a valid staging directory (it is a regular file)",
+ AdminException.class,
+ () -> appAdmin.stageApplication(badStageDir, /* useRemoteResourceLimits= */ false));
+ }
+}
diff --git a/lib/tools_api/src/test/java/com/google/appengine/tools/admin/AppYamlTranslatorTest.java b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/AppYamlTranslatorTest.java
index 351bb5236..bd4f4a058 100644
--- a/lib/tools_api/src/test/java/com/google/appengine/tools/admin/AppYamlTranslatorTest.java
+++ b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/AppYamlTranslatorTest.java
@@ -2761,7 +2761,7 @@ public void testHttpHeaders() {
appEngineWebXml.includeStaticPattern("/my-static-files/*", null);
Map httpHeaders = staticFileInclude.getHttpHeaders();
httpHeaders.put("foo", "1");
- httpHeaders.put("bar", "2");
+ httpHeaders.put("bar", "barf");
AppYamlTranslator translator = createTranslator();
String yaml =
@@ -2778,8 +2778,8 @@ public void testHttpHeaders() {
+ " login: optional\n"
+ " secure: optional\n"
+ " http_headers:\n"
- + " foo: '1'\n"
- + " bar: '2'\n"
+ + " foo: 1\n"
+ + " bar: barf\n"
+ "- url: /\n"
+ " script: unused\n"
+ " login: optional\n"
@@ -2793,6 +2793,7 @@ public void testHttpHeaders() {
+ " login: optional\n"
+ " secure: optional\n";
assertEquals(yaml, translator.getYaml());
+
}
public void testBackends() {
diff --git a/lib/tools_api/src/test/java/com/google/appengine/tools/admin/XmlYamlIntegrationTest.java b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/XmlYamlIntegrationTest.java
new file mode 100644
index 000000000..52bdbe205
--- /dev/null
+++ b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/XmlYamlIntegrationTest.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.appengine.tools.admin;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.appengine.tools.info.Version;
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.apphosting.utils.config.AppEngineWebXml.ApiConfig;
+import com.google.apphosting.utils.config.AppEngineWebXmlReader;
+import com.google.apphosting.utils.config.BackendsXml;
+import com.google.apphosting.utils.config.WebXml;
+import com.google.apphosting.utils.config.WebXmlReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+
+/**
+ * An integration test that tests the relationship between {@link WebXmlReader}, {@link
+ * AppEngineWebXmlReader} and {@link AppYamlTranslator}. The first two classes read XML text
+ * corresponding to web.xml and appengine-web.xml and produce {@link WebXml} and {@link
+ * AppEngineWebXml} objects respectively. The second class takes these objects and produces an
+ * app.yaml in the Python App Engine style. We test that the output yaml expresses the same
+ * semantics as the input XML.
+ *
+ * This test is similar to {@code AppYamlTranslatorTest} except that that test builds {@link
+ * WebXml} and {@link AppEngineWebXml} object programmatically instead of by parsing text.
+ *
+ *
In this test we specify XML text to read and expected Yaml text to write and test that we
+ * generate the expected Yaml text.
+ *
+ * @author rudominer@google.com (Mitch Rudominer)
+ */
+public class XmlYamlIntegrationTest extends TestCase {
+
+ // @formatter:off
+ private static final String XML_HEADER = "\n";
+
+ private static final String WEB_XML_PREFIX =
+ XML_HEADER
+ + "\n"
+ + "\n"
+ + " \n"
+ + " Servlet1\n"
+ + " com.test.Servlet1\n"
+ + " ";
+ private static final String WEB_XML_SUFFIX = "";
+
+ private static final String AE_WEB_XML_PREFIX =
+ XML_HEADER
+ + "\n"
+ + " my_app_id\n"
+ + " 1\n"
+ + " true\n";
+ private static final String AE_WEB_XML_SUFFIX = "";
+
+ private static final String APP_YAML_PREFIX =
+ "application: 'my_app_id'\n"
+ + "runtime: java8\n"
+ + "version: '1'\n"
+ + "inbound_services:\n"
+ + "- warmup\n"
+ + "derived_file_type:\n"
+ + "- java_precompiled\n"
+ + "threadsafe: True\n"
+ + "auto_id_policy: default\n"
+ + "api_version: '1.7.3'\n"
+ + "handlers:\n";
+ private static final String APP_YAML_SUFFIX =
+ "- url: /.*/\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n"
+ + "- url: /_ah/.*\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n";
+ // @formatter:on
+
+ private static String buildWebXml(String content) {
+ return WEB_XML_PREFIX + content + WEB_XML_SUFFIX;
+ }
+
+ private static String buildAEWebXml(String content) {
+ return AE_WEB_XML_PREFIX + content + AE_WEB_XML_SUFFIX;
+ }
+
+ private static String buildYaml(String content, boolean useYamlSuffix) {
+ String suffix = (useYamlSuffix ? APP_YAML_SUFFIX : "");
+ return APP_YAML_PREFIX + content + suffix;
+ }
+
+ /**
+ * Tests that a security constraint with a url pattern of the form {@code /baz/*} will generate
+ * yaml containing a regex that also captures the path {@code /baz}.
+ *
+ * @throws Exception
+ */
+ public void testSecurityConstraintWildcard() throws Exception {
+ // @formatter:off
+ String webXmlString =
+ "\n"
+ + " Servlet1\n"
+ + " /main\n"
+ + " /main/submit\n"
+ + "\n"
+ + "\n"
+ + " \n"
+ + " any\n"
+ + " /main/*\n"
+ + " \n"
+ + " \n"
+ + " admin\n"
+ + " \n"
+ + " \n"
+ + " CONFIDENTIAL\n"
+ + " \n"
+ + "";
+ String appEngineWebXmlString = "";
+ String expectedExtraYaml =
+ "- url: /main/.*/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n"
+ + "- url: /main\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /main/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /main/submit\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n";
+ // @formatter:on
+ doTest(webXmlString, appEngineWebXmlString, expectedExtraYaml);
+ }
+
+ /**
+ * Following on the previous test, tests that if {@code /baz} is explicitly specified in a
+ * security constraint in addition to {@code /baz/*} then we will generate yaml containing only a
+ * single regex that also captures the path {@code /baz}.
+ *
+ * @throws Exception
+ */
+ public void testSecurityConstraintWildcard2() throws Exception {
+ // @formatter:off
+ String webXmlString =
+ "\n"
+ + " Servlet1\n"
+ + " /main\n"
+ + " /main/submit\n"
+ + "\n"
+ + "\n"
+ + " \n"
+ + " any\n"
+ + " /main\n"
+ + " /main/*\n"
+ + " \n"
+ + " \n"
+ + " admin\n"
+ + " \n"
+ + " \n"
+ + " CONFIDENTIAL\n"
+ + " \n"
+ + "";
+ String appEngineWebXmlString = "";
+ String expectedExtraYaml =
+ "- url: /main/.*/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n"
+ + "- url: /main\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /main/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /main/submit\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n";
+ // @formatter:on
+ doTest(webXmlString, appEngineWebXmlString, expectedExtraYaml);
+ }
+
+ /**
+ * In this test we supply the glob /* within a security constraint to make sure that is handled
+ * correctly.
+ *
+ * @throws Exception
+ */
+ public void testSecurityConstraintWildcard3() throws Exception {
+ // @formatter:off
+ String webXmlString =
+ "\n"
+ + " Servlet1\n"
+ + " /main\n"
+ + " /main/submit\n"
+ + "\n"
+ + "\n"
+ + " \n"
+ + " any\n"
+ + " /*\n"
+ + " \n"
+ + " \n"
+ + " admin\n"
+ + " \n"
+ + " \n"
+ + " CONFIDENTIAL\n"
+ + " \n"
+ + "";
+ String appEngineWebXmlString = "";
+ String expectedExtraYaml =
+ "- url: /\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /.*/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /_ah/.*\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /main/submit\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /main\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n";
+ // @formatter:on
+ doTest(webXmlString, appEngineWebXmlString, expectedExtraYaml, false);
+ }
+
+ /**
+ * In this test we specify a security constraint but there is no matching servlet path. The
+ * security constraint will still have an effect on the generated yaml because of the welcome
+ * files.
+ *
+ * @throws Exception
+ */
+ public void testSecurityConstraintWithoutMatch() throws Exception {
+ // @formatter:off
+ String webXmlString =
+ "\n"
+ + " \n"
+ + " any\n"
+ + " /main/*\n"
+ + " \n"
+ + " \n"
+ + " CONFIDENTIAL\n"
+ + " \n"
+ + "";
+ String appEngineWebXmlString = "";
+ String expectedExtraYaml =
+ "- url: /main/.*/\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: always\n"
+ + "- url: /\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n"
+ + "- url: /main/\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: always\n";
+ // @formatter:on
+ doTest(webXmlString, appEngineWebXmlString, expectedExtraYaml);
+ }
+
+ private void doTest(String webXmlString, String appEngineWebXmlString, String expectedExtraYaml) {
+ doTest(webXmlString, appEngineWebXmlString, expectedExtraYaml, true);
+ }
+
+ private void doTest(
+ String webXmlString,
+ String appEngineWebXmlString,
+ String expectedExtraYaml,
+ boolean useYamlSuffix) {
+ WebXml webXml = new WebXmlStringReader(buildWebXml(webXmlString)).readWebXml();
+ AppEngineWebXml aeWebXml =
+ new AEWebXmlStringReader(buildAEWebXml(appEngineWebXmlString)).readAppEngineWebXml();
+ Set staticFiles = new HashSet();
+ ApiConfig apiConfig = null;
+ BackendsXml backendsXml = new BackendsXml();
+ AppYamlTranslator translator =
+ new AppYamlTranslator(
+ aeWebXml,
+ webXml,
+ backendsXml,
+ "1.7.3",
+ staticFiles,
+ apiConfig,
+ "java8",
+ Version.UNKNOWN);
+ assertEquals(buildYaml(expectedExtraYaml, useYamlSuffix), translator.getYaml());
+ }
+
+ private class WebXmlStringReader extends WebXmlReader {
+ private String xml;
+
+ public WebXmlStringReader(String xml) {
+ super(".");
+ this.xml = xml;
+ }
+
+ @Override
+ protected InputStream getInputStream() {
+ return new ByteArrayInputStream(xml.getBytes(UTF_8));
+ }
+ }
+
+ private class AEWebXmlStringReader extends AppEngineWebXmlReader {
+ private final String xml;
+
+ public AEWebXmlStringReader(String xml) {
+ super(".");
+ this.xml = xml;
+ }
+
+ @Override
+ protected InputStream getInputStream() {
+ return new ByteArrayInputStream(xml.getBytes(UTF_8));
+ }
+ }
+}
diff --git a/lib/tools_api/src/test/java/com/google/appengine/tools/admin/YamlXmlIntegrationTest.java b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/YamlXmlIntegrationTest.java
new file mode 100644
index 000000000..ab14b407a
--- /dev/null
+++ b/lib/tools_api/src/test/java/com/google/appengine/tools/admin/YamlXmlIntegrationTest.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.appengine.tools.admin;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.appengine.tools.info.Version;
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.apphosting.utils.config.AppEngineWebXml.ApiConfig;
+import com.google.apphosting.utils.config.AppYaml;
+import com.google.apphosting.utils.config.BackendsXml;
+import com.google.apphosting.utils.config.WebXml;
+import com.google.apphosting.utils.config.WebXmlReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+
+/**
+ * An integration test that tests the relationship between {@link AppYaml} and {@link
+ * AppYamlTranslator}. The first class takes an app.yaml in the Java App Engine style and produces a
+ * web.xml. The second class takes a web.xml and produces an app.yaml in the Python App Engine
+ * style. We test that the output yaml expresses the same semantics as the input yaml.
+ *
+ * As of this writing we are primarily concerned with the translation of the security-related
+ * elements, "secure:" and "login:".
+ *
+ * @author rudominer@google.com (Mitch Rudominer)
+ */
+public class YamlXmlIntegrationTest extends TestCase {
+
+ private static final String YAML_INPUT_PREFIX = "application: app1\n" + "handlers:\n";
+
+ private static final String EXPECTED_YAML_OUTPUT_PREFIX =
+ "application: 'app1'\n"
+ + "runtime: java7\n"
+ + "version: 'ver1'\n"
+ + "auto_id_policy: default\n"
+ + "api_version: '1.0'\n"
+ + "handlers:\n";
+
+ private AppEngineWebXml appEngineWebXml;
+ private BackendsXml backendsXml;
+ private Set staticFiles;
+ private ApiConfig apiConfig;
+
+ @Override
+ public void setUp() throws Exception {
+ appEngineWebXml = new AppEngineWebXml();
+ appEngineWebXml.setAppId("app1");
+ appEngineWebXml.setMajorVersionId("ver1");
+ appEngineWebXml.setPrecompilationEnabled(false);
+ backendsXml = new BackendsXml();
+ staticFiles = new HashSet();
+ }
+
+ /** Tests the case that only the admin pages require admin login. */
+ public void testAdminLoginOnly() {
+ String yamlInput = YAML_INPUT_PREFIX + " - url: /admin/*\n" + " login: admin\n";
+
+ String expectedYamlOutput =
+ EXPECTED_YAML_OUTPUT_PREFIX
+ + "- url: /admin/.*/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: optional\n"
+ + "- url: /\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n"
+ + "- url: /admin/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: optional\n"
+ + "- url: /.*/\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n"
+ + "- url: /_ah/.*\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: optional\n";
+
+ doYamlXmlIntegrationTest(yamlInput, expectedYamlOutput);
+ }
+
+ /** Tests the case that all pages require login and the admin pages require admin login */
+ public void testLoginRequiredOnly() {
+ String yamlInput =
+ YAML_INPUT_PREFIX
+ + " - url: /*\n"
+ + " login: required\n"
+ + " - url: /admin/*\n"
+ + " login: admin\n";
+
+ String expectedYamlOutput =
+ EXPECTED_YAML_OUTPUT_PREFIX
+ + "- url: /admin/.*/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: optional\n"
+ + "- url: /\n"
+ + " script: unused\n"
+ + " login: required\n"
+ + " secure: optional\n"
+ + "- url: /admin/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: optional\n"
+ + "- url: /.*/\n"
+ + " script: unused\n"
+ + " login: required\n"
+ + " secure: optional\n"
+ + "- url: /_ah/.*\n"
+ + " script: unused\n"
+ + " login: required\n"
+ + " secure: optional\n";
+
+ doYamlXmlIntegrationTest(yamlInput, expectedYamlOutput);
+ }
+
+ /** Tests the case that https is required on all pages, but no login */
+ public void testSSLOnly() {
+ String yamlInput = YAML_INPUT_PREFIX + " - url: /*\n" + " secure: always\n";
+
+ String expectedYamlOutput =
+ EXPECTED_YAML_OUTPUT_PREFIX
+ + "- url: /\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: always\n"
+ + "- url: /.*/\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: always\n"
+ + "- url: /_ah/.*\n"
+ + " script: unused\n"
+ + " login: optional\n"
+ + " secure: always\n";
+
+ doYamlXmlIntegrationTest(yamlInput, expectedYamlOutput);
+ }
+
+ /**
+ * Tests the case that login is required on all pages, admin login on admin pages, and https is
+ * required on all pages.
+ */
+ public void testLoginAndSSL() {
+ String yamlInput =
+ YAML_INPUT_PREFIX
+ + " - url: /*\n"
+ + " login: required\n"
+ + " secure: always\n"
+ + " - url: /admin/*\n"
+ + " login: admin\n"
+ + " secure: always\n";
+
+ String expectedYamlOutput =
+ EXPECTED_YAML_OUTPUT_PREFIX
+ + "- url: /admin/.*/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /\n"
+ + " script: unused\n"
+ + " login: required\n"
+ + " secure: always\n"
+ + "- url: /admin/\n"
+ + " script: unused\n"
+ + " login: admin\n"
+ + " secure: always\n"
+ + "- url: /.*/\n"
+ + " script: unused\n"
+ + " login: required\n"
+ + " secure: always\n"
+ + "- url: /_ah/.*\n"
+ + " script: unused\n"
+ + " login: required\n"
+ + " secure: always\n";
+
+ doYamlXmlIntegrationTest(yamlInput, expectedYamlOutput);
+ }
+
+ private void doYamlXmlIntegrationTest(String yamlIn, String expectedYamlOut) {
+ // Use the AppYaml class to parse the input yaml string
+ AppYaml yaml = AppYaml.parse(String.format(yamlIn, "app1"));
+ // Use the AppYaml class to generate a WebXml
+ WebXml xml = getWebXml(yaml);
+ // Use AppYamlTranslator to generate an app.yaml from the WebXml
+ // and check the output.
+ AppYamlTranslator translator = createTranslator(xml);
+ assertEquals(expectedYamlOut, translator.getYaml());
+ }
+
+ private AppYamlTranslator createTranslator(WebXml webXml) {
+ return new AppYamlTranslator(
+ appEngineWebXml,
+ webXml,
+ backendsXml,
+ "1.0",
+ staticFiles,
+ apiConfig,
+ "java7",
+ Version.UNKNOWN);
+ }
+
+ private WebXml getWebXml(AppYaml yaml) {
+ return new WebXmlStringReader(getWebXmlContents(yaml)).readWebXml();
+ }
+
+ private String getWebXmlContents(AppYaml yaml) {
+ StringWriter out = new StringWriter();
+ yaml.generateWebXml(out);
+ return out.toString();
+ }
+
+ private class WebXmlStringReader extends WebXmlReader {
+ private String xml;
+
+ public WebXmlStringReader(String xml) {
+ super(".");
+ this.xml = xml;
+ }
+
+ @Override
+ protected InputStream getInputStream() {
+ return new ByteArrayInputStream(xml.getBytes(UTF_8));
+ }
+ }
+}
diff --git a/lib/xml_validator/pom.xml b/lib/xml_validator/pom.xml
index a715d1103..715b545af 100644
--- a/lib/xml_validator/pom.xml
+++ b/lib/xml_validator/pom.xml
@@ -22,7 +22,7 @@
com.google.appengine
lib-parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
AppEngine :: libxmlvalidator
diff --git a/lib/xml_validator_test/pom.xml b/lib/xml_validator_test/pom.xml
index c21274229..5f30b5266 100644
--- a/lib/xml_validator_test/pom.xml
+++ b/lib/xml_validator_test/pom.xml
@@ -22,7 +22,7 @@
com.google.appengine
lib-parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
AppEngine :: libxmlvalidator_test
diff --git a/local_runtime_shared/pom.xml b/local_runtime_shared/pom.xml
index 8c68a1a84..d57015273 100644
--- a/local_runtime_shared/pom.xml
+++ b/local_runtime_shared/pom.xml
@@ -21,7 +21,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
AppEngine :: appengine-local-runtime-shared
diff --git a/pom.xml b/pom.xml
index 115545820..bcd2c6777 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
4.0.0
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
pom
AppEngine :: Parent project
@@ -45,13 +45,14 @@
sdk_assembly
applications
appengine_testing_tests
+ e2etests
1.8
1.8
UTF-8
- 9.4.51.v20230217
+ 9.4.52.v20230823
https://oss.sonatype.org/content/repositories/google-snapshots/
sonatype-nexus-snapshots
https://oss.sonatype.org/service/local/staging/deploy/maven2/
@@ -386,7 +387,7 @@
com.google.cloud.datastore
datastore-v1-proto-client
- 2.16.3
+ 2.17.0
com.google.geometry
@@ -396,7 +397,7 @@
org.easymock
easymock
- 4.3
+ 5.2.0
com.google.appengine
@@ -433,23 +434,23 @@
com.google.auto.value
auto-value
- 1.10.2
+ 1.10.4
provided
com.google.auto.value
auto-value-annotations
- 1.10.2
+ 1.10.4
- com.esotericsoftware.yamlbeans
+ com.contrastsecurity
yamlbeans
- 1.11
+ 1.17
com.google.api.grpc
proto-google-cloud-datastore-v1
- 0.107.1
+ 0.108.0
com.google.api.grpc
@@ -546,7 +547,7 @@
org.apache.ant
ant
- 1.10.13
+ 1.10.14
org.apache.maven.plugin-tools
@@ -562,7 +563,7 @@
org.checkerframework
checker-qual
- 3.37.0
+ 3.38.0
provided
@@ -626,22 +627,63 @@
io.grpc
grpc-api
- 1.57.1
+ 1.57.2
io.grpc
grpc-stub
- 1.57.1
+ 1.57.2
io.grpc
grpc-protobuf
- 1.57.1
+ 1.57.2
io.grpc
grpc-netty
- 1.57.1
+ 1.57.2
+
+
+
+ io.netty
+ netty-buffer
+ 4.1.97.Final
+
+
+ io.netty
+ netty-codec
+ 4.1.97.Final
+
+
+ io.netty
+ netty-codec-http
+ 4.1.97.Final
+
+
+ io.netty
+ netty-codec-http2
+ 4.1.97.Final
+
+
+ io.netty
+ netty-common
+ 4.1.94.Final
+
+
+ io.netty
+ netty-handler
+ 4.1.97.Final
+
+
+ io.netty
+ netty-transport
+ 4.1.97.Final
+
+
+ io.netty
+ netty-transport-native-unix-common
+ 4.1.97.Final
org.apache.tomcat
@@ -710,7 +752,7 @@
com.google.cloud
google-cloud-logging
- 3.15.7
+ 3.15.8
@@ -719,7 +761,7 @@
org.apache.maven.plugins
maven-enforcer-plugin
- 3.3.0
+ 3.4.0
enforce-maven
@@ -812,7 +854,7 @@
org.codehaus.mojo
javacc-maven-plugin
- 2.6
+ 3.0.1
org.codehaus.mojo
diff --git a/protobuf/api/memcache_service.proto b/protobuf/api/memcache_service.proto
index 681778787..0f0a99454 100644
--- a/protobuf/api/memcache_service.proto
+++ b/protobuf/api/memcache_service.proto
@@ -33,6 +33,9 @@
// used as the top level namespace, and name_space is immediately below.
// If the name_space field is "", it is mapped to the app's root namespace.
+//
+// LINT: ALLOW_GROUPS
+
syntax = "proto2";
// Some generic_services option(s) added automatically.
@@ -102,6 +105,7 @@ message AppOverride {
// NB: Can't use 'reserved' in this file, it needs to work with proto1.
}
+// Next tag: 7
message MemcacheGetRequest {
repeated bytes key = 1;
optional string name_space = 2 [default = ""];
@@ -115,8 +119,29 @@ message MemcacheGetRequest {
optional bool for_cas = 4;
optional AppOverride override = 5;
+
+ // If set true, returned items will include timestamps and indicate whether
+ // the item is delete_locked or not. Peeking at items will update stats, but
+ // will not alter the eviction order of the item.
+ optional bool for_peek = 6; // default=FALSE
}
+// Timestamps relevant to a memcache item.
+// Next tag: 4
+message ItemTimestamps {
+ // Absolute expiration timestamp of the item. Unset if this item has no
+ // expiration timestamp.
+ optional int64 expiration_time_sec = 1;
+
+ // Absolute last accessed timestamp of the item.
+ optional int64 last_access_time_sec = 2;
+
+ // Absolute delete_time timestamp of the item. Unset if this item is not
+ // delete locked.
+ optional int64 delete_lock_time_sec = 3;
+}
+
+// Next tag: 10
message MemcacheGetResponse {
// One item will be returned for each HIT, i.e. where the value was found for
// the requested key. Order is not specified.
@@ -136,6 +161,12 @@ message MemcacheGetResponse {
// Relative to now. Unset if this item has no expiration timestamp.
optional int32 expires_in_seconds = 6;
+
+ // Item timestamps. Only returned if for_peek is set on the request.
+ optional ItemTimestamps timestamps = 8;
+
+ // Item is delete_locked. Only returned if for_peek is set on the request.
+ optional bool is_delete_locked = 9;
}
enum GetStatusCode {
@@ -167,6 +198,8 @@ message MemcacheGetResponse {
}
// One get_status will be returned for each requested key, in the same order
// that the requests were in.
+ // TODO(b/290098535) This can return the wrong status code when there's a mix
+ // of hit/miss.
repeated GetStatusCode get_status = 7;
}
diff --git a/protobuf/pom.xml b/protobuf/pom.xml
index db4bc4d95..306024c54 100644
--- a/protobuf/pom.xml
+++ b/protobuf/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
diff --git a/quickstartgenerator/pom.xml b/quickstartgenerator/pom.xml
index a3f437fc0..fcffd4bc4 100644
--- a/quickstartgenerator/pom.xml
+++ b/quickstartgenerator/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
diff --git a/remoteapi/pom.xml b/remoteapi/pom.xml
index 05ca7aed7..60b6e08b7 100644
--- a/remoteapi/pom.xml
+++ b/remoteapi/pom.xml
@@ -20,7 +20,7 @@
com.google.appengine
parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
AppEngine :: appengine-remote-api
diff --git a/runtime/annotationscanningwebapp/pom.xml b/runtime/annotationscanningwebapp/pom.xml
index d81f65230..9f4d1bd81 100644
--- a/runtime/annotationscanningwebapp/pom.xml
+++ b/runtime/annotationscanningwebapp/pom.xml
@@ -14,13 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
4.0.0
war
- 1.0
+ 1.1-SNAPSHOT
com.google.appengine.demos
annotationscanningwebapp
diff --git a/runtime/deployment/pom.xml b/runtime/deployment/pom.xml
index ab2efa471..f14b318d7 100644
--- a/runtime/deployment/pom.xml
+++ b/runtime/deployment/pom.xml
@@ -22,7 +22,7 @@
com.google.appengine
runtime-parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
pom
diff --git a/runtime/failinitfilterwebapp/pom.xml b/runtime/failinitfilterwebapp/pom.xml
index e6d09593d..5ec694ba6 100644
--- a/runtime/failinitfilterwebapp/pom.xml
+++ b/runtime/failinitfilterwebapp/pom.xml
@@ -14,13 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
4.0.0
war
- 1.0
+ 1.1-SNAPSHOT
com.google.appengine.demos
failinitfilterwebapp
diff --git a/runtime/impl/pom.xml b/runtime/impl/pom.xml
index 756679b07..1f6fd3768 100644
--- a/runtime/impl/pom.xml
+++ b/runtime/impl/pom.xml
@@ -23,7 +23,7 @@
com.google.appengine
runtime-parent
- 2.0.17-SNAPSHOT
+ 2.0.20-SNAPSHOT
jar
@@ -36,7 +36,7 @@
true