diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 0888445c4..68c5d945a 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -31,13 +31,15 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- java: [17, 21, 23]
- jdk: [temurin, liberica, zulu]
- exclude:
+ java: [17, 21, 25-ea]
+ jdk: [temurin]
+ include:
- java: 17
- jdk: liberica
- - java: 17
- jdk: zulu
+ maven_profile: ""
+ - java: 21
+ maven_profile: "-Pjdk21"
+ - java: 25-ea
+ maven_profile: "-Pjdk25"
fail-fast: false
runs-on: ${{ matrix.os }}
@@ -53,7 +55,15 @@ jobs:
java-version: ${{ matrix.java }}
cache: 'maven'
+ - name: Set JAVA_HOME for specific versions
+ run: |
+ if [[ "${{ matrix.java }}" == "21" ]]; then
+ echo "JAVA_HOME_21=$JAVA_HOME" >> $GITHUB_ENV
+ elif [[ "${{ matrix.java }}" == "25-ea" ]]; then
+ echo "JAVA_HOME_25=$JAVA_HOME" >> $GITHUB_ENV
+ fi
+
- name: Build with Maven
run: |
- ./mvnw clean install -B -q
+ ./mvnw clean install -B -q ${{ matrix.maven_profile }}
echo "done"
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index d58dfb70b..44f3cf2c1 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1,19 +1,2 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you 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
-#
-# http://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.
-wrapperVersion=3.3.2
distributionType=only-script
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
diff --git a/README.md b/README.md
index b71f6da78..6e7d075a1 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
[![Maven][maven-version-image]][maven-version-link]
[](https://github.com/GoogleCloudPlatform/appengine-java-standard/blob/main/CODE_OF_CONDUCT.md)
-# Google App Engine Standard Environment Source Code for Java 8, Java 11, Java 17, Java 21.
+# Google App Engine Standard Environment Source Code for Java 17, Java 21, Java 25.
This repository contains the Java Source Code for [Google App Engine
@@ -27,13 +27,13 @@ standard environment][ae-docs], the production runtime, the AppEngine APIs, and
## Prerequisites
-### Use a JDK8 environment, so it can build the Java8 GAE runtime.
+### Use a JDK17 environment, so it can build the Java17 GAE runtime.
-[jdk8](https://adoptium.net/), but using a JDK11 or JDK17 of JDK21 is also possible.
+[jdk8](https://adoptium.net/), but using a JDK21 or JDK25 is also possible.
-The shared code base is also used for GAE Java 11, Java 17 and Java 21 build and test targets, using GitHub actions:
+The shared code base is also used for GAE Java 17, Java 17 and Java 25 build and test targets, using GitHub actions:
-- [Java 17/21 Continuous Integration](https://github.com/GoogleCloudPlatform/appengine-java-standard/actions/workflows/maven.yml)
+- [Java 17/21/25 Continuous Integration](https://github.com/GoogleCloudPlatform/appengine-java-standard/actions/workflows/maven.yml)
## Releases
@@ -69,7 +69,7 @@ Source code for all public APIs for com.google.appengine.api.* packages.
com.google.appengineappengine-api-1.0-sdk
- 2.0.33
+ 2.0.39javax.servlet
@@ -89,7 +89,7 @@ Source code for all public APIs for com.google.appengine.api.* packages.
com.google.appengineappengine-api-1.0-sdk
- 2.0.33
+ 2.0.39jakarta.servlet
@@ -100,12 +100,33 @@ Source code for all public APIs for com.google.appengine.api.* packages.
...
```
-* Java 21 with Jakarta or javax appengine-web.xml
+* Maven Java 25 Alpha with Jakarta EE 11 support pom.xml (EE10 is not supported in Java25, EE11 is fully compatible with EE10)
+
+ ```
+ war
+ ...
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ 3.0.0-beta
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 6.1.0
+ provided
+
+ ...
+ ```
+
+
+* Java 21/25 with javax EE8 profile appengine-web.xml
```
- java21
+ java21 <-- or java25 alpha-->
true
+ 2.0.39
```
@@ -211,7 +243,7 @@ We moved `com.google.appengine.api.memcache.stdimpl` and its old dependency
com.google.appengineappengine-api-legacy.jar/artifactId>
- 2.0.33
+ 2.0.39
```
@@ -226,19 +258,19 @@ We moved `com.google.appengine.api.memcache.stdimpl` and its old dependency
com.google.appengineappengine-testing
- 2.0.33
+ 2.0.39testcom.google.appengineappengine-api-stubs
- 2.0.33
+ 2.0.39testcom.google.appengineappengine-tools-sdk
- 2.0.33
+ 2.0.39test
```
@@ -276,9 +308,9 @@ Source code for the App Engine production application server and utilities. It i
- [End-to-End tests](https://github.com/GoogleCloudPlatform/appengine-java-standard/tree/master/runtime/test)
- [Source Code for runtime utilities](https://github.com/GoogleCloudPlatform/appengine-java-standard/tree/master/runtime/util)
-## Default entrypoint used by Java11, Java17 and Java21
+## Default entrypoint used by Java17, Java21 and Java25
-The Java 11, Java 17 and 21 runtimes can benefit from extra user configuration when starting the JVM for web apps.
+The Java 17, Java 21 and 25 runtimes can benefit from extra user configuration when starting the JVM for web apps.
The default entrypoint used to boot the JVM is generated by App Engine Buildpacks.
Essentially, it is equivalent to define this entrypoint in the `appengine-web.xml` file. For example:
@@ -292,7 +324,7 @@ By default, we use `--add-opens java.base/java.lang=ALL-UNNAMED --add-opens jav
## Entry Point Features
-The entry point for the Java 11, Java 17, 21 runtimes can be customized with user-defined environment variables added in the `appengine-web.xml` configuration file.
+The entry point for the Java 17, Java 21, 25 runtimes can be customized with user-defined environment variables added in the `appengine-web.xml` configuration file.
The following table indicates the environment variables that can be used to enable/disable/configure features, and the default values if they are not set:
diff --git a/THIRD-PARTY.txt b/THIRD-PARTY.txt
index ca9dfa897..1febffad1 100644
--- a/THIRD-PARTY.txt
+++ b/THIRD-PARTY.txt
@@ -1,227 +1,701 @@
-
The repository contains 3rd-party code under the following licenses:
+ Apache 2.0 License
+
+ * Apache MINA Core (org.apache.mina:mina-core:2.2.3 - https://mina.apache.org/mina-core/)
+
+ Apache License, version 2.0
+
+ * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.4.3.Final - http://www.jboss.org)
+
Apache License, Version 2.0
- * Apache Ant Core (org.apache.ant:ant:1.10.12 - https://ant.apache.org/)
- * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.12 - https://ant.apache.org/)
- * Apache Commons Codec (commons-codec:commons-codec:1.15 - https://commons.apache.org/proper/commons-codec/)
+ * Apache Ant Core (org.apache.ant:ant:1.10.15 - https://ant.apache.org/)
+ * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.15 - https://ant.apache.org/)
+ * Apache Commons Codec (commons-codec:commons-codec:1.19.0 - https://commons.apache.org/proper/commons-codec/)
+ * Apache Commons Collections (org.apache.commons:commons-collections4:4.4 - https://commons.apache.org/proper/commons-collections/)
+ * Apache Commons Lang (org.apache.commons:commons-lang3:3.13.0 - https://commons.apache.org/proper/commons-lang/)
* Apache Commons Logging (commons-logging:commons-logging:1.2 - http://commons.apache.org/proper/commons-logging/)
- * Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.13 - http://hc.apache.org/httpcomponents-client)
- * Apache HttpClient Mime (org.apache.httpcomponents:httpmime:4.5.13 - http://hc.apache.org/httpcomponents-client)
- * Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.15 - http://hc.apache.org/httpcomponents-core-ga)
- * Apache HttpCore NIO (org.apache.httpcomponents:httpcore-nio:4.4.15 - http://hc.apache.org/httpcomponents-core-ga)
- * Apache HTTP transport v2 for the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-apache-v2:1.42.1 - https://github.com/googleapis/google-http-java-client/google-http-client-apache-v2)
+ * Apache Directory API ASN.1 API (org.apache.directory.api:api-asn1-api:2.1.5 - https://directory.apache.org/api-parent/api-asn1-parent/api-asn1-api/)
+ * Apache Directory API ASN.1 BER (org.apache.directory.api:api-asn1-ber:2.1.5 - https://directory.apache.org/api-parent/api-asn1-parent/api-asn1-ber/)
+ * Apache Directory LDAP API I18n (org.apache.directory.api:api-i18n:2.1.5 - https://directory.apache.org/api-parent/api-i18n/)
+ * Apache Directory LDAP API Model (org.apache.directory.api:api-ldap-model:2.1.5 - https://directory.apache.org/api-parent/api-ldap-parent/api-ldap-model/)
+ * Apache Directory LDAP API Utilities (org.apache.directory.api:api-util:2.1.5 - https://directory.apache.org/api-parent/api-util/)
+ * Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.14 - http://hc.apache.org/httpcomponents-client-ga)
+ * Apache HttpClient Mime (org.apache.httpcomponents:httpmime:4.5.14 - http://hc.apache.org/httpcomponents-client-ga)
+ * Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga)
+ * Apache HttpCore NIO (org.apache.httpcomponents:httpcore-nio:4.4.16 - http://hc.apache.org/httpcomponents-core-ga)
+ * Apache HTTP transport v2 for the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-apache-v2:2.0.0 - https://github.com/googleapis/google-http-java-client/google-http-client-apache-v2)
* Apache Log4j API (org.apache.logging.log4j:log4j-api:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-api/)
* Apache Log4j to SLF4J Adapter (org.apache.logging.log4j:log4j-to-slf4j:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-to-slf4j/)
- * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.4.8 - https://urielch.github.io/)
+ * Apache Lucene (module: common) (org.apache.lucene:lucene-analysis-common:9.11.1 - https://lucene.apache.org/)
+ * Apache Lucene (module: facet) (org.apache.lucene:lucene-facet:9.11.1 - https://lucene.apache.org/)
+ * Apache Lucene (module: highlighter) (org.apache.lucene:lucene-highlighter:9.11.1 - https://lucene.apache.org/)
+ * Apache Lucene (module: join) (org.apache.lucene:lucene-join:9.11.1 - https://lucene.apache.org/)
+ * Apache Lucene (module: memory) (org.apache.lucene:lucene-memory:9.11.1 - https://lucene.apache.org/)
+ * Apache Lucene (module: queries) (org.apache.lucene:lucene-queries:9.11.1 - https://lucene.apache.org/)
+ * Apache Lucene (module: queryparser) (org.apache.lucene:lucene-queryparser:9.11.1 - https://lucene.apache.org/)
+ * Apache Lucene (module: sandbox) (org.apache.lucene:lucene-sandbox:9.11.1 - https://lucene.apache.org/)
+ * Apache ServiceMix :: Bundles :: antlr (org.apache.servicemix.bundles:org.apache.servicemix.bundles.antlr:2.7.7_5 - http://servicemix.apache.org/bundles-pom/org.apache.servicemix.bundles.antlr/)
+ * Apache Standard Taglib Implementation (org.apache.taglibs:taglibs-standard-impl:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-impl)
+ * Apache Standard Taglib Specification API (org.apache.taglibs:taglibs-standard-spec:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-spec)
+ * Arrow Format (org.apache.arrow:arrow-format:17.0.0 - https://arrow.apache.org/arrow-format/)
+ * Arrow Memory - Core (org.apache.arrow:arrow-memory-core:17.0.0 - https://arrow.apache.org/arrow-memory/arrow-memory-core/)
+ * Arrow Memory - Netty (org.apache.arrow:arrow-memory-netty:17.0.0 - https://arrow.apache.org/arrow-memory/arrow-memory-netty/)
+ * Arrow Memory - Netty Buffer (org.apache.arrow:arrow-memory-netty-buffer-patch:17.0.0 - https://arrow.apache.org/arrow-memory/arrow-memory-netty-buffer-patch/)
+ * Arrow Vectors (org.apache.arrow:arrow-vector:17.0.0 - https://arrow.apache.org/arrow-vector/)
+ * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.4.11 - https://urielch.github.io/)
* AssertJ fluent assertions (org.assertj:assertj-core:3.22.0 - https://assertj.github.io/doc/assertj-core/)
- * Auto Common Libraries (com.google.auto:auto-common:1.2 - https://github.com/google/auto/tree/master/common)
- * AutoService (com.google.auto.service:auto-service-annotations:1.0.1 - https://github.com/google/auto/tree/master/service)
- * AutoService Processor (com.google.auto.service:auto-service:1.0.1 - https://github.com/google/auto/tree/master/service)
- * AutoValue Annotations (com.google.auto.value:auto-value-annotations:1.9 - https://github.com/google/auto/tree/master/value)
- * AutoValue Processor (com.google.auto.value:auto-value:1.9 - https://github.com/google/auto/tree/master/value)
- * BigQuery (com.google.cloud:google-cloud-bigquery:2.10.10 - https://github.com/googleapis/java-bigquery)
- * BigQuery API v2-rev20220326-1.32.1 (com.google.apis:google-api-services-bigquery:v2-rev20220326-1.32.1 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-bigquery)
- * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.12.12 - https://bytebuddy.net/byte-buddy)
- * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.12.13 - https://bytebuddy.net/byte-buddy)
- * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.12.12 - https://bytebuddy.net/byte-buddy-agent)
- * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.12.13 - https://bytebuddy.net/byte-buddy-agent)
+ * Auto Common Libraries (com.google.auto:auto-common:1.2.1 - https://github.com/google/auto/tree/master/common)
+ * AutoService (com.google.auto.service:auto-service-annotations:1.1.1 - https://github.com/google/auto/tree/main/service)
+ * AutoService Processor (com.google.auto.service:auto-service:1.1.1 - https://github.com/google/auto/tree/main/service)
+ * AutoValue Annotations (com.google.auto.value:auto-value-annotations:1.11.0 - https://github.com/google/auto/tree/main/value)
+ * AutoValue Processor (com.google.auto.value:auto-value:1.11.0 - https://github.com/google/auto/tree/main/value)
+ * Awaitility (org.awaitility:awaitility:4.3.0 - http://awaitility.org)
+ * BigQuery (com.google.cloud:google-cloud-bigquery:2.54.2 - https://github.com/googleapis/java-bigquery)
+ * BigQuery API v2-rev20250706-2.0.0 (com.google.apis:google-api-services-bigquery:v2-rev20250706-2.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-bigquery)
+ * BigQuery Storage (com.google.cloud:google-cloud-bigquerystorage:3.16.3 - https://github.com/googleapis/java-bigquerystorage)
+ * brotli4j (com.aayushatharva.brotli4j:brotli4j:1.17.0 - https://github.com/hyperxpro/Brotli4j/brotli4j)
+ * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.12.23 - https://bytebuddy.net/byte-buddy)
+ * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.17.5 - https://bytebuddy.net/byte-buddy)
+ * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.17.6 - https://bytebuddy.net/byte-buddy)
+ * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.12.23 - https://bytebuddy.net/byte-buddy-agent)
+ * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.17.6 - https://bytebuddy.net/byte-buddy-agent)
+ * Caffeine cache (com.github.ben-manes.caffeine:caffeine:2.9.3 - https://github.com/ben-manes/caffeine)
+ * Caffeine cache (com.github.ben-manes.caffeine:caffeine:3.2.0 - https://github.com/ben-manes/caffeine)
+ * CDI APIs (jakarta.enterprise:jakarta.enterprise.cdi-api:4.0.1 - http://cdi-spec.org)
+ * CDI APIs (jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0 - http://cdi-spec.org)
+ * CDI Language Model (jakarta.enterprise:jakarta.enterprise.lang-model:4.0.1 - https://projects.eclipse.org/projects/ee4j/jakarta.enterprise.cdi-parent/jakarta.enterprise.lang-model)
+ * CDI Language Model (jakarta.enterprise:jakarta.enterprise.lang-model:4.1.0 - https://projects.eclipse.org/projects/ee4j/jakarta.enterprise.cdi-parent/jakarta.enterprise.lang-model)
* Cloud SQL Admin API v1beta4-rev20220310-1.32.1 (com.google.apis:google-api-services-sqladmin:v1beta4-rev20220310-1.32.1 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-sqladmin)
* Cloud SQL Core Socket Factory (Core Library, don't depend on this directly) (com.google.cloud.sql:jdbc-socket-factory-core:1.5.0 - https://github.com/GoogleCloudPlatform/cloud-sql-mysql-socket-factory/jdbc-socket-factory-core)
* Cloud SQL MySQL Socket Factory (for Connector/J 5.x) (com.google.cloud.sql:mysql-socket-factory:1.5.0 - https://github.com/GoogleCloudPlatform/cloud-sql-mysql-socket-factory/mysql-socket-factory)
- * Cloud Storage JSON API v1-rev20220401-1.32.1 (com.google.apis:google-api-services-storage:v1-rev20220401-1.32.1 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-storage)
- * datastore-v1-proto-client (com.google.cloud.datastore:datastore-v1-proto-client:2.10.1 - https://github.com/googleapis/java-datastore/datastore-v1-proto-client)
- * EasyMock (org.easymock:easymock:4.3 - http://easymock.org/easymock)
- * error-prone annotations (com.google.errorprone:error_prone_annotations:2.15.0 - https://errorprone.info/error_prone_annotations)
+ * Cloud Storage JSON API v1-rev20250815-2.0.0 (com.google.apis:google-api-services-storage:v1-rev20250815-2.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-storage)
+ * datastore-v1-proto-client (com.google.cloud.datastore:datastore-v1-proto-client:2.18.2 - https://github.com/googleapis/java-datastore/datastore-v1-proto-client)
+ * EasyMock (org.easymock:easymock:5.6.0 - http://easymock.org/easymock)
+ * error-prone annotations (com.google.errorprone:error_prone_annotations:2.41.0 - https://errorprone.info/error_prone_annotations)
* FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/)
- * Flogger (com.google.flogger:flogger:0.7.4 - https://github.com/google/flogger)
- * Flogger System Backend (com.google.flogger:flogger-system-backend:0.7.4 - https://github.com/google/flogger)
+ * Flogger (com.google.flogger:flogger:0.9 - https://github.com/google/flogger)
+ * Flogger System Backend (com.google.flogger:flogger-system-backend:0.9 - https://github.com/google/flogger)
+ * gapic-google-cloud-storage-v2 (com.google.api.grpc:gapic-google-cloud-storage-v2:2.57.0 - https://github.com/googleapis/java-storage/gapic-google-cloud-storage-v2)
* Google Android Annotations Library (com.google.android:annotations:4.1.1.4 - http://source.android.com/)
- * Google APIs Client Library for Java (com.google.api-client:google-api-client:2.0.0 - https://github.com/googleapis/google-api-java-client/google-api-client)
- * Google App Engine extensions to the Google API Client Library for Java. (com.google.api-client:google-api-client-appengine:2.0.0 - https://github.com/googleapis/google-api-java-client/google-api-client-appengine)
- * Google App Engine extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-appengine:1.42.2 - https://github.com/googleapis/google-http-java-client/google-http-client-appengine)
- * Google Cloud Core (com.google.cloud:google-cloud-core:2.6.1 - https://github.com/googleapis/java-core)
- * Google Cloud Core gRPC (com.google.cloud:google-cloud-core-grpc:2.3.3 - https://github.com/googleapis/java-core)
- * Google Cloud Core HTTP (com.google.cloud:google-cloud-core-http:2.6.0 - https://github.com/googleapis/java-core)
- * Google Cloud Datastore (com.google.cloud:google-cloud-datastore:2.4.0 - https://github.com/googleapis/java-datastore)
- * Google Cloud Logging (com.google.cloud:google-cloud-logging:3.7.5 - https://github.com/googleapis/java-logging)
- * Google Cloud Spanner (com.google.cloud:google-cloud-spanner:6.17.3 - https://github.com/googleapis/java-spanner)
- * Google Cloud Storage (com.google.cloud:google-cloud-storage:2.6.1 - https://github.com/googleapis/java-storage)
- * Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.42.2 - https://github.com/googleapis/google-http-java-client/google-http-client)
- * Google Logger (com.google.flogger:google-extensions:0.7.4 - https://github.com/google/flogger)
- * Google OAuth Client Library for Java (com.google.oauth-client:google-oauth-client:1.34.1 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client)
- * gRPC extension library for Google Cloud Platform (com.google.cloud:grpc-gcp:1.1.0 - https://github.com/GoogleCloudPlatform/grpc-gcp-java/tree/master/grpc-gcp)
- * grpc-google-cloud-spanner-admin-database-v1 (com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1:6.17.3 - https://github.com/googleapis/java-spanner/grpc-google-cloud-spanner-admin-database-v1)
- * grpc-google-cloud-spanner-admin-instance-v1 (com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1:6.17.3 - https://github.com/googleapis/java-spanner/grpc-google-cloud-spanner-admin-instance-v1)
- * grpc-google-cloud-spanner-v1 (com.google.api.grpc:grpc-google-cloud-spanner-v1:6.17.3 - https://github.com/googleapis/java-spanner/grpc-google-cloud-spanner-v1)
- * grpc-google-common-protos (com.google.api.grpc:grpc-google-common-protos:2.7.0 - https://github.com/googleapis/java-iam/grpc-google-common-protos)
- * Gson (com.google.code.gson:gson:2.9.0 - https://github.com/google/gson/gson)
- * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.40.1 - https://github.com/googleapis/google-http-java-client/google-http-client-gson)
- * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.42.0 - https://github.com/googleapis/google-http-java-client/google-http-client-gson)
- * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.42.1 - https://github.com/googleapis/google-http-java-client/google-http-client-gson)
- * Guava: Google Core Libraries for Java (com.google.guava:guava:31.1-jre - https://github.com/google/guava)
- * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess)
+ * Google APIs Client Library for Java (com.google.api-client:google-api-client:2.8.1 - https://github.com/googleapis/google-api-java-client/google-api-client)
+ * Google App Engine extensions to the Google API Client Library for Java. (com.google.api-client:google-api-client-appengine:2.8.1 - https://github.com/googleapis/google-api-java-client/google-api-client-appengine)
+ * Google App Engine extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-appengine:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client-appengine)
+ * Google Cloud Core (com.google.cloud:google-cloud-core:2.48.0 - https://github.com/googleapis/sdk-platform-java)
+ * Google Cloud Core (com.google.cloud:google-cloud-core:2.60.1 - https://github.com/googleapis/sdk-platform-java)
+ * Google Cloud Core gRPC (com.google.cloud:google-cloud-core-grpc:2.60.0 - https://github.com/googleapis/sdk-platform-java)
+ * Google Cloud Core HTTP (com.google.cloud:google-cloud-core-http:2.48.0 - https://github.com/googleapis/sdk-platform-java)
+ * Google Cloud Core HTTP (com.google.cloud:google-cloud-core-http:2.60.0 - https://github.com/googleapis/sdk-platform-java)
+ * Google Cloud Datastore (com.google.cloud:google-cloud-datastore:2.24.3 - https://github.com/googleapis/java-datastore)
+ * Google Cloud Datastore (com.google.cloud:google-cloud-datastore:2.31.4 - https://github.com/googleapis/java-datastore)
+ * Google Cloud Logging (com.google.cloud:google-cloud-logging:3.23.3 - https://github.com/googleapis/java-logging)
+ * Google Cloud Monitoring (com.google.cloud:google-cloud-monitoring:3.63.0 - https://github.com/googleapis/google-cloud-java)
+ * Google Cloud Spanner (com.google.cloud:google-cloud-spanner:6.99.0 - https://github.com/googleapis/java-spanner)
+ * Google Cloud Storage (com.google.cloud:google-cloud-storage:2.57.0 - https://github.com/googleapis/java-storage)
+ * Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client)
+ * Google Logger (com.google.flogger:google-extensions:0.9 - https://github.com/google/flogger)
+ * Google OAuth Client Library for Java (com.google.oauth-client:google-oauth-client:1.39.0 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client)
+ * gRPC extension library for Google Cloud Platform (com.google.cloud:grpc-gcp:1.6.1 - https://github.com/GoogleCloudPlatform/grpc-gcp-java/tree/master/grpc-gcp)
+ * grpc-google-cloud-bigquerystorage-v1 (com.google.api.grpc:grpc-google-cloud-bigquerystorage-v1:3.16.3 - https://github.com/googleapis/java-bigquerystorage/grpc-google-cloud-bigquerystorage-v1)
+ * grpc-google-cloud-bigquerystorage-v1beta1 (com.google.api.grpc:grpc-google-cloud-bigquerystorage-v1beta1:0.188.3 - https://github.com/googleapis/java-bigquerystorage/grpc-google-cloud-bigquerystorage-v1beta1)
+ * grpc-google-cloud-bigquerystorage-v1beta2 (com.google.api.grpc:grpc-google-cloud-bigquerystorage-v1beta2:0.188.3 - https://github.com/googleapis/java-bigquerystorage/grpc-google-cloud-bigquerystorage-v1beta2)
+ * grpc-google-cloud-datastore-admin-v1 (com.google.api.grpc:grpc-google-cloud-datastore-admin-v1:2.24.3 - https://github.com/googleapis/java-datastore/grpc-google-cloud-datastore-admin-v1)
+ * grpc-google-cloud-datastore-admin-v1 (com.google.api.grpc:grpc-google-cloud-datastore-admin-v1:2.31.4 - https://github.com/googleapis/java-datastore/grpc-google-cloud-datastore-admin-v1)
+ * grpc-google-cloud-datastore-v1 (com.google.api.grpc:grpc-google-cloud-datastore-v1:2.31.4 - https://github.com/googleapis/java-datastore/grpc-google-cloud-datastore-v1)
+ * grpc-google-cloud-spanner-admin-database-v1 (com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1:6.99.0 - https://github.com/googleapis/java-spanner/grpc-google-cloud-spanner-admin-database-v1)
+ * grpc-google-cloud-spanner-admin-instance-v1 (com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1:6.99.0 - https://github.com/googleapis/java-spanner/grpc-google-cloud-spanner-admin-instance-v1)
+ * grpc-google-cloud-spanner-v1 (com.google.api.grpc:grpc-google-cloud-spanner-v1:6.99.0 - https://github.com/googleapis/java-spanner/grpc-google-cloud-spanner-v1)
+ * grpc-google-cloud-storage-v2 (com.google.api.grpc:grpc-google-cloud-storage-v2:2.57.0 - https://github.com/googleapis/java-storage/grpc-google-cloud-storage-v2)
+ * grpc-google-common-protos (com.google.api.grpc:grpc-google-common-protos:2.61.0 - https://github.com/googleapis/sdk-platform-java)
+ * Gson (com.google.code.gson:gson:2.13.2 - https://github.com/google/gson)
+ * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.43.3 - https://github.com/googleapis/google-http-java-client/google-http-client-gson)
+ * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client-gson)
+ * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:2.0.0 - https://github.com/googleapis/google-http-java-client/google-http-client-gson)
+ * Guava: Google Core Libraries for Java (com.google.guava:guava:33.4.8-jre - https://github.com/google/guava)
+ * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.2 - https://github.com/google/guava/failureaccess)
+ * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.3 - https://github.com/google/guava/failureaccess)
* Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture)
- * Guava Testing Library (com.google.guava:guava-testlib:31.1-jre - https://github.com/google/guava/guava-testlib)
- * io.grpc:grpc-alts (io.grpc:grpc-alts:1.45.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-api (io.grpc:grpc-api:1.49.0 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-auth (io.grpc:grpc-auth:1.42.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-context (io.grpc:grpc-context:1.27.2 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-context (io.grpc:grpc-context:1.42.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-context (io.grpc:grpc-context:1.49.0 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-core (io.grpc:grpc-core:1.45.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-core (io.grpc:grpc-core:1.49.0 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-googleapis (io.grpc:grpc-googleapis:1.45.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-grpclb (io.grpc:grpc-grpclb:1.42.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-netty (io.grpc:grpc-netty:1.49.0 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-netty-shaded (io.grpc:grpc-netty-shaded:1.45.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-protobuf (io.grpc:grpc-protobuf:1.49.0 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-protobuf-lite (io.grpc:grpc-protobuf-lite:1.42.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-protobuf-lite (io.grpc:grpc-protobuf-lite:1.49.0 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-services (io.grpc:grpc-services:1.42.1 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-stub (io.grpc:grpc-stub:1.49.0 - https://github.com/grpc/grpc-java)
- * io.grpc:grpc-xds (io.grpc:grpc-xds:1.45.1 - https://github.com/grpc/grpc-java)
- * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/)
+ * Guava Testing Library (com.google.guava:guava-testlib:33.4.8-jre - https://github.com/google/guava/guava-testlib)
+ * hazelcast (com.hazelcast:hazelcast:3.12.12 - http://www.hazelcast.com/hazelcast/)
+ * hazelcast-client (com.hazelcast:hazelcast-client:3.12.12 - http://www.hazelcast.com/hazelcast-client/)
+ * Hibernate Commons Annotations (org.hibernate.common:hibernate-commons-annotations:7.0.3.Final - http://hibernate.org)
+ * Hibernate Search 5 Migration Helper - Engine (org.hibernate.search:hibernate-search-v5migrationhelper-engine:7.2.3.Final - https://hibernate.org/search/)
+ * Hibernate Search Backend - Lucene (org.hibernate.search:hibernate-search-backend-lucene:7.2.3.Final - https://hibernate.org/search/)
+ * Hibernate Search Engine (org.hibernate.search:hibernate-search-engine:7.2.4.Final - https://hibernate.org/search/)
+ * Hibernate Search Mapper - POJO Base (org.hibernate.search:hibernate-search-mapper-pojo-base:7.2.4.Final - https://hibernate.org/search/)
+ * Hibernate Search Utils - Common (org.hibernate.search:hibernate-search-util-common:7.2.4.Final - https://hibernate.org/search/)
+ * High Performance Primitive Collections (com.carrotsearch:hppc:0.10.0 - https://github.com/carrotsearch/hppc)
+ * Infinispan API (org.infinispan:infinispan-api:15.2.4.Final - https://infinispan.org/infinispan-api)
+ * Infinispan Commons (org.infinispan:infinispan-commons:15.2.4.Final - https://infinispan.org/infinispan-commons-parent/infinispan-commons)
+ * Infinispan Commons SPI (org.infinispan:infinispan-commons-spi:15.2.4.Final - https://infinispan.org/infinispan-commons-parent/infinispan-commons-spi)
+ * Infinispan Core (org.infinispan:infinispan-core:15.2.4.Final - https://infinispan.org/infinispan-core)
+ * Infinispan Counter API (org.infinispan:infinispan-counter-api:15.2.4.Final - https://infinispan.org/infinispan-counter-api)
+ * Infinispan Hot Rod Client (org.infinispan:infinispan-client-hotrod:15.2.4.Final - https://infinispan.org/infinispan-client-hotrod)
+ * Infinispan Object Querying and Filtering API (org.infinispan:infinispan-objectfilter:15.2.4.Final - https://infinispan.org/infinispan-objectfilter)
+ * Infinispan Query (org.infinispan:infinispan-query:15.2.4.Final - https://infinispan.org/infinispan-query)
+ * Infinispan Query-core (org.infinispan:infinispan-query-core:15.2.4.Final - https://infinispan.org/infinispan-query-core)
+ * Infinispan Query DSL API (org.infinispan:infinispan-query-dsl:15.2.4.Final - https://infinispan.org/infinispan-query-dsl)
+ * Infinispan Remote Query Client (org.infinispan:infinispan-remote-query-client:15.2.4.Final - https://infinispan.org/infinispan-remote-query-client)
+ * io.grpc:grpc-alts (io.grpc:grpc-alts:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-alts (io.grpc:grpc-alts:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-api (io.grpc:grpc-api:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-api (io.grpc:grpc-api:1.70.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-api (io.grpc:grpc-api:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-api (io.grpc:grpc-api:1.75.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-auth (io.grpc:grpc-auth:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-auth (io.grpc:grpc-auth:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-context (io.grpc:grpc-context:1.70.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-context (io.grpc:grpc-context:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-core (io.grpc:grpc-core:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-core (io.grpc:grpc-core:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-googleapis (io.grpc:grpc-googleapis:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-googleapis (io.grpc:grpc-googleapis:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-grpclb (io.grpc:grpc-grpclb:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-grpclb (io.grpc:grpc-grpclb:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-inprocess (io.grpc:grpc-inprocess:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-inprocess (io.grpc:grpc-inprocess:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-netty-shaded (io.grpc:grpc-netty-shaded:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-netty-shaded (io.grpc:grpc-netty-shaded:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-opentelemetry (io.grpc:grpc-opentelemetry:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-protobuf (io.grpc:grpc-protobuf:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-protobuf (io.grpc:grpc-protobuf:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-protobuf-lite (io.grpc:grpc-protobuf-lite:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-protobuf-lite (io.grpc:grpc-protobuf-lite:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-rls (io.grpc:grpc-rls:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-s2a (io.grpc:grpc-s2a:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-services (io.grpc:grpc-services:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-services (io.grpc:grpc-services:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-stub (io.grpc:grpc-stub:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-stub (io.grpc:grpc-stub:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-util (io.grpc:grpc-util:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-util (io.grpc:grpc-util:1.71.0 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-xds (io.grpc:grpc-xds:1.68.1 - https://github.com/grpc/grpc-java)
+ * io.grpc:grpc-xds (io.grpc:grpc-xds:1.71.0 - https://github.com/grpc/grpc-java)
+ * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/)
* Jackson (org.codehaus.jackson:jackson-core-asl:1.9.13 - http://jackson.codehaus.org)
- * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.41.7 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2)
- * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.42.0 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2)
- * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.13.3 - http://github.com/FasterXML/jackson)
- * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.13.3 - https://github.com/FasterXML/jackson-core)
- * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.13.3 - http://github.com/FasterXML/jackson)
- * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.3 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8)
- * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310)
+ * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.45.0 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2)
+ * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2)
+ * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.13.5 - http://github.com/FasterXML/jackson)
+ * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.18.2 - https://github.com/FasterXML/jackson)
+ * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.20.0 - https://github.com/FasterXML/jackson-core)
+ * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.13.5 - http://github.com/FasterXML/jackson)
+ * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.18.2 - https://github.com/FasterXML/jackson)
+ * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.5 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8)
+ * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.5 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310)
+ * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310)
* Jackson extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson:1.29.2 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson)
- * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.3 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names)
- * Java 6 (and higher) extensions to the Google OAuth Client Library for Java. (com.google.oauth-client:google-oauth-client-java6:1.34.1 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client-java6)
- * JCommander (com.beust:jcommander:1.48 - http://beust.com/jcommander)
+ * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.5 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names)
+ * Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api)
+ * Jandex: Core (io.smallrye:jandex:3.2.0 - https://smallrye.io)
+ * Java 6 (and higher) extensions to the Google OAuth Client Library for Java. (com.google.oauth-client:google-oauth-client-java6:1.39.0 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client-java6)
+ * Java Concurrency Tools Core Library (org.jctools:jctools-core:4.0.5 - https://github.com/JCTools)
+ * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.0.Final - http://www.jboss.org)
+ * JBoss Threads (org.jboss.threads:jboss-threads:3.6.1.Final - http://www.jboss.org)
+ * jcommander (com.beust:jcommander:1.82 - https://jcommander.org)
* jffi (com.github.jnr:jffi:1.3.9 - http://github.com/jnr/jffi)
+ * JGroups (org.jgroups:jgroups:5.4.5.Final - http://www.jgroups.org)
* jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
* jnr-constants (com.github.jnr:jnr-constants:0.10.3 - http://github.com/jnr/jnr-constants)
+ * jnr-constants (com.github.jnr:jnr-constants:0.10.4 - http://github.com/jnr/jnr-constants)
* jnr-enxio (com.github.jnr:jnr-enxio:0.32.13 - http://github.com/jnr/jnr-enxio)
+ * jnr-enxio (com.github.jnr:jnr-enxio:0.32.18 - http://github.com/jnr/jnr-enxio)
* jnr-ffi (com.github.jnr:jnr-ffi:2.2.11 - http://github.com/jnr/jnr-ffi)
+ * jnr-ffi (com.github.jnr:jnr-ffi:2.2.17 - http://github.com/jnr/jnr-ffi)
* jnr-unixsocket (com.github.jnr:jnr-unixsocket:0.38.17 - http://github.com/jnr/jnr-unixsocket)
- * Joda-Time (joda-time:joda-time:2.11.1 - https://www.joda.org/joda-time/)
+ * jnr-unixsocket (com.github.jnr:jnr-unixsocket:0.38.23 - http://github.com/jnr/jnr-unixsocket)
+ * Joda-Time (joda-time:joda-time:2.14.0 - https://www.joda.org/joda-time/)
* JSONassert (org.skyscreamer:jsonassert:1.5.1 - https://github.com/skyscreamer/JSONassert)
* JSON library from Android SDK (com.vaadin.external.google:android-json:0.0.20131108.vaadin1 - http://developer.android.com/sdk)
- * JSON Small and Fast Parser (net.minidev:json-smart:2.4.8 - https://urielch.github.io/)
+ * JSON Small and Fast Parser (net.minidev:json-smart:2.4.11 - https://urielch.github.io/)
+ * JSpecify annotations (org.jspecify:jspecify:1.0.0 - http://jspecify.org/)
* jsr107cache (net.sf.jsr107cache:jsr107cache:1.1 - http://maven.apache.org)
* juli (org.apache.tomcat:juli:6.0.53 - http://tomcat.apache.org/)
* Logging (commons-logging:commons-logging:1.0.4 - http://jakarta.apache.org/commons/logging/)
* Lucene Analyzers (org.apache.lucene:lucene-analyzers:2.9.4 - http://lucene.apache.org/java/lucene-contrib/lucene-analyzers)
* Lucene Core (org.apache.lucene:lucene-core:2.9.4 - http://lucene.apache.org/java/lucene-core)
+ * MongoDB Java Driver (org.mongodb:mongo-java-driver:2.14.3 - http://www.mongodb.org)
+ * MortBay :: Apache EL :: API and Implementation (org.mortbay.jasper:apache-el:10.1.16 - https://github.com/jetty-project/jasper-jsp/apache-el)
+ * MortBay :: Apache EL :: API and Implementation (org.mortbay.jasper:apache-el:11.0.9 - https://github.com/jetty-project/jasper-jsp/apache-el)
* MortBay :: Apache EL :: API and Implementation (org.mortbay.jasper:apache-el:8.5.70 - https://github.com/jetty-project/jasper-jsp/apache-el)
+ * MortBay :: Apache EL :: API and Implementation (org.mortbay.jasper:apache-el:9.0.52 - https://github.com/jetty-project/jasper-jsp/apache-el)
+ * MortBay :: Apache EL :: API and Implementation (org.mortbay.jasper:mortbay-apache-el:11.0.10.1 - https://github.com/jetty-project/jasper-jsp/mortbay-apache-el)
+ * MortBay :: Apache EL :: API and Implementation (org.mortbay.jasper:mortbay-apache-el:9.0.108.1 - https://github.com/jetty-project/jasper-jsp/mortbay-apache-el)
+ * MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:apache-jsp:10.1.16 - https://github.com/jetty-project/jasper-jsp/apache-jsp)
+ * MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:apache-jsp:10.1.7 - https://github.com/jetty-project/jasper-jsp/apache-jsp)
+ * MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:apache-jsp:11.0.9 - https://github.com/jetty-project/jasper-jsp/apache-jsp)
* MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:apache-jsp:8.5.70 - https://github.com/jetty-project/jasper-jsp/apache-jsp)
- * Netty/Buffer (io.netty:netty-buffer:4.1.77.Final - https://netty.io/netty-buffer/)
- * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.77.Final - https://netty.io/netty-codec-http/)
- * Netty/Codec/HTTP2 (io.netty:netty-codec-http2:4.1.77.Final - https://netty.io/netty-codec-http2/)
- * Netty/Codec/Socks (io.netty:netty-codec-socks:4.1.77.Final - https://netty.io/netty-codec-socks/)
- * Netty/Codec (io.netty:netty-codec:4.1.77.Final - https://netty.io/netty-codec/)
- * Netty/Common (io.netty:netty-common:4.1.77.Final - https://netty.io/netty-common/)
- * Netty/Handler/Proxy (io.netty:netty-handler-proxy:4.1.77.Final - https://netty.io/netty-handler-proxy/)
- * Netty/Handler (io.netty:netty-handler:4.1.77.Final - https://netty.io/netty-handler/)
- * Netty/Resolver (io.netty:netty-resolver:4.1.77.Final - https://netty.io/netty-resolver/)
- * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.77.Final - https://netty.io/netty-transport-native-unix-common/)
- * Netty/Transport (io.netty:netty-transport:4.1.77.Final - https://netty.io/netty-transport/)
+ * MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:apache-jsp:9.0.52 - https://github.com/jetty-project/jasper-jsp/apache-jsp)
+ * MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:mortbay-apache-jsp:11.0.10.1 - https://github.com/jetty-project/jasper-jsp/mortbay-apache-jsp)
+ * MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:mortbay-apache-jsp:9.0.108.1 - https://github.com/jetty-project/jasper-jsp/mortbay-apache-jsp)
+ * native-linux-x86_64 (com.aayushatharva.brotli4j:native-linux-x86_64:1.17.0 - https://github.com/hyperxpro/Brotli4j/natives/native-linux-x86_64)
+ * Netty/Buffer (io.netty:netty-buffer:4.1.110.Final - https://netty.io/netty-buffer/)
+ * Netty/Buffer (io.netty:netty-buffer:4.1.119.Final - https://netty.io/netty-buffer/)
+ * Netty/Codec/DNS (io.netty:netty-codec-dns:4.1.119.Final - https://netty.io/netty-codec-dns/)
+ * Netty/Codec (io.netty:netty-codec:4.1.119.Final - https://netty.io/netty-codec/)
+ * Netty/Common (io.netty:netty-common:4.1.110.Final - https://netty.io/netty-common/)
+ * Netty/Common (io.netty:netty-common:4.1.119.Final - https://netty.io/netty-common/)
+ * Netty/Handler (io.netty:netty-handler:4.1.119.Final - https://netty.io/netty-handler/)
+ * Netty/Resolver/DNS (io.netty:netty-resolver-dns:4.1.119.Final - https://netty.io/netty-resolver-dns/)
+ * Netty/Resolver (io.netty:netty-resolver:4.1.119.Final - https://netty.io/netty-resolver/)
+ * Netty/Transport/Classes/Epoll (io.netty:netty-transport-classes-epoll:4.1.119.Final - https://netty.io/netty-transport-classes-epoll/)
+ * Netty/Transport/Native/Epoll (io.netty:netty-transport-native-epoll:4.1.119.Final - https://netty.io/netty-transport-native-epoll/)
+ * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.119.Final - https://netty.io/netty-transport-native-unix-common/)
+ * Netty/Transport (io.netty:netty-transport:4.1.119.Final - https://netty.io/netty-transport/)
* Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis)
- * OpenCensus (io.opencensus:opencensus-api:0.30.0 - https://github.com/census-instrumentation/opencensus-java)
+ * Objenesis (org.objenesis:objenesis:3.3 - http://objenesis.org/objenesis)
+ * Objenesis (org.objenesis:objenesis:3.4 - https://objenesis.org/objenesis)
+ * Obsolete Truth Extension for Java8 (com.google.truth.extensions:truth-java8-extension:1.4.5 - http://github.com/google/truth/truth-extensions-parent/truth-java8-extension)
* OpenCensus (io.opencensus:opencensus-api:0.31.1 - https://github.com/census-instrumentation/opencensus-java)
- * OpenCensus (io.opencensus:opencensus-contrib-grpc-util:0.30.0 - https://github.com/census-instrumentation/opencensus-java)
- * OpenCensus (io.opencensus:opencensus-contrib-http-util:0.28.0 - https://github.com/census-instrumentation/opencensus-java)
+ * OpenCensus (io.opencensus:opencensus-contrib-grpc-util:0.31.1 - https://github.com/census-instrumentation/opencensus-java)
* OpenCensus (io.opencensus:opencensus-contrib-http-util:0.31.1 - https://github.com/census-instrumentation/opencensus-java)
- * OpenCensus (io.opencensus:opencensus-proto:0.2.0 - https://github.com/census-instrumentation/opencensus-proto)
+ * OpenTelemetry Instrumentation for Java (io.opentelemetry.instrumentation:opentelemetry-grpc-1.6:2.1.0-alpha - https://github.com/open-telemetry/opentelemetry-java-instrumentation)
+ * OpenTelemetry Instrumentation for Java (io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.1.0 - https://github.com/open-telemetry/opentelemetry-java-instrumentation)
+ * OpenTelemetry Instrumentation for Java (io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.1.0-alpha - https://github.com/open-telemetry/opentelemetry-java-instrumentation)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-api:1.42.1 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-api:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-context:1.42.1 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-context:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-extension-incubator:1.35.0-alpha - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-common:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-logs:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-metrics:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-trace:1.47.0 - https://github.com/open-telemetry/opentelemetry-java)
+ * OpenTelemetry Java Contrib (io.opentelemetry.contrib:opentelemetry-gcp-resources:1.37.0-alpha - https://github.com/open-telemetry/opentelemetry-java-contrib)
+ * OpenTelemetry Operations Java (com.google.cloud.opentelemetry:detector-resources-support:0.33.0 - https://github.com/GoogleCloudPlatform/opentelemetry-operations-java)
+ * OpenTelemetry Operations Java (com.google.cloud.opentelemetry:exporter-metrics:0.33.0 - https://github.com/GoogleCloudPlatform/opentelemetry-operations-java)
+ * OpenTelemetry Operations Java (com.google.cloud.opentelemetry:shared-resourcemapping:0.33.0 - https://github.com/GoogleCloudPlatform/opentelemetry-operations-java)
+ * OpenTelemetry Semantic Conventions Java (io.opentelemetry.semconv:opentelemetry-semconv:1.29.0-alpha - https://github.com/open-telemetry/semantic-conventions-java)
* org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian)
- * org.conscrypt:conscrypt-openjdk-uber (org.conscrypt:conscrypt-openjdk-uber:2.5.1 - https://conscrypt.org/)
+ * org.conscrypt:conscrypt-openjdk-uber (org.conscrypt:conscrypt-openjdk-uber:2.5.2 - https://conscrypt.org/)
* org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.2.0 - https://github.com/ota4j-team/opentest4j)
- * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.9.0 - https://www.xmlunit.org/)
- * perfmark:perfmark-api (io.perfmark:perfmark-api:0.23.0 - https://github.com/perfmark/perfmark)
- * perfmark:perfmark-api (io.perfmark:perfmark-api:0.25.0 - https://github.com/perfmark/perfmark)
+ * org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j)
+ * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.9.1 - https://www.xmlunit.org/)
+ * perfmark:perfmark-api (io.perfmark:perfmark-api:0.27.0 - https://github.com/perfmark/perfmark)
* project ':json-path' (com.jayway.jsonpath:json-path:2.7.0 - https://github.com/jayway/JsonPath)
- * Protocol Buffer extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-protobuf:1.41.7 - https://github.com/googleapis/google-http-java-client/google-http-client-protobuf)
- * Protocol Buffer extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-protobuf:1.42.0 - https://github.com/googleapis/google-http-java-client/google-http-client-protobuf)
- * proto-google-cloud-datastore-v1 (com.google.api.grpc:proto-google-cloud-datastore-v1:0.101.1 - https://github.com/googleapis/java-datastore/proto-google-cloud-datastore-v1)
- * proto-google-cloud-logging-v2 (com.google.api.grpc:proto-google-cloud-logging-v2:0.96.5 - https://github.com/googleapis/java-logging/proto-google-cloud-logging-v2)
- * proto-google-cloud-spanner-admin-database-v1 (com.google.api.grpc:proto-google-cloud-spanner-admin-database-v1:6.17.3 - https://github.com/googleapis/java-spanner/proto-google-cloud-spanner-admin-database-v1)
- * proto-google-cloud-spanner-admin-instance-v1 (com.google.api.grpc:proto-google-cloud-spanner-admin-instance-v1:6.17.3 - https://github.com/googleapis/java-spanner/proto-google-cloud-spanner-admin-instance-v1)
- * proto-google-cloud-spanner-v1 (com.google.api.grpc:proto-google-cloud-spanner-v1:6.17.3 - https://github.com/googleapis/java-spanner/proto-google-cloud-spanner-v1)
- * proto-google-common-protos (com.google.api.grpc:proto-google-common-protos:2.9.2 - https://github.com/googleapis/java-iam/proto-google-common-protos)
- * proto-google-iam-v1 (com.google.api.grpc:proto-google-iam-v1:1.1.7 - https://github.com/googleapis/java-iam/proto-google-iam-v1)
+ * Protocol Buffer extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-protobuf:1.43.3 - https://github.com/googleapis/google-http-java-client/google-http-client-protobuf)
+ * Protocol Buffer extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-protobuf:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client-protobuf)
+ * proto-google-cloud-bigquerystorage-v1 (com.google.api.grpc:proto-google-cloud-bigquerystorage-v1:3.16.3 - https://github.com/googleapis/java-bigquerystorage/proto-google-cloud-bigquerystorage-v1)
+ * proto-google-cloud-bigquerystorage-v1alpha (com.google.api.grpc:proto-google-cloud-bigquerystorage-v1alpha:3.16.3 - https://github.com/googleapis/java-bigquerystorage/proto-google-cloud-bigquerystorage-v1alpha)
+ * proto-google-cloud-bigquerystorage-v1beta (com.google.api.grpc:proto-google-cloud-bigquerystorage-v1beta:3.16.3 - https://github.com/googleapis/java-bigquerystorage/proto-google-cloud-bigquerystorage-v1beta)
+ * proto-google-cloud-bigquerystorage-v1beta1 (com.google.api.grpc:proto-google-cloud-bigquerystorage-v1beta1:0.188.3 - https://github.com/googleapis/java-bigquerystorage/proto-google-cloud-bigquerystorage-v1beta1)
+ * proto-google-cloud-bigquerystorage-v1beta2 (com.google.api.grpc:proto-google-cloud-bigquerystorage-v1beta2:0.188.3 - https://github.com/googleapis/java-bigquerystorage/proto-google-cloud-bigquerystorage-v1beta2)
+ * proto-google-cloud-datastore-admin-v1 (com.google.api.grpc:proto-google-cloud-datastore-admin-v1:2.24.3 - https://github.com/googleapis/java-datastore/proto-google-cloud-datastore-admin-v1)
+ * proto-google-cloud-datastore-admin-v1 (com.google.api.grpc:proto-google-cloud-datastore-admin-v1:2.31.4 - https://github.com/googleapis/java-datastore/proto-google-cloud-datastore-admin-v1)
+ * proto-google-cloud-datastore-v1 (com.google.api.grpc:proto-google-cloud-datastore-v1:0.108.5 - https://github.com/googleapis/java-datastore/proto-google-cloud-datastore-v1)
+ * proto-google-cloud-logging-v2 (com.google.api.grpc:proto-google-cloud-logging-v2:0.112.3 - https://github.com/googleapis/java-logging/proto-google-cloud-logging-v2)
+ * proto-google-cloud-monitoring-v3 (com.google.api.grpc:proto-google-cloud-monitoring-v3:3.63.0 - https://github.com/googleapis/google-cloud-java)
+ * proto-google-cloud-spanner-admin-database-v1 (com.google.api.grpc:proto-google-cloud-spanner-admin-database-v1:6.99.0 - https://github.com/googleapis/java-spanner/proto-google-cloud-spanner-admin-database-v1)
+ * proto-google-cloud-spanner-admin-instance-v1 (com.google.api.grpc:proto-google-cloud-spanner-admin-instance-v1:6.99.0 - https://github.com/googleapis/java-spanner/proto-google-cloud-spanner-admin-instance-v1)
+ * proto-google-cloud-spanner-v1 (com.google.api.grpc:proto-google-cloud-spanner-v1:6.99.0 - https://github.com/googleapis/java-spanner/proto-google-cloud-spanner-v1)
+ * proto-google-cloud-storage-v2 (com.google.api.grpc:proto-google-cloud-storage-v2:2.57.0 - https://github.com/googleapis/java-storage/proto-google-cloud-storage-v2)
+ * proto-google-common-protos (com.google.api.grpc:proto-google-common-protos:2.32.0 - https://github.com/googleapis/sdk-platform-java)
+ * proto-google-iam-v1 (com.google.api.grpc:proto-google-iam-v1:1.44.0 - https://github.com/googleapis/sdk-platform-java)
+ * proto-google-iam-v1 (com.google.api.grpc:proto-google-iam-v1:1.56.0 - https://github.com/googleapis/sdk-platform-java)
+ * ProtoStream - annotation processor (org.infinispan.protostream:protostream-processor:5.0.13.Final - https://github.com/infinispan/protostream)
+ * ProtoStream - builtin types (org.infinispan.protostream:protostream-types:5.0.13.Final - https://infinispan.org)
+ * ProtoStream - core (org.infinispan.protostream:protostream:5.0.13.Final - https://github.com/infinispan/protostream)
* quartz (quartz:quartz:1.5.2 - no url defined)
+ * RxJava (io.reactivex.rxjava3:rxjava:3.1.10 - https://github.com/ReactiveX/RxJava)
* S2 Geometry Library for Java (com.google.geometry:s2-geometry:2.0.0 - https://github.com/google/s2-geometry-library-java)
- * Servlet and JDO extensions to the Google API Client Library for Java. (com.google.api-client:google-api-client-servlet:2.0.0 - https://github.com/googleapis/google-api-java-client/google-api-client-servlet)
+ * service (com.aayushatharva.brotli4j:service:1.17.0 - https://github.com/hyperxpro/Brotli4j/service)
+ * Servlet and JDO extensions to the Google API Client Library for Java. (com.google.api-client:google-api-client-servlet:2.8.1 - https://github.com/googleapis/google-api-java-client/google-api-client-servlet)
+ * SmallRye Common: Annotations (io.smallrye.common:smallrye-common-annotation:2.8.0 - http://smallrye.io)
+ * SmallRye Mutiny - Core library (io.smallrye.reactive:mutiny:2.8.0 - https://smallrye.io/smallrye-mutiny)
* SnakeYAML (org.yaml:snakeyaml:1.30 - https://bitbucket.org/snakeyaml/snakeyaml)
- * Spring AOP (org.springframework:spring-aop:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Spring Beans (org.springframework:spring-beans:5.3.22 - https://github.com/spring-projects/spring-framework)
- * spring-boot (org.springframework.boot:spring-boot:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-starter (org.springframework.boot:spring-boot-starter:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-logging (org.springframework.boot:spring-boot-starter-logging:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-test (org.springframework.boot:spring-boot-test:2.7.2 - https://spring.io/projects/spring-boot)
- * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:2.7.2 - https://spring.io/projects/spring-boot)
- * Spring Commons Logging Bridge (org.springframework:spring-jcl:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Spring Context (org.springframework:spring-context:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Spring Core (org.springframework:spring-core:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Spring Expression Language (SpEL) (org.springframework:spring-expression:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Spring TestContext Framework (org.springframework:spring-test:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Spring Web (org.springframework:spring-web:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Spring Web MVC (org.springframework:spring-webmvc:5.3.22 - https://github.com/spring-projects/spring-framework)
- * Truth Core (com.google.truth:truth:1.1.3 - http://github.com/google/truth/truth)
- * Truth Extension for Java8 (com.google.truth.extensions:truth-java8-extension:1.1.3 - http://github.com/google/truth/truth-extensions-parent/truth-java8-extension)
+ * Spring AOP (org.springframework:spring-aop:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring Beans (org.springframework:spring-beans:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring Beans (org.springframework:spring-beans:5.3.39 - https://github.com/spring-projects/spring-framework)
+ * spring-boot (org.springframework.boot:spring-boot:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter (org.springframework.boot:spring-boot-starter:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-logging (org.springframework.boot:spring-boot-starter-logging:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-test (org.springframework.boot:spring-boot-test:2.7.18 - https://spring.io/projects/spring-boot)
+ * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:2.7.18 - https://spring.io/projects/spring-boot)
+ * Spring Commons Logging Bridge (org.springframework:spring-jcl:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring Commons Logging Bridge (org.springframework:spring-jcl:5.3.39 - https://github.com/spring-projects/spring-framework)
+ * Spring Context (org.springframework:spring-context:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring Core (org.springframework:spring-core:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring Core (org.springframework:spring-core:5.3.39 - https://github.com/spring-projects/spring-framework)
+ * Spring Expression Language (SpEL) (org.springframework:spring-expression:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring TestContext Framework (org.springframework:spring-test:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring Web (org.springframework:spring-web:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Spring Web MVC (org.springframework:spring-webmvc:5.3.31 - https://github.com/spring-projects/spring-framework)
+ * Truth Core (com.google.truth:truth:1.4.5 - http://github.com/google/truth/truth)
+ * wildfly-common (org.wildfly.common:wildfly-common:1.6.0.Final - http://www.jboss.org/wildfly-common)
+ * WildFly Elytron - ASN.1 (org.wildfly.security:wildfly-elytron-asn1:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-asn1)
+ * WildFly Elytron - Auth (org.wildfly.security:wildfly-elytron-auth:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-auth)
+ * WildFly Elytron - Auth Server (org.wildfly.security:wildfly-elytron-auth-server:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-auth-server)
+ * WildFly Elytron - Base (org.wildfly.security:wildfly-elytron-base:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-base)
+ * WildFly Elytron - Credential (org.wildfly.security:wildfly-elytron-credential:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-credential)
+ * WildFly Elytron - Credential (org.wildfly.security:wildfly-elytron-keystore:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-keystore)
+ * WildFly Elytron - HTTP (org.wildfly.security:wildfly-elytron-http:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-http)
+ * WildFly Elytron - Mechanism (org.wildfly.security:wildfly-elytron-mechanism:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-mechanism)
+ * WildFly Elytron - Mechanism Digest (org.wildfly.security:wildfly-elytron-mechanism-digest:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-mechanism-digest)
+ * WildFly Elytron - Mechanism GSSAPI (org.wildfly.security:wildfly-elytron-mechanism-gssapi:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-mechanism-gssapi)
+ * WildFly Elytron - Mechanism OAuth2 (org.wildfly.security:wildfly-elytron-mechanism-oauth2:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-mechanism-oauth2)
+ * WildFly Elytron - Mechanism SCRAM (org.wildfly.security:wildfly-elytron-mechanism-scram:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-mechanism-scram)
+ * WildFly Elytron - Password Implementation (org.wildfly.security:wildfly-elytron-password-impl:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-password-impl)
+ * WildFly Elytron - Permission (org.wildfly.security:wildfly-elytron-permission:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-permission)
+ * WildFly Elytron - Provider Util (org.wildfly.security:wildfly-elytron-provider-util:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-provider-util)
+ * WildFly Elytron - SASL (org.wildfly.security:wildfly-elytron-sasl:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl)
+ * WildFly Elytron - SASL Digest (org.wildfly.security:wildfly-elytron-sasl-digest:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl-digest)
+ * WildFly Elytron - SASL External (org.wildfly.security:wildfly-elytron-sasl-external:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl-external)
+ * WildFly Elytron - SASL GS2 (org.wildfly.security:wildfly-elytron-sasl-gs2:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl-gs2)
+ * WildFly Elytron - SASL GSSAPI (org.wildfly.security:wildfly-elytron-sasl-gssapi:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl-gssapi)
+ * WildFly Elytron - SASL OAuth2 (org.wildfly.security:wildfly-elytron-sasl-oauth2:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl-oauth2)
+ * WildFly Elytron - SASL Plain (org.wildfly.security:wildfly-elytron-sasl-plain:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl-plain)
+ * WildFly Elytron - SASL SCRAM (org.wildfly.security:wildfly-elytron-sasl-scram:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-sasl-scram)
+ * WildFly Elytron - Security Manager Action (org.wildfly.security:wildfly-elytron-security-manager-action:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-security-manager-action)
+ * WildFly Elytron - SSL (org.wildfly.security:wildfly-elytron-ssl:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-ssl)
+ * WildFly Elytron - Util (org.wildfly.security:wildfly-elytron-util:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-util)
+ * WildFly Elytron - X.500 (org.wildfly.security:wildfly-elytron-x500:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-x500)
+ * WildFly Elytron - X.500 Certificates (org.wildfly.security:wildfly-elytron-x500-cert:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-x500-cert)
+ * WildFly Elytron - X.500 Certificate Utility Classes (org.wildfly.security:wildfly-elytron-x500-cert-util:2.6.2.Final - http://www.jboss.org/wildfly-elytron-parent/wildfly-elytron-x500-cert-util)
+ * xmemcached (com.googlecode.xmemcached:xmemcached:2.4.8 - https://github.com/killme2008/xmemcached)
Apache License, Version 2.0, Eclipse Public License - Version 1.0
- * Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.49.v20220914 - https://eclipse.org/jetty/apache-jsp)
- * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-client)
- * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-continuation)
- * Jetty :: Distribution Assemblies (org.eclipse.jetty:jetty-distribution:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-distribution)
- * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-http)
- * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-io)
- * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-jmx)
- * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-jndi)
- * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-plus)
- * Jetty :: Quick Start (org.eclipse.jetty:jetty-quickstart:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-quickstart)
+ * Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.58.v20250814 - http://tomcat.apache.org/taglibs/standard/)
+ * Example :: Jetty Spring (org.eclipse.jetty:jetty-spring:9.4.58.v20250814 - https://jetty.org/jetty-spring/)
+ * Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-client/)
+ * Jetty :: ALPN :: Conscrypt Server Implementation (org.eclipse.jetty:jetty-alpn-conscrypt-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-conscrypt-server/)
+ * Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-java-client/)
+ * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-java-server/)
+ * Jetty :: ALPN :: OpenJDK8 Server Implementation (org.eclipse.jetty:jetty-alpn-openjdk8-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-openjdk8-server/)
+ * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-server/)
+ * Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.58.v20250814 - https://jetty.org/apache-jsp/)
+ * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.58.v20250814 - https://jetty.org/jetty-client/)
+ * Jetty :: CDI (org.eclipse.jetty:jetty-cdi:9.4.58.v20250814 - https://jetty.org/jetty-cdi/)
+ * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.58.v20250814 - https://jetty.org/jetty-continuation/)
+ * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.58.v20250814 - https://jetty.org/jetty-deploy/)
+ * Jetty :: Distribution Assemblies (org.eclipse.jetty:jetty-distribution:9.4.58.v20250814 - https://jetty.org/jetty-distribution/)
+ * Jetty :: FastCGI :: Client (org.eclipse.jetty.fcgi:fcgi-client:9.4.58.v20250814 - https://jetty.org/fcgi-parent/fcgi-client/)
+ * Jetty :: FastCGI :: Server (org.eclipse.jetty.fcgi:fcgi-server:9.4.58.v20250814 - https://jetty.org/fcgi-parent/fcgi-server/)
+ * Jetty :: GCloud :: Session Manager (org.eclipse.jetty.gcloud:jetty-gcloud-session-manager:9.4.58.v20250814 - https://jetty.org/gcloud-parent/jetty-gcloud-session-manager/)
+ * Jetty :: Hazelcast Session Manager (org.eclipse.jetty:jetty-hazelcast:9.4.58.v20250814 - https://jetty.org/jetty-hazelcast/)
+ * Jetty :: Home Assembly (org.eclipse.jetty:jetty-home:9.4.58.v20250814 - https://jetty.org/jetty-home/)
+ * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-common/)
+ * Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-hpack/)
+ * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-server/)
+ * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.58.v20250814 - https://jetty.org/jetty-http/)
+ * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.58.v20250814 - https://jetty.org/jetty-io/)
+ * Jetty :: JAAS (org.eclipse.jetty:jetty-jaas:9.4.58.v20250814 - https://jetty.org/jetty-jaas/)
+ * Jetty :: Jakarta Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2 - https://eclipse.org/jetty/jetty-jakarta-servlet-api)
+ * Jetty :: Jakarta WebSocket API for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-jakarta-websocket-api:2.0.0 - https://eclipse.org/jetty/jetty-jakarta-websocket-api)
+ * Jetty :: JASPI Security (org.eclipse.jetty:jetty-jaspi:9.4.58.v20250814 - https://jetty.org/jetty-jaspi/)
+ * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.58.v20250814 - https://jetty.org/jetty-jmx/)
+ * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.58.v20250814 - https://jetty.org/jetty-jndi/)
+ * Jetty :: Memcached :: Sessions (org.eclipse.jetty.memcached:jetty-memcached-sessions:9.4.58.v20250814 - https://jetty.org/memcached-parent/jetty-memcached-sessions/)
+ * Jetty :: NoSQL Session Managers (org.eclipse.jetty:jetty-nosql:9.4.58.v20250814 - https://jetty.org/jetty-nosql/)
+ * Jetty :: OpenID (org.eclipse.jetty:jetty-openid:9.4.58.v20250814 - https://jetty.org/jetty-openid/)
+ * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.58.v20250814 - https://jetty.org/jetty-plus/)
+ * Jetty :: Proxy (org.eclipse.jetty:jetty-proxy:9.4.58.v20250814 - https://jetty.org/jetty-proxy/)
+ * Jetty :: Quick Start (org.eclipse.jetty:jetty-quickstart:9.4.58.v20250814 - https://jetty.org/jetty-quickstart/)
+ * Jetty :: Rewrite Handler (org.eclipse.jetty:jetty-rewrite:9.4.58.v20250814 - https://jetty.org/jetty-rewrite/)
* Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1 - http://www.eclipse.org/jetty/jetty-toolchain/jetty-schemas)
- * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-security)
- * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-server)
- * Jetty :: Servlet Annotations (org.eclipse.jetty:jetty-annotations:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-annotations)
- * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-servlet)
- * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-util-ajax)
- * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-util)
- * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-servlets)
- * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-webapp)
- * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.49.v20220914 - https://eclipse.org/jetty/jetty-xml)
+ * Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:5.2 - https://eclipse.org/jetty/jetty-schemas)
+ * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.58.v20250814 - https://jetty.org/jetty-security/)
+ * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.58.v20250814 - https://jetty.org/jetty-server/)
+ * Jetty :: Servlet Annotations (org.eclipse.jetty:jetty-annotations:9.4.58.v20250814 - https://jetty.org/jetty-annotations/)
+ * Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - https://eclipse.org/jetty/jetty-servlet-api)
+ * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 - https://jetty.org/jetty-servlet/)
+ * Jetty :: Start (org.eclipse.jetty:jetty-start:9.4.58.v20250814 - https://jetty.org/jetty-start/)
+ * Jetty :: UnixSocket (org.eclipse.jetty:jetty-unixsocket:9.4.58.v20250814 - https://jetty.org/jetty-unixsocket/)
+ * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 - https://jetty.org/jetty-util-ajax/)
+ * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.58.v20250814 - https://jetty.org/jetty-util/)
+ * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.58.v20250814 - https://jetty.org/jetty-servlets/)
+ * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 - https://jetty.org/jetty-webapp/)
+ * Jetty :: Websocket :: API (org.eclipse.jetty.websocket:websocket-api:9.4.58.v20250814 - https://jetty.org/websocket-parent/websocket-api/)
+ * Jetty :: Websocket :: Client (org.eclipse.jetty.websocket:websocket-client:9.4.58.v20250814 - https://jetty.org/websocket-parent/websocket-client/)
+ * Jetty :: Websocket :: Common (org.eclipse.jetty.websocket:websocket-common:9.4.58.v20250814 - https://jetty.org/websocket-parent/websocket-common/)
+ * Jetty :: Websocket :: javax.websocket :: Client Implementation (org.eclipse.jetty.websocket:javax-websocket-client-impl:9.4.58.v20250814 - https://jetty.org/websocket-parent/javax-websocket-client-impl/)
+ * Jetty :: Websocket :: javax.websocket.server :: Server Implementation (org.eclipse.jetty.websocket:javax-websocket-server-impl:9.4.58.v20250814 - https://jetty.org/websocket-parent/javax-websocket-server-impl/)
+ * Jetty :: Websocket :: Server (org.eclipse.jetty.websocket:websocket-server:9.4.58.v20250814 - https://jetty.org/websocket-parent/websocket-server/)
+ * Jetty :: Websocket :: Servlet Interface (org.eclipse.jetty.websocket:websocket-servlet:9.4.58.v20250814 - https://jetty.org/websocket-parent/websocket-servlet/)
+ * Jetty :: WebSocket API for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-javax-websocket-api:1.1.2 - https://eclipse.org/jetty/jetty-javax-websocket-api)
+ * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.58.v20250814 - https://jetty.org/jetty-xml/)
+ * Jetty Orbit :: Activation (org.eclipse.jetty.orbit:javax.activation:1.1.0.v201105071233 - http://www.eclipse.org/jetty/jetty-orbit/javax.activation)
+ * Jetty Orbit :: Glassfish Mail (org.eclipse.jetty.orbit:javax.mail.glassfish:1.4.1.v201005082020 - http://www.eclipse.org/jetty/jetty-orbit/javax.mail.glassfish)
+ * Jetty Orbit :: JASPI API (org.eclipse.jetty.orbit:javax.security.auth.message:1.0.0.v201108011116 - http://www.eclipse.org/jetty/jetty-orbit/javax.security.auth.message)
+
+ Apache License, Version 2.0, Eclipse Public License - Version 2.0
+
+ * Core :: ALPN :: Bouncy Castle Server (org.eclipse.jetty:jetty-alpn-bouncycastle-server:12.0.26 - https://jetty.org/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server)
+ * Core :: ALPN :: Bouncy Castle Server (org.eclipse.jetty:jetty-alpn-bouncycastle-server:12.1.1 - https://jetty.org/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server)
+ * Core :: Annotations (org.eclipse.jetty:jetty-annotations:12.1.1 - https://jetty.org/jetty-core/jetty-annotations)
+ * Core :: App (org.eclipse.jetty:jetty-coreapp:12.1.1 - https://jetty.org/jetty-core/jetty-coreapp)
+ * Core :: Compression :: Brotli Support (org.eclipse.jetty.compression:jetty-compression-brotli:12.1.1 - https://jetty.org/jetty-core/jetty-compression/jetty-compression-brotli)
+ * Core :: Compression :: Common (org.eclipse.jetty.compression:jetty-compression-common:12.1.1 - https://jetty.org/jetty-core/jetty-compression/jetty-compression-common)
+ * Core :: Compression :: Gzip Support (org.eclipse.jetty.compression:jetty-compression-gzip:12.1.1 - https://jetty.org/jetty-core/jetty-compression/jetty-compression-gzip)
+ * Core :: Compression :: Server (org.eclipse.jetty.compression:jetty-compression-server:12.1.1 - https://jetty.org/jetty-core/jetty-compression/jetty-compression-server)
+ * Core :: Compression :: Zstandard Support (org.eclipse.jetty.compression:jetty-compression-zstandard:12.1.1 - https://jetty.org/jetty-core/jetty-compression/jetty-compression-zstandard)
+ * Core :: EE Common :: Webapp (org.eclipse.jetty.ee:jetty-ee-webapp:12.1.1 - https://jetty.org/jetty-core/jetty-ee/jetty-ee-webapp)
+ * Core :: EE Common (org.eclipse.jetty:jetty-ee:12.0.26 - https://jetty.org/jetty-core/jetty-ee)
+ * Core :: HTTP (org.eclipse.jetty:jetty-http:12.0.26 - https://jetty.org/jetty-core/jetty-http)
+ * Core :: HTTP (org.eclipse.jetty:jetty-http:12.1.1 - https://jetty.org/jetty-core/jetty-http)
+ * Core :: HTTP2 :: Client (org.eclipse.jetty.http2:jetty-http2-client:12.0.26 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-client)
+ * Core :: HTTP2 :: Client (org.eclipse.jetty.http2:jetty-http2-client:12.1.1 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-client)
+ * Core :: HTTP2 :: Client Transport (org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.26 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-client-transport)
+ * Core :: HTTP2 :: Client Transport (org.eclipse.jetty.http2:jetty-http2-client-transport:12.1.1 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-client-transport)
+ * Core :: HTTP2 :: Common (org.eclipse.jetty.http2:jetty-http2-common:12.0.26 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-common)
+ * Core :: HTTP2 :: Common (org.eclipse.jetty.http2:jetty-http2-common:12.1.1 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-common)
+ * Core :: HTTP2 :: HPACK (org.eclipse.jetty.http2:jetty-http2-hpack:12.0.26 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-hpack)
+ * Core :: HTTP2 :: HPACK (org.eclipse.jetty.http2:jetty-http2-hpack:12.1.1 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-hpack)
+ * Core :: HTTP2 :: Server (org.eclipse.jetty.http2:jetty-http2-server:12.0.26 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-server)
+ * Core :: HTTP2 :: Server (org.eclipse.jetty.http2:jetty-http2-server:12.1.1 - https://jetty.org/jetty-core/jetty-http2/jetty-http2-server)
+ * Core :: HTTP3 :: Common (org.eclipse.jetty.http3:jetty-http3-common:12.0.26 - https://jetty.org/jetty-core/jetty-http3/jetty-http3-common)
+ * Core :: HTTP3 :: Common (org.eclipse.jetty.http3:jetty-http3-common:12.1.1 - https://jetty.org/jetty-core/jetty-http3/jetty-http3-common)
+ * Core :: HTTP3 :: QPACK (org.eclipse.jetty.http3:jetty-http3-qpack:12.0.26 - https://jetty.org/jetty-core/jetty-http3/jetty-http3-qpack)
+ * Core :: HTTP3 :: QPACK (org.eclipse.jetty.http3:jetty-http3-qpack:12.1.1 - https://jetty.org/jetty-core/jetty-http3/jetty-http3-qpack)
+ * Core :: HTTP3 :: Server (org.eclipse.jetty.http3:jetty-http3-server:12.0.26 - https://jetty.org/jetty-core/jetty-http3/jetty-http3-server)
+ * Core :: HTTP3 :: Server (org.eclipse.jetty.http3:jetty-http3-server:12.1.1 - https://jetty.org/jetty-core/jetty-http3/jetty-http3-server)
+ * Core :: HTTP Client (org.eclipse.jetty:jetty-client:12.0.26 - https://jetty.org/jetty-core/jetty-client)
+ * Core :: HTTP Client (org.eclipse.jetty:jetty-client:12.1.1 - https://jetty.org/jetty-core/jetty-client)
+ * Core :: IO (org.eclipse.jetty:jetty-io:12.0.26 - https://jetty.org/jetty-core/jetty-io)
+ * Core :: IO (org.eclipse.jetty:jetty-io:12.1.1 - https://jetty.org/jetty-core/jetty-io)
+ * Core :: JNDI (org.eclipse.jetty:jetty-jndi:12.0.26 - https://jetty.org/jetty-core/jetty-jndi)
+ * Core :: JNDI (org.eclipse.jetty:jetty-jndi:12.1.1 - https://jetty.org/jetty-core/jetty-jndi)
+ * Core :: Plus (org.eclipse.jetty:jetty-plus:12.0.26 - https://jetty.org/jetty-core/jetty-plus)
+ * Core :: Plus (org.eclipse.jetty:jetty-plus:12.1.1 - https://jetty.org/jetty-core/jetty-plus)
+ * Core :: QUIC :: APIs (org.eclipse.jetty.quic:jetty-quic-api:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-api)
+ * Core :: QUIC :: Common (org.eclipse.jetty.quic:jetty-quic-common:12.0.26 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-common)
+ * Core :: QUIC :: Common (org.eclipse.jetty.quic:jetty-quic-common:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-common)
+ * Core :: QUIC :: Quiche :: Common (org.eclipse.jetty.quic:jetty-quic-quiche-common:12.0.26 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common)
+ * Core :: QUIC :: Quiche :: Common (org.eclipse.jetty.quic:jetty-quic-quiche-common:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common)
+ * Core :: QUIC :: Quiche :: Foreign (org.eclipse.jetty.quic:jetty-quic-quiche-foreign:12.0.26 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign)
+ * Core :: QUIC :: Quiche :: Foreign Binding (org.eclipse.jetty.quic:jetty-quic-quiche-foreign:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign)
+ * Core :: QUIC :: Quiche :: JNA Binding (org.eclipse.jetty.quic:jetty-quic-quiche-jna:12.0.26 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna)
+ * Core :: QUIC :: Quiche :: JNA Binding (org.eclipse.jetty.quic:jetty-quic-quiche-jna:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna)
+ * Core :: QUIC :: Quiche :: Server (org.eclipse.jetty.quic:jetty-quic-quiche-server:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-server)
+ * Core :: QUIC :: Server (org.eclipse.jetty.quic:jetty-quic-server:12.0.26 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-server)
+ * Core :: QUIC :: Server (org.eclipse.jetty.quic:jetty-quic-server:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-server)
+ * Core :: QUIC :: Utilities (org.eclipse.jetty.quic:jetty-quic-util:12.1.1 - https://jetty.org/jetty-core/jetty-quic/jetty-quic-util)
+ * Core :: Security (org.eclipse.jetty:jetty-security:12.0.26 - https://jetty.org/jetty-core/jetty-security)
+ * Core :: Security (org.eclipse.jetty:jetty-security:12.1.1 - https://jetty.org/jetty-core/jetty-security)
+ * Core :: Server (org.eclipse.jetty:jetty-server:12.0.26 - https://jetty.org/jetty-core/jetty-server)
+ * Core :: Server (org.eclipse.jetty:jetty-server:12.1.1 - https://jetty.org/jetty-core/jetty-server)
+ * Core :: Sessions (org.eclipse.jetty:jetty-session:12.0.26 - https://jetty.org/jetty-core/jetty-session)
+ * Core :: Sessions (org.eclipse.jetty:jetty-session:12.1.1 - https://jetty.org/jetty-core/jetty-session)
+ * Core :: Sign-In with Ethereum (org.eclipse.jetty:jetty-ethereum:12.1.1 - https://jetty.org/jetty-integrations/jetty-ethereum)
+ * Core :: Start (org.eclipse.jetty:jetty-start:12.0.26 - https://jetty.org/jetty-core/jetty-start)
+ * Core :: Start (org.eclipse.jetty:jetty-start:12.1.1 - https://jetty.org/jetty-core/jetty-start)
+ * Core :: Static App (org.eclipse.jetty:jetty-staticapp:12.1.1 - https://jetty.org/jetty-core/jetty-staticapp)
+ * Core :: Utilities (org.eclipse.jetty:jetty-util:12.0.26 - https://jetty.org/jetty-core/jetty-util)
+ * Core :: Utilities (org.eclipse.jetty:jetty-util:12.1.1 - https://jetty.org/jetty-core/jetty-util)
+ * Core :: Websocket :: Client (org.eclipse.jetty.websocket:jetty-websocket-core-client:12.0.26 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-core-client)
+ * Core :: Websocket :: Client (org.eclipse.jetty.websocket:jetty-websocket-core-client:12.1.1 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-core-client)
+ * Core :: Websocket :: Common (org.eclipse.jetty.websocket:jetty-websocket-core-common:12.0.26 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-core-common)
+ * Core :: Websocket :: Common (org.eclipse.jetty.websocket:jetty-websocket-core-common:12.1.1 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-core-common)
+ * Core :: Websocket :: Jetty API (org.eclipse.jetty.websocket:jetty-websocket-jetty-api:12.0.26 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-api)
+ * Core :: Websocket :: Jetty API (org.eclipse.jetty.websocket:jetty-websocket-jetty-api:12.1.1 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-api)
+ * Core :: Websocket :: Jetty Client (org.eclipse.jetty.websocket:jetty-websocket-jetty-client:12.0.26 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-client)
+ * Core :: Websocket :: Jetty Client (org.eclipse.jetty.websocket:jetty-websocket-jetty-client:12.1.1 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-client)
+ * Core :: Websocket :: Jetty Common (org.eclipse.jetty.websocket:jetty-websocket-jetty-common:12.0.26 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-common)
+ * Core :: Websocket :: Jetty Common (org.eclipse.jetty.websocket:jetty-websocket-jetty-common:12.1.1 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-common)
+ * Core :: Websocket :: Jetty Server (org.eclipse.jetty.websocket:jetty-websocket-jetty-server:12.0.26 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-server)
+ * Core :: Websocket :: Jetty Server (org.eclipse.jetty.websocket:jetty-websocket-jetty-server:12.1.1 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-jetty-server)
+ * Core :: Websocket :: Server (org.eclipse.jetty.websocket:jetty-websocket-core-server:12.0.26 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-core-server)
+ * Core :: Websocket :: Server (org.eclipse.jetty.websocket:jetty-websocket-core-server:12.1.1 - https://jetty.org/jetty-core/jetty-websocket/jetty-websocket-core-server)
+ * Core :: XML (org.eclipse.jetty:jetty-xml:12.0.26 - https://jetty.org/jetty-core/jetty-xml)
+ * Core :: XML (org.eclipse.jetty:jetty-xml:12.1.1 - https://jetty.org/jetty-core/jetty-xml)
+ * EE10 :: Apache JSP (org.eclipse.jetty.ee10:jetty-ee10-apache-jsp:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-apache-jsp)
+ * EE10 :: Apache JSP (org.eclipse.jetty.ee10:jetty-ee10-apache-jsp:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-apache-jsp)
+ * EE10 :: Glassfish JSTL (org.eclipse.jetty.ee10:jetty-ee10-glassfish-jstl:12.0.26 - https://projects.eclipse.org/projects/ee4j.glassfish)
+ * EE10 :: Glassfish JSTL (org.eclipse.jetty.ee10:jetty-ee10-glassfish-jstl:12.1.1 - https://projects.eclipse.org/projects/ee4j.glassfish)
+ * EE10 :: Home Assembly (org.eclipse.jetty.ee10:jetty-ee10-home:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-home)
+ * EE10 :: Home Assembly (org.eclipse.jetty.ee10:jetty-ee10-home:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-home)
+ * EE10 :: Plus (org.eclipse.jetty.ee10:jetty-ee10-plus:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-plus)
+ * EE10 :: Plus (org.eclipse.jetty.ee10:jetty-ee10-plus:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-plus)
+ * EE10 :: Proxy (org.eclipse.jetty.ee10:jetty-ee10-proxy:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-proxy)
+ * EE10 :: Proxy (org.eclipse.jetty.ee10:jetty-ee10-proxy:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-proxy)
+ * EE10 :: Quick Start (org.eclipse.jetty.ee10:jetty-ee10-quickstart:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-quickstart)
+ * EE10 :: Quick Start (org.eclipse.jetty.ee10:jetty-ee10-quickstart:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-quickstart)
+ * EE10 :: Servlet (org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-servlet)
+ * EE10 :: Servlet (org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-servlet)
+ * EE10 :: Servlet Annotations (org.eclipse.jetty.ee10:jetty-ee10-annotations:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-annotations)
+ * EE10 :: Servlet Annotations (org.eclipse.jetty.ee10:jetty-ee10-annotations:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-annotations)
+ * EE10 :: Utility Servlets and Filters (org.eclipse.jetty.ee10:jetty-ee10-servlets:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-servlets)
+ * EE10 :: Utility Servlets and Filters (org.eclipse.jetty.ee10:jetty-ee10-servlets:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-servlets)
+ * EE10 :: WebApp (org.eclipse.jetty.ee10:jetty-ee10-webapp:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-webapp)
+ * EE10 :: WebApp (org.eclipse.jetty.ee10:jetty-ee10-webapp:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-webapp)
+ * EE10 :: Websocket :: Jakarta Client (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-client)
+ * EE10 :: Websocket :: Jakarta Client (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-client)
+ * EE10 :: Websocket :: Jakarta Common (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-common:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common)
+ * EE10 :: Websocket :: Jakarta Common (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-common:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common)
+ * EE10 :: Websocket :: Jakarta Server (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-server)
+ * EE10 :: Websocket :: Jakarta Server (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-server)
+ * EE10 :: Websocket :: Jetty Client WebApp (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-client-webapp:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client-webapp)
+ * EE10 :: Websocket :: Jetty Client WebApp (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-client-webapp:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client-webapp)
+ * EE10 :: Websocket :: Jetty Server (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server)
+ * EE10 :: Websocket :: Jetty Server (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server)
+ * EE10 :: Websocket :: Servlet (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-servlet:12.0.26 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-servlet)
+ * EE10 :: Websocket :: Servlet (org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-servlet:12.1.1 - https://jetty.org/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-servlet)
+ * EE11 :: Apache JSP (org.eclipse.jetty.ee11:jetty-ee11-apache-jsp:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-apache-jsp)
+ * EE11 :: Glassfish JSTL (org.eclipse.jetty.ee11:jetty-ee11-glassfish-jstl:12.1.1 - https://projects.eclipse.org/projects/ee4j.glassfish)
+ * EE11 :: Home Assembly (org.eclipse.jetty.ee11:jetty-ee11-home:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-home)
+ * EE11 :: Plus (org.eclipse.jetty.ee11:jetty-ee11-plus:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-plus)
+ * EE11 :: Proxy (org.eclipse.jetty.ee11:jetty-ee11-proxy:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-proxy)
+ * EE11 :: Quick Start (org.eclipse.jetty.ee11:jetty-ee11-quickstart:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-quickstart)
+ * EE11 :: Servlet (org.eclipse.jetty.ee11:jetty-ee11-servlet:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-servlet)
+ * EE11 :: Servlet Annotations (org.eclipse.jetty.ee11:jetty-ee11-annotations:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-annotations)
+ * EE11 :: Utility Servlets and Filters (org.eclipse.jetty.ee11:jetty-ee11-servlets:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-servlets)
+ * EE11 :: WebApp (org.eclipse.jetty.ee11:jetty-ee11-webapp:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-webapp)
+ * EE11 :: Websocket :: Jakarta Client (org.eclipse.jetty.ee11.websocket:jetty-ee11-websocket-jakarta-client:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client)
+ * EE11 :: Websocket :: Jakarta Common (org.eclipse.jetty.ee11.websocket:jetty-ee11-websocket-jakarta-common:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common)
+ * EE11 :: Websocket :: Jakarta Server (org.eclipse.jetty.ee11.websocket:jetty-ee11-websocket-jakarta-server:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server)
+ * EE11 :: Websocket :: Jetty Client WebApp (org.eclipse.jetty.ee11.websocket:jetty-ee11-websocket-jetty-client-webapp:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jetty-client-webapp)
+ * EE11 :: Websocket :: Jetty Server (org.eclipse.jetty.ee11.websocket:jetty-ee11-websocket-jetty-server:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jetty-server)
+ * EE11 :: Websocket :: Servlet (org.eclipse.jetty.ee11.websocket:jetty-ee11-websocket-servlet:12.1.1 - https://jetty.org/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-servlet)
+ * EE8 :: Apache JSP (org.eclipse.jetty.ee8:jetty-ee8-apache-jsp:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-apache-jsp)
+ * EE8 :: Apache JSP (org.eclipse.jetty.ee8:jetty-ee8-apache-jsp:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-apache-jsp)
+ * EE8 :: Glassfish JSTL (org.eclipse.jetty.ee8:jetty-ee8-glassfish-jstl:12.0.26 - https://projects.eclipse.org/projects/ee4j.glassfish)
+ * EE8 :: Glassfish JSTL (org.eclipse.jetty.ee8:jetty-ee8-glassfish-jstl:12.1.1 - https://projects.eclipse.org/projects/ee4j.glassfish)
+ * EE8 :: Home Assembly (org.eclipse.jetty.ee8:jetty-ee8-home:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-home)
+ * EE8 :: Home Assembly (org.eclipse.jetty.ee8:jetty-ee8-home:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-home)
+ * EE8 :: Nested (org.eclipse.jetty.ee8:jetty-ee8-nested:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-nested)
+ * EE8 :: Nested (org.eclipse.jetty.ee8:jetty-ee8-nested:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-nested)
+ * EE8 :: Plus (org.eclipse.jetty.ee8:jetty-ee8-plus:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-plus)
+ * EE8 :: Plus (org.eclipse.jetty.ee8:jetty-ee8-plus:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-plus)
+ * EE8 :: Proxy (org.eclipse.jetty.ee8:jetty-ee8-proxy:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-proxy)
+ * EE8 :: Proxy (org.eclipse.jetty.ee8:jetty-ee8-proxy:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-proxy)
+ * EE8 :: Quick Start (org.eclipse.jetty.ee8:jetty-ee8-quickstart:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-quickstart)
+ * EE8 :: Quick Start (org.eclipse.jetty.ee8:jetty-ee8-quickstart:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-quickstart)
+ * EE8 :: Security (org.eclipse.jetty.ee8:jetty-ee8-security:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-security)
+ * EE8 :: Security (org.eclipse.jetty.ee8:jetty-ee8-security:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-security)
+ * EE8 :: Servlet (org.eclipse.jetty.ee8:jetty-ee8-servlet:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-servlet)
+ * EE8 :: Servlet (org.eclipse.jetty.ee8:jetty-ee8-servlet:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-servlet)
+ * EE8 :: Servlet Annotations (org.eclipse.jetty.ee8:jetty-ee8-annotations:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-annotations)
+ * EE8 :: Servlet Annotations (org.eclipse.jetty.ee8:jetty-ee8-annotations:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-annotations)
+ * EE8 :: Utility Servlets and Filters (org.eclipse.jetty.ee8:jetty-ee8-servlets:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-servlets)
+ * EE8 :: Utility Servlets and Filters (org.eclipse.jetty.ee8:jetty-ee8-servlets:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-servlets)
+ * EE8 :: WebApp (org.eclipse.jetty.ee8:jetty-ee8-webapp:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-webapp)
+ * EE8 :: WebApp (org.eclipse.jetty.ee8:jetty-ee8-webapp:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-webapp)
+ * EE8 :: Websocket :: Javax Client (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-javax-client:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-javax-client)
+ * EE8 :: Websocket :: Javax Client (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-javax-client:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-javax-client)
+ * EE8 :: Websocket :: Javax Common (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-javax-common:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-javax-common)
+ * EE8 :: Websocket :: Javax Common (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-javax-common:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-javax-common)
+ * EE8 :: Websocket :: Javax Server (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-javax-server:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-javax-server)
+ * EE8 :: Websocket :: Javax Server (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-javax-server:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-javax-server)
+ * EE8 :: Websocket :: Jetty API (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-api:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-api)
+ * EE8 :: Websocket :: Jetty API (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-api:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-api)
+ * EE8 :: Websocket :: Jetty Client (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-client:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-client)
+ * EE8 :: Websocket :: Jetty Client (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-client:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-client)
+ * EE8 :: Websocket :: Jetty Client WebApp (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-client-webapp:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-client-webapp)
+ * EE8 :: Websocket :: Jetty Client WebApp (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-client-webapp:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-client-webapp)
+ * EE8 :: Websocket :: Jetty Common (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-common:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-common)
+ * EE8 :: Websocket :: Jetty Common (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-common:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-common)
+ * EE8 :: Websocket :: Jetty Server (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-server:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-server)
+ * EE8 :: Websocket :: Jetty Server (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-server:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-jetty-server)
+ * EE8 :: Websocket :: Servlet (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-servlet:12.0.26 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-servlet)
+ * EE8 :: Websocket :: Servlet (org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-servlet:12.1.1 - https://jetty.org/jetty-ee8/jetty-ee8-websocket/jetty-ee8-websocket-servlet)
+ * EE9 :: Apache JSP (org.eclipse.jetty.ee9:jetty-ee9-apache-jsp:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-apache-jsp)
+ * EE9 :: Apache JSP (org.eclipse.jetty.ee9:jetty-ee9-apache-jsp:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-apache-jsp)
+ * EE9 :: Glassfish JSTL (org.eclipse.jetty.ee9:jetty-ee9-glassfish-jstl:12.0.26 - https://projects.eclipse.org/projects/ee4j.glassfish)
+ * EE9 :: Glassfish JSTL (org.eclipse.jetty.ee9:jetty-ee9-glassfish-jstl:12.1.1 - https://projects.eclipse.org/projects/ee4j.glassfish)
+ * EE9 :: Home Assembly (org.eclipse.jetty.ee9:jetty-ee9-home:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-home)
+ * EE9 :: Home Assembly (org.eclipse.jetty.ee9:jetty-ee9-home:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-home)
+ * EE9 :: Nested (org.eclipse.jetty.ee9:jetty-ee9-nested:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-nested)
+ * EE9 :: Nested (org.eclipse.jetty.ee9:jetty-ee9-nested:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-nested)
+ * EE9 :: Plus (org.eclipse.jetty.ee9:jetty-ee9-plus:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-plus)
+ * EE9 :: Plus (org.eclipse.jetty.ee9:jetty-ee9-plus:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-plus)
+ * EE9 :: Proxy (org.eclipse.jetty.ee9:jetty-ee9-proxy:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-proxy)
+ * EE9 :: Proxy (org.eclipse.jetty.ee9:jetty-ee9-proxy:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-proxy)
+ * EE9 :: Quick Start (org.eclipse.jetty.ee9:jetty-ee9-quickstart:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-quickstart)
+ * EE9 :: Quick Start (org.eclipse.jetty.ee9:jetty-ee9-quickstart:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-quickstart)
+ * EE9 :: Security (org.eclipse.jetty.ee9:jetty-ee9-security:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-security)
+ * EE9 :: Security (org.eclipse.jetty.ee9:jetty-ee9-security:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-security)
+ * EE9 :: Servlet (org.eclipse.jetty.ee9:jetty-ee9-servlet:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-servlet)
+ * EE9 :: Servlet (org.eclipse.jetty.ee9:jetty-ee9-servlet:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-servlet)
+ * EE9 :: Servlet Annotations (org.eclipse.jetty.ee9:jetty-ee9-annotations:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-annotations)
+ * EE9 :: Servlet Annotations (org.eclipse.jetty.ee9:jetty-ee9-annotations:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-annotations)
+ * EE9 :: Utility Servlets and Filters (org.eclipse.jetty.ee9:jetty-ee9-servlets:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-servlets)
+ * EE9 :: Utility Servlets and Filters (org.eclipse.jetty.ee9:jetty-ee9-servlets:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-servlets)
+ * EE9 :: WebApp (org.eclipse.jetty.ee9:jetty-ee9-webapp:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-webapp)
+ * EE9 :: WebApp (org.eclipse.jetty.ee9:jetty-ee9-webapp:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-webapp)
+ * EE9 :: Websocket :: Jakarta Client (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jakarta-client:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-client)
+ * EE9 :: Websocket :: Jakarta Client (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jakarta-client:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-client)
+ * EE9 :: Websocket :: Jakarta Common (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jakarta-common:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common)
+ * EE9 :: Websocket :: Jakarta Common (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jakarta-common:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common)
+ * EE9 :: Websocket :: Jakarta Server (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jakarta-server:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-server)
+ * EE9 :: Websocket :: Jakarta Server (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jakarta-server:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-server)
+ * EE9 :: Websocket :: Jetty API (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-api:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-api)
+ * EE9 :: Websocket :: Jetty API (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-api:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-api)
+ * EE9 :: Websocket :: Jetty Client (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-client:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client)
+ * EE9 :: Websocket :: Jetty Client (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-client:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client)
+ * EE9 :: Websocket :: Jetty Client WebApp (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-client-webapp:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client-webapp)
+ * EE9 :: Websocket :: Jetty Client WebApp (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-client-webapp:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client-webapp)
+ * EE9 :: Websocket :: Jetty Common (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-common:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-common)
+ * EE9 :: Websocket :: Jetty Common (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-common:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-common)
+ * EE9 :: Websocket :: Jetty Server (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-server:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server)
+ * EE9 :: Websocket :: Jetty Server (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-server:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server)
+ * EE9 :: Websocket :: Servlet (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-servlet:12.0.26 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-servlet)
+ * EE9 :: Websocket :: Servlet (org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-servlet:12.1.1 - https://jetty.org/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-servlet)
+ * Home (org.eclipse.jetty:jetty-home:12.0.26 - https://jetty.org/jetty-home)
+ * Home (org.eclipse.jetty:jetty-home:12.1.1 - https://jetty.org/jetty-home)
+ * Integrations :: Infinispan :: Remote with Querying (org.eclipse.jetty:jetty-infinispan-remote-query:12.0.26 - https://jetty.org/jetty-integrations/jetty-infinispan/jetty-infinispan-remote-query)
+ * Integrations :: Infinispan :: Remote with Querying (org.eclipse.jetty:jetty-infinispan-remote-query:12.1.1 - https://jetty.org/jetty-integrations/jetty-infinispan/jetty-infinispan-remote-query)
+ * Integrations :: Infinispan :: Sessions (org.eclipse.jetty:jetty-infinispan-common:12.0.26 - https://jetty.org/jetty-integrations/jetty-infinispan/jetty-infinispan-common)
+ * Integrations :: Infinispan :: Sessions (org.eclipse.jetty:jetty-infinispan-common:12.1.1 - https://jetty.org/jetty-integrations/jetty-infinispan/jetty-infinispan-common)
+ * Jetty :: SetUID JNA (org.eclipse.jetty.toolchain.setuid:jetty-setuid-jna:2.0.3 - https://eclipse.org/jetty/jetty-setuid-jna)
+
+ Apache License, Version 2.0, GNU Lesser General Public License version 3
+
+ * jffi (com.github.jnr:jffi:1.3.13 - http://github.com/jnr/jffi)
+
+ Apache License, Version 2.0, LGPL-2.1-or-later
+
+ * Java Native Access (net.java.dev.jna:jna-jpms:5.14.0 - https://github.com/java-native-access/jna)
+ * Java Native Access (net.java.dev.jna:jna-jpms:5.17.0 - https://github.com/java-native-access/jna)
+
+ Apache License V2.0
+
+ * FlatBuffers Java API (com.google.flatbuffers:flatbuffers-java:24.3.25 - https://github.com/google/flatbuffers)
Bouncy Castle Licence
- * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk15on:1.67 - http://www.bouncycastle.org/java.html)
- * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.67 - http://www.bouncycastle.org/java.html)
+ * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/)
+
+ BSD 2-Clause "Simplified" License
+
+ * Jetty :: Quiche Native Library (org.mortbay.jetty.quiche:jetty-quiche-native:0.24.5 - https://github.com/jetty-project/jetty-quiche-native)
+
+ BSD 2-Clause License
+
+ * zstd-jni (com.github.luben:zstd-jni:1.5.6-4 - https://github.com/luben/zstd-jni)
+
+ BSD Licence 3
+
+ * Hamcrest (org.hamcrest:hamcrest:2.1 - http://hamcrest.org/JavaHamcrest/)
BSD License
* ANTLR 3 Runtime (org.antlr:antlr-runtime:3.5.3 - http://www.antlr.org)
- * API Common (com.google.api:api-common:2.1.1 - https://github.com/googleapis/api-common-java)
- * API Common (com.google.api:api-common:2.2.1 - https://github.com/googleapis/api-common-java)
- * GAX (Google Api eXtensions) for Java (com.google.api:gax:2.16.0 - https://github.com/googleapis/gax-java)
- * GAX (Google Api eXtensions) for Java (com.google.api:gax-grpc:2.16.0 - https://github.com/googleapis/gax-java)
- * GAX (Google Api eXtensions) for Java (com.google.api:gax-httpjson:0.101.0 - https://github.com/googleapis/gax-java)
CDDL + GPLv2
* JavaServer Pages(TM) Standard Tag Library API (javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2 - http://jcp.org/en/jsr/detail?id=52)
+ * JavaServer Pages (TM) TagLib Implementation (org.glassfish.web:javax.servlet.jsp.jstl:1.2.5 - http://jstl.java.net)
* Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net)
* javax.annotation API (javax.annotation:javax.annotation-api:1.3.2 - http://jcp.org/en/jsr/detail?id=250)
* javax.transaction API (javax.transaction:javax.transaction-api:1.3 - http://jta-spec.java.net)
- * jstl (jstl:jstl:1.2 - no url defined)
* servlet-api (javax.servlet:servlet-api:2.5 - no url defined)
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
* JavaBeans(TM) Activation Framework (javax.activation:activation:1.1.1 - http://java.sun.com/javase/technologies/desktop/javabeans/jaf/index.jsp)
+ Dual license consisting of the CDDL v1.1 and GPL v2
+
+ * WebSocket client API (javax.websocket:javax.websocket-client-api:1.0 - http://websocket-spec.java.net)
+ * WebSocket server API (javax.websocket:javax.websocket-api:1.0 - http://websocket-spec.java.net)
+
Eclipse Distribution License 1.0 (BSD)
* Jakarta Activation API jar (jakarta.activation:jakarta.activation-api:1.2.2 - https://github.com/eclipse-ee4j/jaf/jakarta.activation-api)
@@ -233,77 +707,142 @@ The repository contains 3rd-party code under the following licenses:
Eclipse Public License 1.0, GNU Lesser General Public License
- * Logback Classic Module (ch.qos.logback:logback-classic:1.2.11 - http://logback.qos.ch/logback-classic)
- * Logback Core Module (ch.qos.logback:logback-core:1.2.11 - http://logback.qos.ch/logback-core)
+ * Logback Classic Module (ch.qos.logback:logback-classic:1.2.12 - http://logback.qos.ch/logback-classic)
+ * Logback Core Module (ch.qos.logback:logback-core:1.2.12 - http://logback.qos.ch/logback-core)
Eclipse Public License 2.0
* Eclipse Compiler for Java(TM) (org.eclipse.jdt:ecj:3.19.0 - http://www.eclipse.org/jdt)
+ * Eclipse Compiler for Java(TM) (org.eclipse.jdt:ecj:3.26.0 - http://www.eclipse.org/jdt)
+ * Eclipse Compiler for Java(TM) (org.eclipse.jdt:ecj:3.33.0 - https://projects.eclipse.org/projects/eclipse.jdt)
+ * JUnit Jupiter (Aggregator) (org.junit.jupiter:junit-jupiter:5.13.2 - https://junit.org/)
* JUnit Jupiter (Aggregator) (org.junit.jupiter:junit-jupiter:5.8.2 - https://junit.org/junit5/)
+ * JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.13.2 - https://junit.org/)
+ * JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.13.4 - https://junit.org/)
* JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.8.2 - https://junit.org/junit5/)
+ * JUnit Jupiter Engine (org.junit.jupiter:junit-jupiter-engine:5.13.2 - https://junit.org/)
* JUnit Jupiter Engine (org.junit.jupiter:junit-jupiter-engine:5.8.2 - https://junit.org/junit5/)
+ * JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.13.2 - https://junit.org/)
* JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.8.2 - https://junit.org/junit5/)
+ * JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.13.2 - https://junit.org/)
+ * JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.13.4 - https://junit.org/)
* JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.8.2 - https://junit.org/junit5/)
+ * JUnit Platform Engine API (org.junit.platform:junit-platform-engine:1.13.2 - https://junit.org/)
* JUnit Platform Engine API (org.junit.platform:junit-platform-engine:1.8.2 - https://junit.org/junit5/)
Eclipse Public License 2.0, GNU General Public License, version 2
+ * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta)
* Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:1.3.5 - https://projects.eclipse.org/projects/ee4j.ca)
+ * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca)
+ * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:3.0.0 - https://projects.eclipse.org/projects/ee4j.ca)
+ * Jakarta Interceptors (jakarta.interceptor:jakarta.interceptor-api:2.1.0 - https://github.com/eclipse-ee4j/interceptor-api)
+ * Jakarta Interceptors (jakarta.interceptor:jakarta.interceptor-api:2.2.0 - https://github.com/jakartaee/interceptors)
+ * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:4.0.4 - https://projects.eclipse.org/projects/ee4j.servlet)
+ * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.0.0 - https://projects.eclipse.org/projects/ee4j.servlet)
+ * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.1.0 - https://projects.eclipse.org/projects/ee4j.servlet)
+ * Jakarta Standard Tag Library API (jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0 - https://projects.eclipse.org/projects/ee4j.jstl)
+ * Jakarta Standard Tag Library API (jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2 - https://projects.eclipse.org/projects/ee4j.jstl)
+ * Jakarta Standard Tag Library Implementation (org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1 - https://projects.eclipse.org/projects/ee4j.jstl)
+ * javax.transaction API (jakarta.transaction:jakarta.transaction-api:1.3.3 - https://projects.eclipse.org/projects/ee4j.jta)
Eclipse Public License 2.0, GNU General Public License, version 2, GNU Lesser General Public License Version 2.1
* jnr-posix (com.github.jnr:jnr-posix:3.1.15 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
+ * jnr-posix (com.github.jnr:jnr-posix:3.1.20 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
- GNU General Public License, version 2
+ Eclipse Public License v. 2.0, GNU General Public License, version 2 with the GNU Classpath Exception
- * MySQL Connector/J (mysql:mysql-connector-java:8.0.28 - http://dev.mysql.com/doc/connector-j/en/)
+ * Jakarta Expression Language API (jakarta.el:jakarta.el-api:5.0.0 - https://projects.eclipse.org/projects/ee4j.el)
+ * Jakarta Expression Language API (jakarta.el:jakarta.el-api:6.0.0 - https://projects.eclipse.org/projects/ee4j.el)
+ * Jakarta Server Pages API (jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.0 - https://projects.eclipse.org/projects/ee4j.jsp)
+ * Jakarta Server Pages API (jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.1 - https://projects.eclipse.org/projects/ee4j.jsp)
+ * Jakarta Server Pages API (jakarta.servlet.jsp:jakarta.servlet.jsp-api:4.0.0 - https://projects.eclipse.org/projects/ee4j.jsp)
+ * Jakarta WebSocket - Client API (jakarta.websocket:jakarta.websocket-client-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.websocket)
+ * Jakarta WebSocket - Client API (jakarta.websocket:jakarta.websocket-client-api:2.2.0 - https://projects.eclipse.org/projects/ee4j.websocket)
+ * Jakarta WebSocket - Server API (jakarta.websocket:jakarta.websocket-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.websocket)
+ * Jakarta WebSocket - Server API (jakarta.websocket:jakarta.websocket-api:2.2.0 - https://projects.eclipse.org/projects/ee4j.websocket)
+ * WaSP (org.glassfish.wasp:wasp:4.0.0 - https://projects.eclipse.org/projects/ee4j.wasp)
- GNU General Public License, version 2, The MIT License
+ EPL-2.0
- * Checker Qual (org.checkerframework:checker-compat-qual:2.5.3 - https://checkerframework.org)
- * Checker Qual (org.checkerframework:checker-compat-qual:2.5.5 - https://checkerframework.org)
+ * Eclipse Compiler for Java(TM) (org.eclipse.jdt:ecj:3.42.0 - https://github.com/eclipse-jdt/eclipse.jdt.core/)
Go License
- * RE2/J (com.google.re2j:re2j:1.5 - http://github.com/google/re2j)
+ * RE2/J (com.google.re2j:re2j:1.7 - http://github.com/google/re2j)
+
+ MIT
+
+ * mockito-core (org.mockito:mockito-core:5.19.0 - https://github.com/mockito/mockito)
+ * mockito-junit-jupiter (org.mockito:mockito-junit-jupiter:5.19.0 - https://github.com/mockito/mockito)
+ * SLF4J API Module (org.slf4j:slf4j-api:2.0.17 - http://www.slf4j.org)
+ * SLF4J JDK14 Provider (org.slf4j:slf4j-jdk14:2.0.17 - http://www.slf4j.org)
+
+ MIT-0
+
+ * reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/)
+
+ Public Domain
+
+ * JSON in Java (org.json:json:20250107 - https://github.com/douglascrockford/JSON-java)
The 3-Clause BSD License
- * asm (org.ow2.asm:asm:9.1 - http://asm.ow2.io/)
+ * API Common (com.google.api:api-common:2.24.0 - https://github.com/googleapis/sdk-platform-java)
+ * API Common (com.google.api:api-common:2.53.0 - https://github.com/googleapis/sdk-platform-java)
* asm (org.ow2.asm:asm:9.3 - http://asm.ow2.io/)
+ * asm (org.ow2.asm:asm:9.7.1 - http://asm.ow2.io/)
+ * asm (org.ow2.asm:asm:9.8 - http://asm.ow2.io/)
* asm-analysis (org.ow2.asm:asm-analysis:9.2 - http://asm.ow2.io/)
- * asm-analysis (org.ow2.asm:asm-analysis:9.3 - http://asm.ow2.io/)
- * asm-commons (org.ow2.asm:asm-commons:9.2 - http://asm.ow2.io/)
- * asm-commons (org.ow2.asm:asm-commons:9.3 - http://asm.ow2.io/)
- * asm-tree (org.ow2.asm:asm-tree:9.2 - http://asm.ow2.io/)
- * asm-tree (org.ow2.asm:asm-tree:9.3 - http://asm.ow2.io/)
+ * asm-analysis (org.ow2.asm:asm-analysis:9.7.1 - http://asm.ow2.io/)
+ * asm-analysis (org.ow2.asm:asm-analysis:9.8 - http://asm.ow2.io/)
+ * asm-commons (org.ow2.asm:asm-commons:9.7.1 - http://asm.ow2.io/)
+ * asm-commons (org.ow2.asm:asm-commons:9.8 - http://asm.ow2.io/)
+ * asm-tree (org.ow2.asm:asm-tree:9.7.1 - http://asm.ow2.io/)
+ * asm-tree (org.ow2.asm:asm-tree:9.8 - http://asm.ow2.io/)
* asm-util (org.ow2.asm:asm-util:9.2 - http://asm.ow2.io/)
- * Google Auth Library for Java - Credentials (com.google.auth:google-auth-library-credentials:1.3.0 - https://github.com/googleapis/google-auth-library-java/google-auth-library-credentials)
- * Google Auth Library for Java - OAuth2 HTTP (com.google.auth:google-auth-library-oauth2-http:1.3.0 - https://github.com/googleapis/google-auth-library-java/google-auth-library-oauth2-http)
+ * asm-util (org.ow2.asm:asm-util:9.7.1 - http://asm.ow2.io/)
+ * GAX (Google Api eXtensions) for Java (Core) (com.google.api:gax:2.58.0 - https://github.com/googleapis/sdk-platform-java)
+ * GAX (Google Api eXtensions) for Java (Core) (com.google.api:gax:2.70.1 - https://github.com/googleapis/sdk-platform-java)
+ * GAX (Google Api eXtensions) for Java (gRPC) (com.google.api:gax-grpc:2.58.0 - https://github.com/googleapis/sdk-platform-java)
+ * GAX (Google Api eXtensions) for Java (gRPC) (com.google.api:gax-grpc:2.70.1 - https://github.com/googleapis/sdk-platform-java)
+ * GAX (Google Api eXtensions) for Java (HTTP JSON) (com.google.api:gax-httpjson:2.58.0 - https://github.com/googleapis/sdk-platform-java)
+ * GAX (Google Api eXtensions) for Java (HTTP JSON) (com.google.api:gax-httpjson:2.70.1 - https://github.com/googleapis/sdk-platform-java)
+ * Google Auth Library for Java - Credentials (com.google.auth:google-auth-library-credentials:1.30.0 - https://github.com/googleapis/google-auth-library-java/google-auth-library-credentials)
+ * Google Auth Library for Java - Credentials (com.google.auth:google-auth-library-credentials:1.37.1 - https://github.com/googleapis/google-auth-library-java/google-auth-library-credentials)
+ * Google Auth Library for Java - OAuth2 HTTP (com.google.auth:google-auth-library-oauth2-http:1.30.0 - https://github.com/googleapis/google-auth-library-java/google-auth-library-oauth2-http)
+ * Google Auth Library for Java - OAuth2 HTTP (com.google.auth:google-auth-library-oauth2-http:1.37.1 - https://github.com/googleapis/google-auth-library-java/google-auth-library-oauth2-http)
* Hamcrest (org.hamcrest:hamcrest:2.2 - http://hamcrest.org/JavaHamcrest/)
* Hamcrest Core (org.hamcrest:hamcrest-core:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-core)
* Hamcrest Core (org.hamcrest:hamcrest-core:2.2 - http://hamcrest.org/JavaHamcrest/)
- * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.21.5 - https://developers.google.com/protocol-buffers/protobuf-java/)
- * Protocol Buffers [Util] (com.google.protobuf:protobuf-java-util:3.21.5 - https://developers.google.com/protocol-buffers/protobuf-java-util/)
- * ThreeTen backport (org.threeten:threetenbp:1.5.2 - https://www.threeten.org/threetenbp)
- * ThreeTen-Extra (org.threeten:threeten-extra:1.7.0 - https://www.threeten.org/threeten-extra)
- * YamlBeans (com.contrastsecurity:yamlbeans:1.11 - https://github.com/Contrast-Security-OSS/yamlbeans)
+ * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.21.12 - https://developers.google.com/protocol-buffers/protobuf-java/)
+ * Protocol Buffers [Util] (com.google.protobuf:protobuf-java-util:3.21.12 - https://developers.google.com/protocol-buffers/protobuf-java-util/)
+ * ThreeTen backport (org.threeten:threetenbp:1.7.0 - https://www.threeten.org/threetenbp)
+ * ThreeTen-Extra (org.threeten:threeten-extra:1.8.0 - https://www.threeten.org/threeten-extra)
- The JSON License
+ The GNU General Public License, v2 with Universal FOSS Exception, v1.0
- * JSON in Java (org.json:json:20220320 - https://github.com/douglascrockford/JSON-java)
+ * MySQL Connector/J (com.mysql:mysql-connector-j:9.4.0 - http://dev.mysql.com/doc/connector-j/en/)
The MIT License
- * Animal Sniffer Annotations (org.codehaus.mojo:animal-sniffer-annotations:1.20 - http://www.mojohaus.org/animal-sniffer/animal-sniffer-annotations)
- * Animal Sniffer Annotations (org.codehaus.mojo:animal-sniffer-annotations:1.21 - https://www.mojohaus.org/animal-sniffer/animal-sniffer-annotations)
- * Checker Qual (org.checkerframework:checker-qual:3.24.0 - https://checkerframework.org)
+ * Animal Sniffer Annotations (org.codehaus.mojo:animal-sniffer-annotations:1.24 - https://www.mojohaus.org/animal-sniffer/animal-sniffer-annotations)
+ * Checker Qual (org.checkerframework:checker-compat-qual:2.5.6 - https://checkerframework.org)
+ * Checker Qual (org.checkerframework:checker-qual:3.47.0 - https://checkerframework.org/)
+ * Checker Qual (org.checkerframework:checker-qual:3.49.0 - https://checkerframework.org/)
* jnr-x86asm (com.github.jnr:jnr-x86asm:1.0.2 - http://github.com/jnr/jnr-x86asm)
- * jsoup Java HTML Parser (org.jsoup:jsoup:1.15.3 - https://jsoup.org/)
- * JUL to SLF4J bridge (org.slf4j:jul-to-slf4j:1.7.30 - http://www.slf4j.org)
+ * jsoup Java HTML Parser (org.jsoup:jsoup:1.21.2 - https://jsoup.org/)
+ * JUL to SLF4J bridge (org.slf4j:jul-to-slf4j:1.7.36 - http://www.slf4j.org)
* Mockito (org.mockito:mockito-all:1.10.19 - http://www.mockito.org)
* Mockito (org.mockito:mockito-all:2.0.2-beta - http://www.mockito.org)
* mockito-core (org.mockito:mockito-core:4.5.1 - https://github.com/mockito/mockito)
- * mockito-core (org.mockito:mockito-core:4.7.0 - https://github.com/mockito/mockito)
+ * mockito-inline (org.mockito:mockito-inline:5.2.0 - https://github.com/mockito/mockito)
* mockito-junit-jupiter (org.mockito:mockito-junit-jupiter:4.5.1 - https://github.com/mockito/mockito)
* SLF4J API Module (org.slf4j:slf4j-api:1.7.36 - http://www.slf4j.org)
+ * SLF4J API Module (org.slf4j:slf4j-api:2.0.13 - http://www.slf4j.org)
+ * YamlBeans (com.contrastsecurity:yamlbeans:1.17 - https://github.com/Contrast-Security-OSS/yamlbeans)
+
+ Unknown license
+
+ * jstl (javax.servlet:jstl:1.2 - no url defined)
diff --git a/TRYLATESTBITSINPROD.md b/TRYLATESTBITSINPROD.md
index 1ebd40053..cac5768a8 100644
--- a/TRYLATESTBITSINPROD.md
+++ b/TRYLATESTBITSINPROD.md
@@ -31,12 +31,15 @@ are bundled as a Maven assembly under `runtime-deployment
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTtarget/${project.artifactId}-${project.version}
...
@@ -106,6 +110,10 @@ deployed web application.
${appengine.runtime.location}/WEB-INF/lib/runtime-impl-jetty12-${appengine.runtime.version}.jar${appengine.runtime.location}/runtime-impl-jetty12.jar
+
+
+ ${appengine.runtime.location}/WEB-INF/lib/runtime-impl-jetty121-${appengine.runtime.version}.jar
+ ${appengine.runtime.location}/runtime-impl-jetty121.jar${appengine.runtime.location}/WEB-INF/lib/runtime-shared-jetty12-${appengine.runtime.version}.jar
@@ -114,6 +122,14 @@ deployed web application.
${appengine.runtime.location}/WEB-INF/lib/runtime-shared-jetty12-ee10-${appengine.runtime.version}.jar${appengine.runtime.location}/runtime-shared-jetty12-ee10.jar
+
+
+ ${appengine.runtime.location}/WEB-INF/lib/runtime-shared-jetty121-ee8-${appengine.runtime.version}.jar
+ ${appengine.runtime.location}/runtime-shared-jetty121-ee8.jar
+
+
+ ${appengine.runtime.location}/WEB-INF/lib/runtime-shared-jetty121-ee11-${appengine.runtime.version}.jar
+ ${appengine.runtime.location}/runtime-shared-jetty121-ee11.jar${appengine.runtime.location}/WEB-INF/lib/runtime-main-${appengine.runtime.version}.jar
@@ -132,7 +148,7 @@ In the appengine-web.xml, modify the entrypoint to use the bundled runtime jars
```
- java17
+ java21true
diff --git a/api/pom.xml b/api/pom.xml
index 85e37020d..5968a75ba 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -21,13 +21,14 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTtruejarAppEngine :: appengine-apis
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/API for Google App Engine standard environment
@@ -239,7 +240,7 @@
org.apache.maven.pluginsmaven-javadoc-plugin
- 3.11.2
+ 3.11.3com.microsoft.doclet.DocFxDocletfalse
diff --git a/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceFailureException.java b/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceFailureException.java
index b0f3659a1..efeb8264b 100644
--- a/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceFailureException.java
+++ b/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceFailureException.java
@@ -16,7 +16,7 @@
package com.google.appengine.api.appidentity;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@link AppIdentityServiceFailureException} is thrown when any unknown error occurs while
diff --git a/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceImpl.java b/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceImpl.java
index 3cbf6a491..2a2eb19e6 100644
--- a/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/appidentity/AppIdentityServiceImpl.java
@@ -46,7 +46,7 @@
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/** Implementation of the AppIdentityService interface. */
class AppIdentityServiceImpl implements AppIdentityService {
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/BlobInfo.java b/api/src/main/java/com/google/appengine/api/blobstore/BlobInfo.java
index 433c22506..b289bc897 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/BlobInfo.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/BlobInfo.java
@@ -21,7 +21,7 @@
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code BlobInfo} contains metadata about a blob. This metadata is gathered by
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/BlobInfoFactory.java b/api/src/main/java/com/google/appengine/api/blobstore/BlobInfoFactory.java
index cba413524..b788a4ccb 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/BlobInfoFactory.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/BlobInfoFactory.java
@@ -28,7 +28,7 @@
import com.google.appengine.api.datastore.Query;
import java.util.Date;
import java.util.Iterator;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code BlobInfoFactory} provides a trivial interface for retrieving
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/BlobKey.java b/api/src/main/java/com/google/appengine/api/blobstore/BlobKey.java
index a8ca23698..84e0d2e8c 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/BlobKey.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/BlobKey.java
@@ -18,7 +18,7 @@
import java.io.Serializable;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code BlobKey} contains the string identifier of a large (possibly
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreInputStream.java b/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreInputStream.java
index 46684cc23..dc9dfdabe 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreInputStream.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreInputStream.java
@@ -22,7 +22,7 @@
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.io.InputStream;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* BlobstoreInputStream provides an InputStream view of a blob in
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreService.java b/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreService.java
index 9af41b484..1bf277db1 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreService.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreService.java
@@ -21,7 +21,7 @@
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code BlobstoreService} allows you to manage the creation and
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreServiceImpl.java b/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreServiceImpl.java
index e6797953d..738ae0bf5 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/BlobstoreServiceImpl.java
@@ -40,7 +40,7 @@
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code BlobstoreServiceImpl} is an implementation of {@link BlobstoreService} that makes API
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/ByteRange.java b/api/src/main/java/com/google/appengine/api/blobstore/ByteRange.java
index da31d53dd..268a3852c 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/ByteRange.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/ByteRange.java
@@ -21,7 +21,7 @@
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A byte range as parsed from a request Range header. Format produced by this class is
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/FileInfo.java b/api/src/main/java/com/google/appengine/api/blobstore/FileInfo.java
index 98084af7a..8e7d322c4 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/FileInfo.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/FileInfo.java
@@ -18,7 +18,7 @@
import com.google.common.base.Objects;
import java.util.Date;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code FileInfo} contains metadata about an uploaded file. This metadata is
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/UploadOptions.java b/api/src/main/java/com/google/appengine/api/blobstore/UploadOptions.java
index 5598787ef..c761b8919 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/UploadOptions.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/UploadOptions.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.blobstore;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Allows users to customize the behavior of a single upload to the
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreService.java b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreService.java
index 73165c169..37ebfe4cd 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreService.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreService.java
@@ -26,13 +26,13 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
- * {@code BlobstoreService} allows you to manage the creation and
- * serving of large, immutable blobs to users.
- *
+ * @deprecated as of version 3.0, use {@link
+ * com.google.appengine.api.blobstore.jakarta.BlobstoreService} instead.
*/
+@Deprecated(since = "3.0.0")
public interface BlobstoreService {
public static final int MAX_BLOB_FETCH_SIZE = (1 << 20) - (1 << 15); // 1MB - 16K;
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceFactory.java b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceFactory.java
index 33e0d2c70..5ebde48c2 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceFactory.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceFactory.java
@@ -17,7 +17,11 @@
package com.google.appengine.api.blobstore.ee10;
-/** Creates {@link BlobstoreService} implementations for java EE 10. */
+/**
+ * @deprecated as of version 3.0, use {@link
+ * com.google.appengine.api.blobstore.jakarta.BlobstoreServiceFactory} instead.
+ */
+@Deprecated(since = "3.0.0")
public final class BlobstoreServiceFactory {
/** Creates a {@code BlobstoreService} for java EE 10. */
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceImpl.java b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceImpl.java
index d3cda0e5e..488d7fae4 100644
--- a/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceImpl.java
@@ -47,13 +47,13 @@
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
- * {@code BlobstoreServiceImpl} is an implementation of {@link BlobstoreService} that makes API
- * calls to {@link ApiProxy}.
- *
+ * @deprecated as of version 3.0, use {@link
+ * com.google.appengine.api.blobstore.jakarta.BlobstoreServiceImpl} instead.
*/
+@Deprecated(since = "3.0.0")
class BlobstoreServiceImpl implements BlobstoreService {
static final String PACKAGE = "blobstore";
static final String SERVE_HEADER = "X-AppEngine-BlobKey";
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreService.java b/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreService.java
new file mode 100644
index 000000000..786630ce2
--- /dev/null
+++ b/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreService.java
@@ -0,0 +1,243 @@
+/*
+ * 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.api.blobstore.jakarta;
+
+import com.google.appengine.api.blobstore.BlobInfo;
+import com.google.appengine.api.blobstore.BlobKey;
+import com.google.appengine.api.blobstore.ByteRange;
+import com.google.appengine.api.blobstore.FileInfo;
+import com.google.appengine.api.blobstore.UploadOptions;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@code BlobstoreService} allows you to manage the creation and
+ * serving of large, immutable blobs to users.
+ *
+ */
+public interface BlobstoreService {
+ public static final int MAX_BLOB_FETCH_SIZE = (1 << 20) - (1 << 15); // 1MB - 16K;
+
+ /**
+ * Create an absolute URL that can be used by a user to
+ * asynchronously upload a large blob. Upon completion of the
+ * upload, a callback is made to the specified URL.
+ *
+ * @param successPath A relative URL which will be invoked
+ * after the user successfully uploads a blob. Must start with a "/",
+ * and must be URL-encoded.
+ *
+ * @throws IllegalArgumentException If successPath was not valid.
+ * @throws BlobstoreFailureException If an error occurred while
+ * communicating with the blobstore.
+ */
+ String createUploadUrl(String successPath);
+
+ /**
+ * Create an absolute URL that can be used by a user to
+ * asynchronously upload a large blob. Upon completion of the
+ * upload, a callback is made to the specified URL.
+ *
+ * @param successPath A relative URL which will be invoked
+ * after the user successfully uploads a blob. Must start with a "/".
+ * @param uploadOptions Specific options applicable only for this
+ * upload URL.
+ *
+ * @throws IllegalArgumentException If successPath was not valid.
+ * @throws BlobstoreFailureException If an error occurred while
+ * communicating with the blobstore.
+ */
+ String createUploadUrl(String successPath, UploadOptions uploadOptions);
+
+ /**
+ * Arrange for the specified blob to be served as the response
+ * content for the current request. {@code response} should be
+ * uncommitted before invoking this method, and should be assumed to
+ * be committed after invoking it. Any content written before
+ * calling this method will be ignored. You may, however, append
+ * custom headers before or after calling this method.
+ *
+ *
Range header will be automatically translated from the Content-Range
+ * header in the response.
+ *
+ * @param blobKey Blob-key to serve in response.
+ * @param response HTTP response object.
+ *
+ * @throws IOException If an I/O error occurred.
+ * @throws IllegalStateException If {@code response} was already committed.
+ */
+ void serve(BlobKey blobKey, HttpServletResponse response) throws IOException;
+
+ /**
+ * Arrange for the specified blob to be served as the response
+ * content for the current request. {@code response} should be
+ * uncommitted before invoking this method, and should be assumed to
+ * be committed after invoking it. Any content written before
+ * calling this method will be ignored. You may, however, append
+ * custom headers before or after calling this method.
+ *
+ *
This method will set the App Engine blob range header to serve a
+ * byte range of that blob.
+ *
+ * @param blobKey Blob-key to serve in response.
+ * @param byteRange Byte range to serve in response.
+ * @param response HTTP response object.
+ *
+ * @throws IOException If an I/O error occurred.
+ * @throws IllegalStateException If {@code response} was already committed.
+ */
+ void serve(BlobKey blobKey, @Nullable ByteRange byteRange, HttpServletResponse response)
+ throws IOException;
+
+ /**
+ * Arrange for the specified blob to be served as the response
+ * content for the current request. {@code response} should be
+ * uncommitted before invoking this method, and should be assumed to
+ * be committed after invoking it. Any content written before
+ * calling this method will be ignored. You may, however, append
+ * custom headers before or after calling this method.
+ *
+ *
This method will set the App Engine blob range header to the content
+ * specified.
+ *
+ * @param blobKey Blob-key to serve in response.
+ * @param rangeHeader Content for range header to serve.
+ * @param response HTTP response object.
+ *
+ * @throws IOException If an I/O error occurred.
+ * @throws IllegalStateException If {@code response} was already committed.
+ */
+ void serve(BlobKey blobKey, String rangeHeader, HttpServletResponse response)
+ throws IOException;
+
+ /**
+ * Get byte range from the request.
+ *
+ * @param request HTTP request object.
+ *
+ * @return Byte range as parsed from the HTTP range header. null if there is no header.
+ *
+ * @throws RangeFormatException Unable to parse header because of invalid format.
+ * @throws UnsupportedRangeFormatException Header is a valid HTTP range header, the specific
+ * form is not supported by app engine. This includes unit types other than "bytes" and multiple
+ * ranges.
+ */
+ @Nullable ByteRange getByteRange(HttpServletRequest request);
+
+ /**
+ * Permanently deletes the specified blobs. Deleting unknown blobs is a
+ * no-op.
+ *
+ * @throws BlobstoreFailureException If an error occurred while
+ * communicating with the blobstore.
+ */
+ void delete(BlobKey... blobKeys);
+
+ /**
+ * Returns the {@link BlobKey} for any files that were uploaded, keyed by the
+ * upload form "name" field.
+ *
This method should only be called from within a request served by
+ * the destination of a {@code createUploadUrl} call.
+ *
+ * @throws IllegalStateException If not called from a blob upload
+ * callback request.
+ *
+ * @deprecated Use {@link #getUploads} instead. Note that getUploadedBlobs
+ * does not handle cases where blobs have been uploaded using the
+ * multiple="true" attribute of the file input form element.
+ */
+ @Deprecated Map getUploadedBlobs(HttpServletRequest request);
+
+ /**
+ * Returns the {@link BlobKey} for any files that were uploaded, keyed by the
+ * upload form "name" field.
+ * This method should only be called from within a request served by
+ * the destination of a {@link #createUploadUrl} call.
+ *
+ * @throws IllegalStateException If not called from a blob upload
+ * callback request.
+ * @see #getBlobInfos
+ * @see #getFileInfos
+ */
+ Map> getUploads(HttpServletRequest request);
+
+ /**
+ * Returns the {@link BlobInfo} for any files that were uploaded, keyed by the
+ * upload form "name" field.
+ * This method should only be called from within a request served by
+ * the destination of a {@link #createUploadUrl} call.
+ *
+ * @throws IllegalStateException If not called from a blob upload
+ * callback request.
+ * @see #getFileInfos
+ * @see #getUploads
+ * @since 1.7.5
+ */
+ Map> getBlobInfos(HttpServletRequest request);
+
+ /**
+ * Returns the {@link FileInfo} for any files that were uploaded, keyed by the
+ * upload form "name" field.
+ * This method should only be called from within a request served by
+ * the destination of a {@link #createUploadUrl} call.
+ *
+ * Prefer this method over {@link #getBlobInfos} or {@link #getUploads} if
+ * uploading files to Cloud Storage, as the FileInfo contains the name of the
+ * created filename in Cloud Storage.
+ *
+ * @throws IllegalStateException If not called from a blob upload
+ * callback request.
+ * @see #getBlobInfos
+ * @see #getUploads
+ * @since 1.7.5
+ */
+ Map> getFileInfos(HttpServletRequest request);
+
+ /**
+ * Get fragment from specified blob.
+ *
+ * @param blobKey Blob-key from which to fetch data.
+ * @param startIndex Start index of data to fetch.
+ * @param endIndex End index (inclusive) of data to fetch.
+ * @throws IllegalArgumentException If blob not found, indexes are negative, indexes are inverted
+ * or fetch size is too large.
+ * @throws SecurityException If the application does not have access to the blob.
+ * @throws BlobstoreFailureException If an error occurred while communicating with the blobstore.
+ */
+ byte[] fetchData(BlobKey blobKey, long startIndex, long endIndex);
+
+ /**
+ * Create a {@link BlobKey} for a Google Storage File.
+ *
+ *
The existence of the file represented by filename is not checked, hence a BlobKey can be
+ * created for a file that does not currently exist.
+ *
+ *
You can safely persist the {@link BlobKey} generated by this function.
+ *
+ *
The created {@link BlobKey} can then be used as a parameter in API methods that can support
+ * objects in Google Storage, for example {@link serve}.
+ *
+ * @param filename The Google Storage filename. The filename must be in the format
+ * "/gs/bucket_name/object_name".
+ * @throws IllegalArgumentException If the filename does not have the prefix "/gs/".
+ */
+ BlobKey createGsBlobKey(String filename);
+}
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreServiceFactory.java b/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreServiceFactory.java
new file mode 100644
index 000000000..22c664ef2
--- /dev/null
+++ b/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreServiceFactory.java
@@ -0,0 +1,28 @@
+/*
+ * 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.api.blobstore.jakarta;
+
+
+/** Creates {@link BlobstoreService} implementations for java EE 10. */
+public final class BlobstoreServiceFactory {
+
+ /** Creates a {@code BlobstoreService} for java EE 10. */
+ public static BlobstoreService getBlobstoreService() {
+ return new BlobstoreServiceImpl();
+ }
+
+}
diff --git a/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreServiceImpl.java b/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreServiceImpl.java
new file mode 100644
index 000000000..2d0bb8b20
--- /dev/null
+++ b/api/src/main/java/com/google/appengine/api/blobstore/jakarta/BlobstoreServiceImpl.java
@@ -0,0 +1,403 @@
+/*
+ * 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.api.blobstore.jakarta;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.appengine.api.blobstore.BlobInfo;
+import com.google.appengine.api.blobstore.BlobKey;
+import com.google.appengine.api.blobstore.BlobstoreFailureException;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.BlobstoreServiceError;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateEncodedGoogleStorageKeyRequest;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateEncodedGoogleStorageKeyResponse;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateUploadURLRequest;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateUploadURLResponse;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.DeleteBlobRequest;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.FetchDataRequest;
+import com.google.appengine.api.blobstore.BlobstoreServicePb.FetchDataResponse;
+import com.google.appengine.api.blobstore.ByteRange;
+import com.google.appengine.api.blobstore.FileInfo;
+import com.google.appengine.api.blobstore.UnsupportedRangeFormatException;
+import com.google.appengine.api.blobstore.UploadOptions;
+import com.google.apphosting.api.ApiProxy;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.InvalidProtocolBufferException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@code BlobstoreServiceImpl} is an implementation of {@link BlobstoreService} that makes API
+ * calls to {@link ApiProxy}.
+ *
+ */
+class BlobstoreServiceImpl implements BlobstoreService {
+ static final String PACKAGE = "blobstore";
+ static final String SERVE_HEADER = "X-AppEngine-BlobKey";
+ static final String UPLOADED_BLOBKEY_ATTR = "com.google.appengine.api.blobstore.upload.blobkeys";
+ static final String UPLOADED_BLOBINFO_ATTR =
+ "com.google.appengine.api.blobstore.upload.blobinfos";
+ static final String BLOB_RANGE_HEADER = "X-AppEngine-BlobRange";
+ static final String CREATION_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
+
+ @Override
+ public String createUploadUrl(String successPath) {
+ return createUploadUrl(successPath, UploadOptions.Builder.withDefaults());
+ }
+
+ @Override
+ public String createUploadUrl(String successPath, UploadOptions uploadOptions) {
+ if (successPath == null) {
+ throw new NullPointerException("Success path must not be null.");
+ }
+
+ CreateUploadURLRequest.Builder request =
+ CreateUploadURLRequest.newBuilder().setSuccessPath(successPath);
+
+ if (uploadOptions.hasMaxUploadSizeBytesPerBlob()) {
+ request.setMaxUploadSizePerBlobBytes(uploadOptions.getMaxUploadSizeBytesPerBlob());
+ }
+
+ if (uploadOptions.hasMaxUploadSizeBytes()) {
+ request.setMaxUploadSizeBytes(uploadOptions.getMaxUploadSizeBytes());
+ }
+
+ if (uploadOptions.hasGoogleStorageBucketName()) {
+ request.setGsBucketName(uploadOptions.getGoogleStorageBucketName());
+ }
+
+ byte[] responseBytes;
+ try {
+ responseBytes =
+ ApiProxy.makeSyncCall(PACKAGE, "CreateUploadURL", request.build().toByteArray());
+ } catch (ApiProxy.ApplicationException ex) {
+ switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
+ case URL_TOO_LONG:
+ throw new IllegalArgumentException("The resulting URL was too long.");
+ case INTERNAL_ERROR:
+ throw new BlobstoreFailureException("An internal blobstore error occurred.");
+ default:
+ throw new BlobstoreFailureException("An unexpected error occurred.", ex);
+ }
+ }
+
+ try {
+ CreateUploadURLResponse response =
+ CreateUploadURLResponse.parseFrom(
+ responseBytes, ExtensionRegistry.getEmptyRegistry());
+ if (!response.isInitialized()) {
+ throw new BlobstoreFailureException("Could not parse CreateUploadURLResponse");
+ }
+ return response.getUrl();
+
+ } catch (InvalidProtocolBufferException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public void serve(BlobKey blobKey, HttpServletResponse response) {
+ serve(blobKey, (ByteRange) null, response);
+ }
+
+ @Override
+ public void serve(BlobKey blobKey, String rangeHeader, HttpServletResponse response) {
+ serve(blobKey, ByteRange.parse(rangeHeader), response);
+ }
+
+ @Override
+ public void serve(BlobKey blobKey, @Nullable ByteRange byteRange, HttpServletResponse response) {
+ if (response.isCommitted()) {
+ throw new IllegalStateException("Response was already committed.");
+ }
+
+ // N.B.(gregwilkins): Content-Length is not needed by blobstore and causes error in jetty94
+ response.setContentLength(-1);
+
+ // N.B.: Blobstore serving is only enabled for 200 responses.
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.setHeader(SERVE_HEADER, blobKey.getKeyString());
+ if (byteRange != null) {
+ response.setHeader(BLOB_RANGE_HEADER, byteRange.toString());
+ }
+ }
+
+ @Override
+ public @Nullable ByteRange getByteRange(HttpServletRequest request) {
+ @SuppressWarnings("unchecked")
+ Enumeration rangeHeaders = request.getHeaders("range");
+ if (!rangeHeaders.hasMoreElements()) {
+ return null;
+ }
+
+ String rangeHeader = rangeHeaders.nextElement();
+ if (rangeHeaders.hasMoreElements()) {
+ throw new UnsupportedRangeFormatException("Cannot accept multiple range headers.");
+ }
+
+ return ByteRange.parse(rangeHeader);
+ }
+
+ @Override
+ public void delete(BlobKey... blobKeys) {
+ DeleteBlobRequest.Builder request = DeleteBlobRequest.newBuilder();
+ for (BlobKey blobKey : blobKeys) {
+ request.addBlobKey(blobKey.getKeyString());
+ }
+
+ if (request.getBlobKeyCount() == 0) {
+ return;
+ }
+
+ try {
+ ApiProxy.makeSyncCall(PACKAGE, "DeleteBlob", request.build().toByteArray());
+ } catch (ApiProxy.ApplicationException ex) {
+ switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
+ case INTERNAL_ERROR:
+ throw new BlobstoreFailureException("An internal blobstore error occurred.");
+ default:
+ throw new BlobstoreFailureException("An unexpected error occurred.", ex);
+ }
+ }
+ }
+
+ @Override
+ @Deprecated
+ public Map getUploadedBlobs(HttpServletRequest request) {
+ Map> blobKeys = getUploads(request);
+ Map result = Maps.newHashMapWithExpectedSize(blobKeys.size());
+
+ for (Map.Entry> entry : blobKeys.entrySet()) {
+ // In throery it is not possible for the value for an entry to be empty,
+ // and the following check is simply defensive against a possible future
+ // change to that assumption.
+ if (!entry.getValue().isEmpty()) {
+ result.put(entry.getKey(), entry.getValue().get(0));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Map> getUploads(HttpServletRequest request) {
+ // N.B.: We're storing strings instead of BlobKey
+ // objects in the request attributes to avoid conflicts between
+ // the BlobKey classes loaded by the two classloaders in the
+ // DevAppServer. We convert back to BlobKey objects here.
+ @SuppressWarnings("unchecked")
+ Map> attributes =
+ (Map>) request.getAttribute(UPLOADED_BLOBKEY_ATTR);
+ if (attributes == null) {
+ throw new IllegalStateException("Must be called from a blob upload callback request.");
+ }
+ Map> blobKeys = Maps.newHashMapWithExpectedSize(attributes.size());
+ for (Map.Entry> attr : attributes.entrySet()) {
+ List blobs = new ArrayList<>(attr.getValue().size());
+ for (String key : attr.getValue()) {
+ blobs.add(new BlobKey(key));
+ }
+ blobKeys.put(attr.getKey(), blobs);
+ }
+ return blobKeys;
+ }
+
+ @Override
+ public Map> getBlobInfos(HttpServletRequest request) {
+ @SuppressWarnings("unchecked")
+ Map>> attributes =
+ (Map>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
+ if (attributes == null) {
+ throw new IllegalStateException("Must be called from a blob upload callback request.");
+ }
+ Map> blobInfos = Maps.newHashMapWithExpectedSize(attributes.size());
+ for (Map.Entry>> attr : attributes.entrySet()) {
+ List blobs = new ArrayList<>(attr.getValue().size());
+ for (Map info : attr.getValue()) {
+ BlobKey key = new BlobKey(requireNonNull(info.get("key"), "Missing key attribute"));
+ String contentType =
+ requireNonNull(info.get("content-type"), "Missing content-type attribute");
+ String creationDateAttribute =
+ requireNonNull(info.get("creation-date"), "Missing creation-date attribute");
+ Date creationDate =
+ requireNonNull(
+ parseCreationDate(creationDateAttribute),
+ () -> "Bad creation-date attribute: " + creationDateAttribute);
+ String filename = requireNonNull(info.get("filename"), "Missing filename attribute");
+ int size = Integer.parseInt(requireNonNull(info.get("size"), "Missing size attribute"));
+ String md5Hash = requireNonNull(info.get("md5-hash"), "Missing md5-hash attribute");
+ String gsObjectName = info.get("gs-name");
+ blobs.add(
+ new BlobInfo(key, contentType, creationDate, filename, size, md5Hash, gsObjectName));
+ }
+ blobInfos.put(attr.getKey(), blobs);
+ }
+ return blobInfos;
+ }
+
+ @Override
+ public Map> getFileInfos(HttpServletRequest request) {
+ @SuppressWarnings("unchecked")
+ Map>> attributes =
+ (Map>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
+ if (attributes == null) {
+ throw new IllegalStateException("Must be called from a blob upload callback request.");
+ }
+ Map> fileInfos = Maps.newHashMapWithExpectedSize(attributes.size());
+ for (Map.Entry>> attr : attributes.entrySet()) {
+ List files = new ArrayList<>(attr.getValue().size());
+ for (Map info : attr.getValue()) {
+ String contentType =
+ requireNonNull(info.get("content-type"), "Missing content-type attribute");
+ String creationDateAttribute =
+ requireNonNull(info.get("creation-date"), "Missing creation-date attribute");
+ Date creationDate =
+ requireNonNull(
+ parseCreationDate(creationDateAttribute),
+ () -> "Invalid creation-date attribute " + creationDateAttribute);
+ String filename = requireNonNull(info.get("filename"), "Missing filename attribute");
+ long size = Long.parseLong(requireNonNull(info.get("size"), "Missing size attribute"));
+ String md5Hash = requireNonNull(info.get("md5-hash"), "Missing md5-hash attribute");
+ String gsObjectName = info.getOrDefault("gs-name", null);
+ files.add(new FileInfo(contentType, creationDate, filename, size, md5Hash, gsObjectName));
+ }
+ fileInfos.put(attr.getKey(), files);
+ }
+ return fileInfos;
+ }
+
+ @VisibleForTesting
+ protected static @Nullable Date parseCreationDate(String date) {
+ Date creationDate = null;
+ try {
+ date = date.trim().substring(0, CREATION_DATE_FORMAT.length());
+ SimpleDateFormat dateFormat = new SimpleDateFormat(CREATION_DATE_FORMAT);
+ // Enforce strict adherence to the format
+ dateFormat.setLenient(false);
+ creationDate = dateFormat.parse(date);
+ } catch (IndexOutOfBoundsException e) {
+ // This should never happen. We got a date that is shorter than the format.
+ // TODO: add log
+ } catch (ParseException e) {
+ // This should never happen. We got a date that does not match the format.
+ // TODO: add log
+ }
+ return creationDate;
+ }
+
+ @Override
+ public byte[] fetchData(BlobKey blobKey, long startIndex, long endIndex) {
+ if (startIndex < 0) {
+ throw new IllegalArgumentException("Start index must be >= 0.");
+ }
+
+ if (endIndex < startIndex) {
+ throw new IllegalArgumentException("End index must be >= startIndex.");
+ }
+
+ // +1 since endIndex is inclusive
+ long fetchSize = endIndex - startIndex + 1;
+ if (fetchSize > MAX_BLOB_FETCH_SIZE) {
+ throw new IllegalArgumentException(
+ "Blob fetch size "
+ + fetchSize
+ + " is larger "
+ + "than maximum size "
+ + MAX_BLOB_FETCH_SIZE
+ + " bytes.");
+ }
+
+ FetchDataRequest request =
+ FetchDataRequest.newBuilder()
+ .setBlobKey(blobKey.getKeyString())
+ .setStartIndex(startIndex)
+ .setEndIndex(endIndex)
+ .build();
+
+ byte[] responseBytes;
+ try {
+ responseBytes = ApiProxy.makeSyncCall(PACKAGE, "FetchData", request.toByteArray());
+ } catch (ApiProxy.ApplicationException ex) {
+ switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
+ case PERMISSION_DENIED:
+ throw new SecurityException("This application does not have access to that blob.");
+ case BLOB_NOT_FOUND:
+ throw new IllegalArgumentException("Blob not found.");
+ case INTERNAL_ERROR:
+ throw new BlobstoreFailureException("An internal blobstore error occurred.");
+ default:
+ throw new BlobstoreFailureException("An unexpected error occurred.", ex);
+ }
+ }
+
+ try {
+ FetchDataResponse response =
+ FetchDataResponse.parseFrom(responseBytes, ExtensionRegistry.getEmptyRegistry());
+ if (!response.isInitialized()) {
+ throw new BlobstoreFailureException("Could not parse FetchDataResponse");
+ }
+ return response.getData().toByteArray();
+ } catch (InvalidProtocolBufferException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public BlobKey createGsBlobKey(String filename) {
+
+ if (!filename.startsWith("/gs/")) {
+ throw new IllegalArgumentException(
+ "Google storage filenames must be" + " prefixed with /gs/");
+ }
+ CreateEncodedGoogleStorageKeyRequest request =
+ CreateEncodedGoogleStorageKeyRequest.newBuilder().setFilename(filename).build();
+
+ byte[] responseBytes;
+ try {
+ responseBytes =
+ ApiProxy.makeSyncCall(PACKAGE, "CreateEncodedGoogleStorageKey", request.toByteArray());
+ } catch (ApiProxy.ApplicationException ex) {
+ switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
+ case INTERNAL_ERROR:
+ throw new BlobstoreFailureException("An internal blobstore error occurred.");
+ default:
+ throw new BlobstoreFailureException("An unexpected error occurred.", ex);
+ }
+ }
+
+ try {
+ CreateEncodedGoogleStorageKeyResponse response =
+ CreateEncodedGoogleStorageKeyResponse.parseFrom(
+ responseBytes, ExtensionRegistry.getEmptyRegistry());
+ if (!response.isInitialized()) {
+ throw new BlobstoreFailureException(
+ "Could not parse CreateEncodedGoogleStorageKeyResponse");
+ }
+ return new BlobKey(response.getBlobKey());
+ } catch (InvalidProtocolBufferException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/api/src/main/java/com/google/appengine/api/datastore/AdminDatastoreService.java b/api/src/main/java/com/google/appengine/api/datastore/AdminDatastoreService.java
index d99d55050..cbfede301 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/AdminDatastoreService.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/AdminDatastoreService.java
@@ -35,7 +35,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An AsyncDatastoreService implementation that is pinned to a specific appId and namesapce. This
diff --git a/api/src/main/java/com/google/appengine/api/datastore/AppIdNamespace.java b/api/src/main/java/com/google/appengine/api/datastore/AppIdNamespace.java
index 0c2cc6708..01c938019 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/AppIdNamespace.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/AppIdNamespace.java
@@ -18,7 +18,7 @@
import com.google.apphosting.api.NamespaceResources;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Abstraction for a "mangled" AppId. A mangled AppId is a combination of the application id and the
diff --git a/api/src/main/java/com/google/appengine/api/datastore/AsyncCloudDatastoreV1ServiceImpl.java b/api/src/main/java/com/google/appengine/api/datastore/AsyncCloudDatastoreV1ServiceImpl.java
index a6205fc27..adcf1a723 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/AsyncCloudDatastoreV1ServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/AsyncCloudDatastoreV1ServiceImpl.java
@@ -63,7 +63,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/** An implementation of {@link AsyncDatastoreService} using the Cloud Datastore v1 API. */
class AsyncCloudDatastoreV1ServiceImpl extends BaseAsyncDatastoreServiceImpl {
diff --git a/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreService.java b/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreService.java
index c62578958..ad99ed907 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreService.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreService.java
@@ -19,7 +19,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An asynchronous version of {@link DatastoreService}. All methods return immediately and provide
diff --git a/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreServiceImpl.java b/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreServiceImpl.java
index d24e3d00d..165f4f976 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/AsyncDatastoreServiceImpl.java
@@ -59,7 +59,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An implementation of AsyncDatastoreService using the DatastoreV3 API.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/BaseAsyncDatastoreServiceImpl.java b/api/src/main/java/com/google/appengine/api/datastore/BaseAsyncDatastoreServiceImpl.java
index 39977b31c..a278d525e 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/BaseAsyncDatastoreServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/BaseAsyncDatastoreServiceImpl.java
@@ -38,7 +38,7 @@
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Logger;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* State and behavior that is common to all asynchronous Datastore API implementations.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/BaseEntityComparator.java b/api/src/main/java/com/google/appengine/api/datastore/BaseEntityComparator.java
index fb07c6318..970adcfe9 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/BaseEntityComparator.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/BaseEntityComparator.java
@@ -28,7 +28,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/** Base class for Entity comparators. */
abstract class BaseEntityComparator implements Comparator {
diff --git a/api/src/main/java/com/google/appengine/api/datastore/BaseQueryResultsSource.java b/api/src/main/java/com/google/appengine/api/datastore/BaseQueryResultsSource.java
index 7741deea4..19a4a67b0 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/BaseQueryResultsSource.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/BaseQueryResultsSource.java
@@ -23,7 +23,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Concrete implementation of QueryResultsSource which knows how to make callbacks back into the
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Blob.java b/api/src/main/java/com/google/appengine/api/datastore/Blob.java
index 8cfd0d898..f72b65df2 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Blob.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Blob.java
@@ -18,7 +18,7 @@
import java.io.Serializable;
import java.util.Arrays;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code Blob} contains an array of bytes. This byte array can be no bigger than 1MB. To store
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Category.java b/api/src/main/java/com/google/appengine/api/datastore/Category.java
index 0640c0c17..26f85074f 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Category.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Category.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A tag, ie a descriptive word or phrase. Entities may be tagged by users, and later returned by a
diff --git a/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreRemoteServiceConfig.java b/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreRemoteServiceConfig.java
index f71ea3505..fba3c7ef8 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreRemoteServiceConfig.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreRemoteServiceConfig.java
@@ -25,7 +25,7 @@
import com.google.common.collect.ImmutableSet;
import java.security.PrivateKey;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* User-configurable global properties of Cloud Datastore.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreV1ClientImpl.java b/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreV1ClientImpl.java
index 5793f2d23..8f5d5d479 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreV1ClientImpl.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/CloudDatastoreV1ClientImpl.java
@@ -60,7 +60,7 @@
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/** A thread-safe {@link CloudDatastoreV1Client} that makes remote proto-over-HTTP calls. */
final class CloudDatastoreV1ClientImpl implements CloudDatastoreV1Client {
diff --git a/api/src/main/java/com/google/appengine/api/datastore/CompositeIndexManager.java b/api/src/main/java/com/google/appengine/api/datastore/CompositeIndexManager.java
index 25cdd7ba4..369e25ef7 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/CompositeIndexManager.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/CompositeIndexManager.java
@@ -37,7 +37,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
// CAUTION: this is one of several files that implement parsing and
// validation of the index definition schema; they all must be kept in
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Cursor.java b/api/src/main/java/com/google/appengine/api/datastore/Cursor.java
index 64e6ad8e2..cd5c48041 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Cursor.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Cursor.java
@@ -26,7 +26,7 @@
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A cursor that represents a position in a query.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/DataTypeTranslator.java b/api/src/main/java/com/google/appengine/api/datastore/DataTypeTranslator.java
index 5b70196f1..8ca83c31b 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/DataTypeTranslator.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/DataTypeTranslator.java
@@ -61,7 +61,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code DataTypeTranslator} is a utility class for converting between the data store's {@code
diff --git a/api/src/main/java/com/google/appengine/api/datastore/DataTypeUtils.java b/api/src/main/java/com/google/appengine/api/datastore/DataTypeUtils.java
index 178ea42c6..814fb34e2 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/DataTypeUtils.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/DataTypeUtils.java
@@ -32,7 +32,7 @@
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code DataTypeUtils} presents a simpler interface that allows user-code to determine what
diff --git a/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceConfig.java b/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceConfig.java
index e45343adf..10e58c436 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceConfig.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceConfig.java
@@ -22,7 +22,7 @@
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collection;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* User-configurable properties of the datastore.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceGlobalConfig.java b/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceGlobalConfig.java
index 8a292c30d..d0385051b 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceGlobalConfig.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/DatastoreServiceGlobalConfig.java
@@ -40,7 +40,7 @@
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/** See {@link CloudDatastoreRemoteServiceConfig}. */
// TODO(b/64163395): consider merging with CloudDatastoreRemoteServiceConfig
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Email.java b/api/src/main/java/com/google/appengine/api/datastore/Email.java
index 6e827c0d8..d699be31b 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Email.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Email.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An e-mail address datatype. Makes no attempt at validation.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/EmbeddedEntity.java b/api/src/main/java/com/google/appengine/api/datastore/EmbeddedEntity.java
index 65ef9ba65..9623aa021 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/EmbeddedEntity.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/EmbeddedEntity.java
@@ -19,7 +19,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A property value containing embedded entity properties (and optionally a {@link Key}).
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Entities.java b/api/src/main/java/com/google/appengine/api/datastore/Entities.java
index 5a8a930c8..d14f9263e 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Entities.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Entities.java
@@ -18,7 +18,7 @@
import static java.util.Objects.requireNonNull;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Utility functions and constants for entities.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Entity.java b/api/src/main/java/com/google/appengine/api/datastore/Entity.java
index 53a765cbf..47add6ed5 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Entity.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Entity.java
@@ -22,7 +22,7 @@
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code Entity} is the fundamental unit of data storage. It has an immutable identifier (contained
diff --git a/api/src/main/java/com/google/appengine/api/datastore/EntityComparator.java b/api/src/main/java/com/google/appengine/api/datastore/EntityComparator.java
index c86afed74..10ee81bf9 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/EntityComparator.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/EntityComparator.java
@@ -26,7 +26,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A comparator with the same ordering as {@link EntityProtoComparators} which uses Entity objects
diff --git a/api/src/main/java/com/google/appengine/api/datastore/EntityProtoComparators.java b/api/src/main/java/com/google/appengine/api/datastore/EntityProtoComparators.java
index 1d8cc786b..468f506ba 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/EntityProtoComparators.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/EntityProtoComparators.java
@@ -25,7 +25,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Utilities for comparing {@link EntityProto}. This class is only public because the dev appserver
diff --git a/api/src/main/java/com/google/appengine/api/datastore/EntityTranslator.java b/api/src/main/java/com/google/appengine/api/datastore/EntityTranslator.java
index b8b20ed0a..48cbb811b 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/EntityTranslator.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/EntityTranslator.java
@@ -22,7 +22,7 @@
import com.google.storage.onestore.v3.OnestoreEntity.Reference;
import java.util.Collection;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code EntityTranslator} contains the logic to translate an {@code Entity} into the protocol
diff --git a/api/src/main/java/com/google/appengine/api/datastore/FetchOptions.java b/api/src/main/java/com/google/appengine/api/datastore/FetchOptions.java
index e630a24e0..c8b3a4a95 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/FetchOptions.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/FetchOptions.java
@@ -19,7 +19,7 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Describes the limit, offset, and chunk size to be applied when executing a {@link PreparedQuery}.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/FutureHelper.java b/api/src/main/java/com/google/appengine/api/datastore/FutureHelper.java
index c7efa5db5..4406e5ca5 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/FutureHelper.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/FutureHelper.java
@@ -23,7 +23,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Utilities for working with {@link Future Futures} in the synchronous datastore api.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/GeoPt.java b/api/src/main/java/com/google/appengine/api/datastore/GeoPt.java
index 1a1ee96da..0e13d5b96 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/GeoPt.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/GeoPt.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A geographical point, specified by float latitude and longitude coordinates. Often used to
diff --git a/api/src/main/java/com/google/appengine/api/datastore/GetOrCreateTransactionResult.java b/api/src/main/java/com/google/appengine/api/datastore/GetOrCreateTransactionResult.java
index c790a1473..610f09432 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/GetOrCreateTransactionResult.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/GetOrCreateTransactionResult.java
@@ -16,7 +16,7 @@
package com.google.appengine.api.datastore;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Helper class used to encapsulate the result of a call to {@link
diff --git a/api/src/main/java/com/google/appengine/api/datastore/IMHandle.java b/api/src/main/java/com/google/appengine/api/datastore/IMHandle.java
index 27ed2cbdc..1496ac8bb 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/IMHandle.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/IMHandle.java
@@ -19,7 +19,7 @@
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An instant messaging handle. Includes both an address and its protocol. The protocol value is
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Index.java b/api/src/main/java/com/google/appengine/api/datastore/Index.java
index f92aa9822..a84451661 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Index.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Index.java
@@ -22,7 +22,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A Datastore {@code Index} definition.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/IndexComponentsOnlyQuery.java b/api/src/main/java/com/google/appengine/api/datastore/IndexComponentsOnlyQuery.java
index c1cbeaf78..4981124ff 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/IndexComponentsOnlyQuery.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/IndexComponentsOnlyQuery.java
@@ -27,7 +27,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A query as it is actually planned on the datastore indices.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/InternalTransactionV3.java b/api/src/main/java/com/google/appengine/api/datastore/InternalTransactionV3.java
index e6ddcfc65..439981173 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/InternalTransactionV3.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/InternalTransactionV3.java
@@ -25,7 +25,7 @@
import com.google.io.protocol.ProtocolMessage;
import com.google.protobuf.MessageLite;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Implementation of the V3-specific logic to handle a {@link Transaction}.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Key.java b/api/src/main/java/com/google/appengine/api/datastore/Key.java
index a6a5e3aeb..54417dc94 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Key.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Key.java
@@ -25,7 +25,7 @@
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The primary key for a datastore entity.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/KeyFactory.java b/api/src/main/java/com/google/appengine/api/datastore/KeyFactory.java
index 9a2dd9e7f..56f0258db 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/KeyFactory.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/KeyFactory.java
@@ -20,7 +20,7 @@
import com.google.common.base.CharMatcher;
import com.google.storage.onestore.v3.OnestoreEntity.Reference;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* This class enables direct creation of {@code Key} objects, both in the root entity group (no
diff --git a/api/src/main/java/com/google/appengine/api/datastore/KeyRange.java b/api/src/main/java/com/google/appengine/api/datastore/KeyRange.java
index 748c3cdfc..055b6e191 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/KeyRange.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/KeyRange.java
@@ -19,7 +19,7 @@
import java.io.Serializable;
import java.util.Iterator;
import java.util.NoSuchElementException;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Represents a range of unique datastore identifiers from {@code getStart().getId()} to {@code
diff --git a/api/src/main/java/com/google/appengine/api/datastore/LazyList.java b/api/src/main/java/com/google/appengine/api/datastore/LazyList.java
index 6da1d36d4..fdb9e48b8 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/LazyList.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/LazyList.java
@@ -25,7 +25,7 @@
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A {@link List} implementation that pulls query results from the server lazily.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Link.java b/api/src/main/java/com/google/appengine/api/datastore/Link.java
index b907329d9..3903d3327 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Link.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Link.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A {@code Link} is a URL of limited length.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/LocationMapper.java b/api/src/main/java/com/google/appengine/api/datastore/LocationMapper.java
index 59a2f9b93..df87eeb27 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/LocationMapper.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/LocationMapper.java
@@ -18,7 +18,7 @@
import com.google.appengine.api.datastore.CloudDatastoreRemoteServiceConfig.AppId.Location;
import com.google.common.collect.ImmutableBiMap;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
class LocationMapper {
diff --git a/api/src/main/java/com/google/appengine/api/datastore/MonitoredIndexUsageTracker.java b/api/src/main/java/com/google/appengine/api/datastore/MonitoredIndexUsageTracker.java
index 8e32208a4..33f912b6f 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/MonitoredIndexUsageTracker.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/MonitoredIndexUsageTracker.java
@@ -36,7 +36,7 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* This class is used to log usages of indexes that have been selected for usage monitoring by the
diff --git a/api/src/main/java/com/google/appengine/api/datastore/MultiQueryIterator.java b/api/src/main/java/com/google/appengine/api/datastore/MultiQueryIterator.java
index a83d25414..b7b0d1950 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/MultiQueryIterator.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/MultiQueryIterator.java
@@ -25,7 +25,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* This class constructs lists of filters as defined by the components as needed.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/PhoneNumber.java b/api/src/main/java/com/google/appengine/api/datastore/PhoneNumber.java
index 8871af099..7f8d75ab7 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/PhoneNumber.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/PhoneNumber.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A human-readable phone number. No validation is performed because phone numbers have many
diff --git a/api/src/main/java/com/google/appengine/api/datastore/PostalAddress.java b/api/src/main/java/com/google/appengine/api/datastore/PostalAddress.java
index 6c91a63d7..c20a4fcfd 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/PostalAddress.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/PostalAddress.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A human-readable mailing address. Mailing address formats vary widely so no validation is
diff --git a/api/src/main/java/com/google/appengine/api/datastore/PreQueryContext.java b/api/src/main/java/com/google/appengine/api/datastore/PreQueryContext.java
index 54411b10e..7b57dfa5e 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/PreQueryContext.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/PreQueryContext.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.util.Arrays;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Concrete {@link CallbackContext} implementation that is specific to intercepted queries. Methods
diff --git a/api/src/main/java/com/google/appengine/api/datastore/PreparedMultiQuery.java b/api/src/main/java/com/google/appengine/api/datastore/PreparedMultiQuery.java
index 64b71211b..d9c529a18 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/PreparedMultiQuery.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/PreparedMultiQuery.java
@@ -36,7 +36,7 @@
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A {@link PreparedQuery} implementation for use with {@link MultiQueryBuilder}.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/PreparedQueryImpl.java b/api/src/main/java/com/google/appengine/api/datastore/PreparedQueryImpl.java
index 4dfa0fdce..5a4831763 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/PreparedQueryImpl.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/PreparedQueryImpl.java
@@ -36,11 +36,6 @@ public PreparedQueryImpl(Query query, Transaction txn, QueryRunner queryRunner)
this.txn = txn;
this.queryRunner = queryRunner;
- // TODO Move this check and the one that follows into the
- // LocalDatastoreService (it may already be there).
- checkArgument(
- txn == null || query.getAncestor() != null,
- "Only ancestor queries are allowed inside transactions.");
TransactionImpl.ensureTxnActive(txn);
}
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Projection.java b/api/src/main/java/com/google/appengine/api/datastore/Projection.java
index ec1f3ec9d..50b95421c 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Projection.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Projection.java
@@ -18,7 +18,7 @@
import java.io.Serializable;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A query projection.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/PropertyContainer.java b/api/src/main/java/com/google/appengine/api/datastore/PropertyContainer.java
index f01532e6e..67a235258 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/PropertyContainer.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/PropertyContainer.java
@@ -28,7 +28,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A mutable property container.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/PropertyProjection.java b/api/src/main/java/com/google/appengine/api/datastore/PropertyProjection.java
index bc2d50e8f..bd8735d9a 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/PropertyProjection.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/PropertyProjection.java
@@ -21,7 +21,7 @@
import java.util.Map;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A property projection.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Query.java b/api/src/main/java/com/google/appengine/api/datastore/Query.java
index 7e76ee796..73b4aae44 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Query.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Query.java
@@ -34,7 +34,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@link Query} encapsulates a request for zero or more {@link Entity} objects out of the
diff --git a/api/src/main/java/com/google/appengine/api/datastore/QueryResultListDelegator.java b/api/src/main/java/com/google/appengine/api/datastore/QueryResultListDelegator.java
index fe8bddad1..b9aa97e4b 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/QueryResultListDelegator.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/QueryResultListDelegator.java
@@ -20,7 +20,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A class that simply forwards {@link QueryResult} methods to one delegate and forwards {@link
diff --git a/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceCloudDatastoreV1.java b/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceCloudDatastoreV1.java
index 362b8fe23..3fff9ecff 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceCloudDatastoreV1.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceCloudDatastoreV1.java
@@ -20,7 +20,7 @@
import com.google.datastore.v1.RunQueryRequest;
import com.google.datastore.v1.RunQueryResponse;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
class QueryResultsSourceCloudDatastoreV1
extends BaseQueryResultsSource {
diff --git a/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceV3.java b/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceV3.java
index f0a74f9d9..ba092e2a6 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceV3.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/QueryResultsSourceV3.java
@@ -31,7 +31,7 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* V3 service specific code for iterating query results and requesting more results. Instances can
diff --git a/api/src/main/java/com/google/appengine/api/datastore/QuerySplitComponent.java b/api/src/main/java/com/google/appengine/api/datastore/QuerySplitComponent.java
index 9d8a55a9d..424d7b95d 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/QuerySplitComponent.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/QuerySplitComponent.java
@@ -23,7 +23,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A class that holds information about a given query component that will later be converted into a
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Rating.java b/api/src/main/java/com/google/appengine/api/datastore/Rating.java
index 017960da7..7f9b5f2bb 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Rating.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Rating.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/** A user-provided integer rating for a piece of content. Normalized to a 0-100 scale. */
// TODO: Make the file GWT-compatible (the method
diff --git a/api/src/main/java/com/google/appengine/api/datastore/RawValue.java b/api/src/main/java/com/google/appengine/api/datastore/RawValue.java
index 4a52e993f..0c15a316d 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/RawValue.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/RawValue.java
@@ -31,7 +31,7 @@
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A raw datastore value.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/ReadPolicy.java b/api/src/main/java/com/google/appengine/api/datastore/ReadPolicy.java
index d8555e3e0..feff11729 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/ReadPolicy.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/ReadPolicy.java
@@ -16,7 +16,7 @@
package com.google.appengine.api.datastore;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Policy for reads.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/ShortBlob.java b/api/src/main/java/com/google/appengine/api/datastore/ShortBlob.java
index 935d47275..94f79e972 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/ShortBlob.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/ShortBlob.java
@@ -18,7 +18,7 @@
import java.io.Serializable;
import java.util.Arrays;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code ShortBlob} contains an array of bytes no longer than {@link
diff --git a/api/src/main/java/com/google/appengine/api/datastore/Text.java b/api/src/main/java/com/google/appengine/api/datastore/Text.java
index c95c6ab2c..f5cde6a14 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/Text.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/Text.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.datastore;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
// TODO: deprecate in favor of an unindexed String.
/**
diff --git a/api/src/main/java/com/google/appengine/api/datastore/TransactionImpl.java b/api/src/main/java/com/google/appengine/api/datastore/TransactionImpl.java
index 3b88b2a5f..8c753bf8b 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/TransactionImpl.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/TransactionImpl.java
@@ -22,7 +22,7 @@
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* State and behavior that is common to all {@link Transaction} implementations.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/TransactionOptions.java b/api/src/main/java/com/google/appengine/api/datastore/TransactionOptions.java
index afb1b8de5..9d04ac58b 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/TransactionOptions.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/TransactionOptions.java
@@ -18,7 +18,7 @@
import java.util.ArrayList;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Describes options for transactions, passed at transaction creation time.
diff --git a/api/src/main/java/com/google/appengine/api/datastore/ValidatedQuery.java b/api/src/main/java/com/google/appengine/api/datastore/ValidatedQuery.java
index ffb5d83e8..5c356fc2f 100644
--- a/api/src/main/java/com/google/appengine/api/datastore/ValidatedQuery.java
+++ b/api/src/main/java/com/google/appengine/api/datastore/ValidatedQuery.java
@@ -31,7 +31,7 @@
import com.google.storage.onestore.v3.OnestoreEntity.Reference;
import java.util.HashSet;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/** Wrapper around {@link Query} that performs validation. */
class ValidatedQuery extends NormalizedQuery {
@@ -93,13 +93,6 @@ private void validateQuery() {
}
}
- // Transaction requires ancestor
- if (query.hasTransaction() && !query.hasAncestor()) {
- throw new IllegalQueryException(
- "Only ancestor queries are allowed inside transactions.",
- IllegalQueryType.TRANSACTION_REQUIRES_ANCESTOR);
- }
-
// Filters and sort orders require kind.
if (!query.hasKind()) {
for (Filter filter : query.filters()) {
diff --git a/api/src/main/java/com/google/appengine/api/images/Image.java b/api/src/main/java/com/google/appengine/api/images/Image.java
index 5778b2cbf..8e7a0c008 100644
--- a/api/src/main/java/com/google/appengine/api/images/Image.java
+++ b/api/src/main/java/com/google/appengine/api/images/Image.java
@@ -18,7 +18,7 @@
import com.google.appengine.api.blobstore.BlobKey;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code Image} represents an image that can be manipulated by the
diff --git a/api/src/main/java/com/google/appengine/api/images/ImageImpl.java b/api/src/main/java/com/google/appengine/api/images/ImageImpl.java
index 6cbf787ce..f667de31e 100644
--- a/api/src/main/java/com/google/appengine/api/images/ImageImpl.java
+++ b/api/src/main/java/com/google/appengine/api/images/ImageImpl.java
@@ -22,7 +22,7 @@
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Implementation of the {@link Image} interface.
diff --git a/api/src/main/java/com/google/appengine/api/images/ImagesServiceImpl.java b/api/src/main/java/com/google/appengine/api/images/ImagesServiceImpl.java
index 9d98fb306..1d447b78d 100644
--- a/api/src/main/java/com/google/appengine/api/images/ImagesServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/images/ImagesServiceImpl.java
@@ -44,7 +44,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Implementation of the ImagesService interface.
diff --git a/api/src/main/java/com/google/appengine/api/images/ServingUrlOptions.java b/api/src/main/java/com/google/appengine/api/images/ServingUrlOptions.java
index d2d75586e..ef10dab3e 100644
--- a/api/src/main/java/com/google/appengine/api/images/ServingUrlOptions.java
+++ b/api/src/main/java/com/google/appengine/api/images/ServingUrlOptions.java
@@ -20,7 +20,7 @@
import com.google.appengine.api.blobstore.BlobKey;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Allow users to customize the behavior of creating a image serving URL using
diff --git a/api/src/main/java/com/google/appengine/api/log/AppLogLine.java b/api/src/main/java/com/google/appengine/api/log/AppLogLine.java
index bac059479..fcfd8b563 100644
--- a/api/src/main/java/com/google/appengine/api/log/AppLogLine.java
+++ b/api/src/main/java/com/google/appengine/api/log/AppLogLine.java
@@ -19,7 +19,7 @@
import com.google.appengine.api.log.LogService.LogLevel;
import java.io.Serializable;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An AppLogLine contains all the information for a single application
diff --git a/api/src/main/java/com/google/appengine/api/log/LogQuery.java b/api/src/main/java/com/google/appengine/api/log/LogQuery.java
index 12ed81ed6..7c0611e1b 100644
--- a/api/src/main/java/com/google/appengine/api/log/LogQuery.java
+++ b/api/src/main/java/com/google/appengine/api/log/LogQuery.java
@@ -27,7 +27,7 @@
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Allows users to customize the behavior of {@link LogService#fetch(LogQuery)}.
diff --git a/api/src/main/java/com/google/appengine/api/log/LogQueryResult.java b/api/src/main/java/com/google/appengine/api/log/LogQueryResult.java
index d07c9b4e6..aa1513cfa 100644
--- a/api/src/main/java/com/google/appengine/api/log/LogQueryResult.java
+++ b/api/src/main/java/com/google/appengine/api/log/LogQueryResult.java
@@ -29,7 +29,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An object that is the result of performing a LogService.fetch() operation. LogQueryResults
diff --git a/api/src/main/java/com/google/appengine/api/log/LogServiceException.java b/api/src/main/java/com/google/appengine/api/log/LogServiceException.java
index 18a05eec0..431157d78 100644
--- a/api/src/main/java/com/google/appengine/api/log/LogServiceException.java
+++ b/api/src/main/java/com/google/appengine/api/log/LogServiceException.java
@@ -16,7 +16,7 @@
package com.google.appengine.api.log;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Log errors apart from InvalidRequestException. These errors will generally benefit from retrying
diff --git a/api/src/main/java/com/google/appengine/api/log/LogServiceImpl.java b/api/src/main/java/com/google/appengine/api/log/LogServiceImpl.java
index d455e99b2..9a9de68de 100644
--- a/api/src/main/java/com/google/appengine/api/log/LogServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/log/LogServiceImpl.java
@@ -33,7 +33,7 @@
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code LogServiceImpl} is an implementation of {@link LogService} that makes API calls to {@link
diff --git a/api/src/main/java/com/google/appengine/api/log/RequestLogs.java b/api/src/main/java/com/google/appengine/api/log/RequestLogs.java
index 246d9a561..be3b2cb11 100644
--- a/api/src/main/java/com/google/appengine/api/log/RequestLogs.java
+++ b/api/src/main/java/com/google/appengine/api/log/RequestLogs.java
@@ -24,7 +24,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* RequestLogs contain all the log information for a single request. This
diff --git a/api/src/main/java/com/google/appengine/api/mail/BounceNotification.java b/api/src/main/java/com/google/appengine/api/mail/BounceNotification.java
index e72ba7617..6a9232bd6 100644
--- a/api/src/main/java/com/google/appengine/api/mail/BounceNotification.java
+++ b/api/src/main/java/com/google/appengine/api/mail/BounceNotification.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.mail;
import javax.mail.internet.MimeMessage;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The {@code BounceNotification} object represents an incoming bounce
diff --git a/api/src/main/java/com/google/appengine/api/mail/MailService.java b/api/src/main/java/com/google/appengine/api/mail/MailService.java
index 024d124e2..078b7d001 100644
--- a/api/src/main/java/com/google/appengine/api/mail/MailService.java
+++ b/api/src/main/java/com/google/appengine/api/mail/MailService.java
@@ -19,7 +19,7 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The {@code MailService} provides a way for user code to send emails
diff --git a/api/src/main/java/com/google/appengine/api/mail/ee10/BounceNotificationParser.java b/api/src/main/java/com/google/appengine/api/mail/ee10/BounceNotificationParser.java
index 5d2450cd5..1580bd8e5 100644
--- a/api/src/main/java/com/google/appengine/api/mail/ee10/BounceNotificationParser.java
+++ b/api/src/main/java/com/google/appengine/api/mail/ee10/BounceNotificationParser.java
@@ -28,10 +28,10 @@
import javax.mail.internet.MimeMultipart;
/**
- * The {@code BounceNotificationParser} parses an incoming HTTP request into
- * a description of a bounce notification.
- *
+ * @deprecated as of version 3.0, use {@link
+ * com.google.appengine.api.mail.jakarta.BounceNotificationParser} instead.
*/
+@Deprecated(since = "3.0.0")
public final class BounceNotificationParser extends HttpRequestParser {
/**
* Parse the POST data of the given request to get details about the bounce notification.
diff --git a/api/src/main/java/com/google/appengine/api/mail/jakarta/BounceNotificationParser.java b/api/src/main/java/com/google/appengine/api/mail/jakarta/BounceNotificationParser.java
new file mode 100644
index 000000000..b58a754da
--- /dev/null
+++ b/api/src/main/java/com/google/appengine/api/mail/jakarta/BounceNotificationParser.java
@@ -0,0 +1,102 @@
+/*
+ * 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.api.mail.jakarta;
+
+import com.google.appengine.api.mail.BounceNotification;
+import com.google.appengine.api.utils.jakarta.HttpRequestParser;
+import jakarta.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.Properties;
+import javax.mail.BodyPart;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+
+/**
+ * The {@code BounceNotificationParser} parses an incoming HTTP request into
+ * a description of a bounce notification.
+ *
+ */
+public final class BounceNotificationParser extends HttpRequestParser {
+ /**
+ * Parse the POST data of the given request to get details about the bounce notification.
+ *
+ * @param request The {@link HttpServletRequest} whose POST data should be parsed.
+ * @return a BounceNotification
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public static BounceNotification parse(HttpServletRequest request)
+ throws IOException, MessagingException {
+ MimeMultipart multipart = parseMultipartRequest(request);
+
+ BounceNotification.DetailsBuilder originalDetailsBuilder = null;
+ BounceNotification.DetailsBuilder notificationDetailsBuilder = null;
+ BounceNotification.BounceNotificationBuilder bounceNotificationBuilder =
+ new BounceNotification.BounceNotificationBuilder();
+ int parts = multipart.getCount();
+ for (int i = 0; i < parts; i++) {
+ BodyPart part = multipart.getBodyPart(i);
+ String fieldName = getFieldName(part);
+ if ("raw-message".equals(fieldName)) {
+ Session session = Session.getDefaultInstance(new Properties());
+ MimeMessage message = new MimeMessage(session, part.getInputStream());
+ bounceNotificationBuilder.withRawMessage(message);
+ } else {
+ String[] subFields = fieldName.split("-");
+ BounceNotification.DetailsBuilder detailsBuilder = null;
+ if ("original".equals(subFields[0])) {
+ if (originalDetailsBuilder == null) {
+ originalDetailsBuilder = new BounceNotification.DetailsBuilder();
+ }
+ detailsBuilder = originalDetailsBuilder;
+ } else if ("notification".equals(subFields[0])) {
+ if (notificationDetailsBuilder == null) {
+ notificationDetailsBuilder = new BounceNotification.DetailsBuilder();
+ }
+ detailsBuilder = notificationDetailsBuilder;
+ }
+ if (detailsBuilder != null) {
+ String field = subFields[1];
+ String value = getTextContent(part);
+ if ("to".equals(field)) {
+ detailsBuilder.withTo(value);
+ } else if ("from".equals(field)) {
+ detailsBuilder.withFrom(value);
+ } else if ("subject".equals(field)) {
+ detailsBuilder.withSubject(value);
+ } else if ("text".equals(field)) {
+ detailsBuilder.withText(value);
+ } else if ("cc".equals(field)) {
+ detailsBuilder.withCc(value);
+ } else if ("bcc".equals(field)) {
+ detailsBuilder.withBcc(value);
+ }
+ }
+ }
+ }
+
+ if (originalDetailsBuilder != null) {
+ bounceNotificationBuilder.withOriginal(originalDetailsBuilder.build());
+ }
+ if (notificationDetailsBuilder != null) {
+ bounceNotificationBuilder.withNotification(notificationDetailsBuilder.build());
+ }
+ return bounceNotificationBuilder.build();
+ }
+}
diff --git a/api/src/main/java/com/google/appengine/api/search/Cursor.java b/api/src/main/java/com/google/appengine/api/search/Cursor.java
index 7c60b0b43..b9a2e8927 100644
--- a/api/src/main/java/com/google/appengine/api/search/Cursor.java
+++ b/api/src/main/java/com/google/appengine/api/search/Cursor.java
@@ -20,7 +20,7 @@
import com.google.appengine.api.search.proto.SearchServicePb.SearchParams;
import com.google.common.base.Preconditions;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Represents a cursor on the set of results found for executing a {@link Query}
diff --git a/api/src/main/java/com/google/appengine/api/search/Document.java b/api/src/main/java/com/google/appengine/api/search/Document.java
index 0496effd4..2db68bd4a 100644
--- a/api/src/main/java/com/google/appengine/api/search/Document.java
+++ b/api/src/main/java/com/google/appengine/api/search/Document.java
@@ -34,7 +34,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Represents a user generated document. The following example shows how to
diff --git a/api/src/main/java/com/google/appengine/api/search/Facet.java b/api/src/main/java/com/google/appengine/api/search/Facet.java
index a69add370..db5d61605 100644
--- a/api/src/main/java/com/google/appengine/api/search/Facet.java
+++ b/api/src/main/java/com/google/appengine/api/search/Facet.java
@@ -23,7 +23,7 @@
import com.google.common.base.Preconditions;
import java.io.Serializable;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A {@code Facet} can be used to categorize a {@link Document}. It is not a {@link Field}.
diff --git a/api/src/main/java/com/google/appengine/api/search/Field.java b/api/src/main/java/com/google/appengine/api/search/Field.java
index 9c5818d17..7cf6b01af 100644
--- a/api/src/main/java/com/google/appengine/api/search/Field.java
+++ b/api/src/main/java/com/google/appengine/api/search/Field.java
@@ -30,7 +30,7 @@
import java.util.Date;
import java.util.List;
import java.util.Locale;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Represents a field of a {@link Document}, which is a name, an optional locale, and at most one
diff --git a/api/src/main/java/com/google/appengine/api/search/GetIndexesRequest.java b/api/src/main/java/com/google/appengine/api/search/GetIndexesRequest.java
index ef49147ba..472400666 100644
--- a/api/src/main/java/com/google/appengine/api/search/GetIndexesRequest.java
+++ b/api/src/main/java/com/google/appengine/api/search/GetIndexesRequest.java
@@ -20,7 +20,7 @@
import com.google.appengine.api.search.checkers.SearchApiLimits;
import com.google.appengine.api.search.proto.SearchServicePb.ListIndexesParams;
import com.google.common.base.Preconditions;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A request to get a range of indexes. You can specify a number of
diff --git a/api/src/main/java/com/google/appengine/api/search/GetRequest.java b/api/src/main/java/com/google/appengine/api/search/GetRequest.java
index d9a562c34..b403c6a86 100644
--- a/api/src/main/java/com/google/appengine/api/search/GetRequest.java
+++ b/api/src/main/java/com/google/appengine/api/search/GetRequest.java
@@ -19,7 +19,7 @@
import com.google.appengine.api.search.checkers.GetRequestChecker;
import com.google.appengine.api.search.checkers.SearchApiLimits;
import com.google.appengine.api.search.proto.SearchServicePb.ListDocumentsParams;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A request to list objects in an index. You can specify a number of
diff --git a/api/src/main/java/com/google/appengine/api/search/Query.java b/api/src/main/java/com/google/appengine/api/search/Query.java
index 88bd36f43..1cef41da4 100644
--- a/api/src/main/java/com/google/appengine/api/search/Query.java
+++ b/api/src/main/java/com/google/appengine/api/search/Query.java
@@ -22,7 +22,7 @@
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A query to search an index for documents which match,
diff --git a/api/src/main/java/com/google/appengine/api/search/QueryOptions.java b/api/src/main/java/com/google/appengine/api/search/QueryOptions.java
index cb0b1c519..a53709196 100644
--- a/api/src/main/java/com/google/appengine/api/search/QueryOptions.java
+++ b/api/src/main/java/com/google/appengine/api/search/QueryOptions.java
@@ -25,7 +25,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Represents options which control where and what in the search results to return, from restricting
diff --git a/api/src/main/java/com/google/appengine/api/search/ScoredDocument.java b/api/src/main/java/com/google/appengine/api/search/ScoredDocument.java
index b7c95fb76..d856be3c2 100644
--- a/api/src/main/java/com/google/appengine/api/search/ScoredDocument.java
+++ b/api/src/main/java/com/google/appengine/api/search/ScoredDocument.java
@@ -23,7 +23,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Represents a document which may have been scored, possibly
diff --git a/api/src/main/java/com/google/appengine/api/search/SearchApiHelper.java b/api/src/main/java/com/google/appengine/api/search/SearchApiHelper.java
index 462e29953..69db5ff07 100644
--- a/api/src/main/java/com/google/appengine/api/search/SearchApiHelper.java
+++ b/api/src/main/java/com/google/appengine/api/search/SearchApiHelper.java
@@ -26,7 +26,7 @@
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.concurrent.Future;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/** Provides support for translation of calls between userland and appserver land. */
class SearchApiHelper {
diff --git a/api/src/main/java/com/google/appengine/api/search/SortExpression.java b/api/src/main/java/com/google/appengine/api/search/SortExpression.java
index a795e4465..3a4ef2287 100644
--- a/api/src/main/java/com/google/appengine/api/search/SortExpression.java
+++ b/api/src/main/java/com/google/appengine/api/search/SortExpression.java
@@ -20,7 +20,7 @@
import com.google.appengine.api.search.proto.SearchServicePb;
import com.google.common.base.Preconditions;
import java.util.Date;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Sorting specification for a single dimension. Multi-dimensional sorting
diff --git a/api/src/main/java/com/google/appengine/api/search/SortOptions.java b/api/src/main/java/com/google/appengine/api/search/SortOptions.java
index 3dd68d78f..75c2659e0 100644
--- a/api/src/main/java/com/google/appengine/api/search/SortOptions.java
+++ b/api/src/main/java/com/google/appengine/api/search/SortOptions.java
@@ -24,7 +24,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Definition of how to sort documents. You may specify zero or more sort
diff --git a/api/src/main/java/com/google/appengine/api/search/checkers/Preconditions.java b/api/src/main/java/com/google/appengine/api/search/checkers/Preconditions.java
index d7a511543..d83307661 100644
--- a/api/src/main/java/com/google/appengine/api/search/checkers/Preconditions.java
+++ b/api/src/main/java/com/google/appengine/api/search/checkers/Preconditions.java
@@ -16,7 +16,7 @@
package com.google.appengine.api.search.checkers;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Simple static methods to be called at the start of your own methods to verify correct arguments
diff --git a/api/src/main/java/com/google/appengine/api/taskqueue/LeaseOptions.java b/api/src/main/java/com/google/appengine/api/taskqueue/LeaseOptions.java
index 9065a8eec..9d3e71b8a 100644
--- a/api/src/main/java/com/google/appengine/api/taskqueue/LeaseOptions.java
+++ b/api/src/main/java/com/google/appengine/api/taskqueue/LeaseOptions.java
@@ -18,7 +18,7 @@
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Contains various options for lease requests following the builder pattern. Calls to {@link
diff --git a/api/src/main/java/com/google/appengine/api/taskqueue/Queue.java b/api/src/main/java/com/google/appengine/api/taskqueue/Queue.java
index 83dfe9bd8..2d98ac29a 100644
--- a/api/src/main/java/com/google/appengine/api/taskqueue/Queue.java
+++ b/api/src/main/java/com/google/appengine/api/taskqueue/Queue.java
@@ -20,7 +20,7 @@
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@link Queue} is used to manage a task queue.
diff --git a/api/src/main/java/com/google/appengine/api/taskqueue/QueueImpl.java b/api/src/main/java/com/google/appengine/api/taskqueue/QueueImpl.java
index 4b4810de6..d52d67568 100644
--- a/api/src/main/java/com/google/appengine/api/taskqueue/QueueImpl.java
+++ b/api/src/main/java/com/google/appengine/api/taskqueue/QueueImpl.java
@@ -59,7 +59,7 @@
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Implements the {@link Queue} interface. {@link QueueImpl} is thread safe.
diff --git a/api/src/main/java/com/google/appengine/api/taskqueue/TaskHandle.java b/api/src/main/java/com/google/appengine/api/taskqueue/TaskHandle.java
index 5049a4237..91551f631 100644
--- a/api/src/main/java/com/google/appengine/api/taskqueue/TaskHandle.java
+++ b/api/src/main/java/com/google/appengine/api/taskqueue/TaskHandle.java
@@ -24,7 +24,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Created from {@link Queue#add(TaskOptions)}. Contains the task name (generated if otherwise
diff --git a/api/src/main/java/com/google/appengine/api/taskqueue/TaskOptions.java b/api/src/main/java/com/google/appengine/api/taskqueue/TaskOptions.java
index c5c862096..7a14b3660 100644
--- a/api/src/main/java/com/google/appengine/api/taskqueue/TaskOptions.java
+++ b/api/src/main/java/com/google/appengine/api/taskqueue/TaskOptions.java
@@ -37,7 +37,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Contains various options for a task following the builder pattern. Calls to {@link TaskOptions}
diff --git a/api/src/main/java/com/google/appengine/api/taskqueue/ee10/DeferredTaskContext.java b/api/src/main/java/com/google/appengine/api/taskqueue/ee10/DeferredTaskContext.java
index 67411378a..a791d91c2 100644
--- a/api/src/main/java/com/google/appengine/api/taskqueue/ee10/DeferredTaskContext.java
+++ b/api/src/main/java/com/google/appengine/api/taskqueue/ee10/DeferredTaskContext.java
@@ -23,9 +23,10 @@
import java.util.Map;
/**
- * Resources for managing {@link DeferredTask}.
- *
+ * @deprecated as of version 3.0, use {@link
+ * com.google.appengine.api.taskqueue.jakarta.DeferredTaskContext} instead.
*/
+@Deprecated(since = "3.0.0")
public class DeferredTaskContext {
/** The content type of a serialized {@link DeferredTask}. */
public static final String RUNNABLE_TASK_CONTENT_TYPE =
diff --git a/api/src/main/java/com/google/appengine/api/taskqueue/jakarta/DeferredTaskContext.java b/api/src/main/java/com/google/appengine/api/taskqueue/jakarta/DeferredTaskContext.java
new file mode 100644
index 000000000..8cbdc5c09
--- /dev/null
+++ b/api/src/main/java/com/google/appengine/api/taskqueue/jakarta/DeferredTaskContext.java
@@ -0,0 +1,103 @@
+/*
+ * 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.api.taskqueue.jakarta;
+
+import com.google.apphosting.api.ApiProxy;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Map;
+
+/**
+ * Resources for managing {@link DeferredTask}.
+ *
+ */
+public class DeferredTaskContext {
+ /** The content type of a serialized {@link DeferredTask}. */
+ public static final String RUNNABLE_TASK_CONTENT_TYPE =
+ "application/x-binary-app-engine-java-runnable-task";
+
+ /** The URL the DeferredTask servlet is mapped to by default. */
+ public static final String DEFAULT_DEFERRED_URL = "/_ah/queue/__deferred__";
+
+ static final String DEFERRED_TASK_SERVLET_KEY =
+ DeferredTaskContext.class.getName() + ".httpServlet";
+ static final String DEFERRED_TASK_REQUEST_KEY =
+ DeferredTaskContext.class.getName() + ".httpServletRequest";
+ static final String DEFERRED_TASK_RESPONSE_KEY =
+ DeferredTaskContext.class.getName() + ".httpServletResponse";
+ static final String DEFERRED_DO_NOT_RETRY_KEY =
+ DeferredTaskContext.class.getName() + ".doNotRetry";
+ static final String DEFERRED_MARK_RETRY_KEY = DeferredTaskContext.class.getName() + ".markRetry";
+
+ /**
+ * Returns the {@link HttpServlet} instance for the current running deferred task for the current
+ * thread or {@code null} if there is no current deferred task active for this thread.
+ */
+ public static HttpServlet getCurrentServlet() {
+ Map attributes = getCurrentEnvironmentOrThrow().getAttributes();
+ return (HttpServlet) attributes.get(DEFERRED_TASK_SERVLET_KEY);
+ }
+
+ /**
+ * Returns the {@link HttpServletRequest} instance for the current running deferred task for the
+ * current thread or {@code null} if there is no current deferred task active for this thread.
+ */
+ public static HttpServletRequest getCurrentRequest() {
+ Map attributes = getCurrentEnvironmentOrThrow().getAttributes();
+ return (HttpServletRequest) attributes.get(DEFERRED_TASK_REQUEST_KEY);
+ }
+
+ /**
+ * Returns the {@link HttpServletResponse} instance for the current running deferred task for the
+ * current thread or {@code null} if there is no current deferred task active for this thread.
+ */
+ public static HttpServletResponse getCurrentResponse() {
+ Map attributes = getCurrentEnvironmentOrThrow().getAttributes();
+ return (HttpServletResponse) attributes.get(DEFERRED_TASK_RESPONSE_KEY);
+ }
+
+ /**
+ * Sets the action on task failure. Normally when an exception is thrown, the task will be
+ * retried, however if {@code setDoNotRetry} is set to {@code true}, the task will not be retried.
+ */
+ public static void setDoNotRetry(boolean value) {
+ Map attributes = getCurrentEnvironmentOrThrow().getAttributes();
+ attributes.put(DEFERRED_DO_NOT_RETRY_KEY, value);
+ }
+
+ /**
+ * Request a retry of this task, even if an exception was not thrown. If an exception was thrown
+ * and {@link #setDoNotRetry} is set to {@code true} the request will not be retried.
+ */
+ public static void markForRetry() {
+ Map attributes = getCurrentEnvironmentOrThrow().getAttributes();
+ attributes.put(DEFERRED_MARK_RETRY_KEY, true);
+ }
+
+ private static ApiProxy.Environment getCurrentEnvironmentOrThrow() {
+ ApiProxy.Environment environment = ApiProxy.getCurrentEnvironment();
+ if (environment == null) {
+ throw new IllegalStateException(
+ "Operation not allowed in a thread that is neither the original request thread "
+ + "nor a thread created by ThreadManager");
+ }
+ return environment;
+ }
+
+ private DeferredTaskContext() {}
+}
diff --git a/api/src/main/java/com/google/appengine/api/urlfetch/FetchOptions.java b/api/src/main/java/com/google/appengine/api/urlfetch/FetchOptions.java
index e8036174d..ea7301de3 100644
--- a/api/src/main/java/com/google/appengine/api/urlfetch/FetchOptions.java
+++ b/api/src/main/java/com/google/appengine/api/urlfetch/FetchOptions.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.urlfetch;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Allows users to customize the behavior of {@link URLFetchService}
diff --git a/api/src/main/java/com/google/appengine/api/urlfetch/HTTPRequest.java b/api/src/main/java/com/google/appengine/api/urlfetch/HTTPRequest.java
index 40c2cb64a..81fe4e475 100644
--- a/api/src/main/java/com/google/appengine/api/urlfetch/HTTPRequest.java
+++ b/api/src/main/java/com/google/appengine/api/urlfetch/HTTPRequest.java
@@ -21,7 +21,7 @@
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code HTTPRequest} encapsulates a single HTTP request that is made
diff --git a/api/src/main/java/com/google/appengine/api/urlfetch/HTTPResponse.java b/api/src/main/java/com/google/appengine/api/urlfetch/HTTPResponse.java
index 2b02fb333..cec4e2bd1 100644
--- a/api/src/main/java/com/google/appengine/api/urlfetch/HTTPResponse.java
+++ b/api/src/main/java/com/google/appengine/api/urlfetch/HTTPResponse.java
@@ -24,7 +24,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code HTTPResponse} encapsulates the results of a {@code
diff --git a/api/src/main/java/com/google/appengine/api/urlfetch/URLFetchServiceImpl.java b/api/src/main/java/com/google/appengine/api/urlfetch/URLFetchServiceImpl.java
index 80ec4505c..b72f17081 100644
--- a/api/src/main/java/com/google/appengine/api/urlfetch/URLFetchServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/urlfetch/URLFetchServiceImpl.java
@@ -35,7 +35,7 @@
import java.util.concurrent.Future;
import java.util.logging.Logger;
import javax.net.ssl.SSLHandshakeException;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
class URLFetchServiceImpl implements URLFetchService {
static final String PACKAGE = "urlfetch";
diff --git a/api/src/main/java/com/google/appengine/api/users/User.java b/api/src/main/java/com/google/appengine/api/users/User.java
index de7a7ae9e..7646ea78a 100644
--- a/api/src/main/java/com/google/appengine/api/users/User.java
+++ b/api/src/main/java/com/google/appengine/api/users/User.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.users;
import java.io.Serializable;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code User} represents a specific user, represented by the
diff --git a/api/src/main/java/com/google/appengine/api/users/UserService.java b/api/src/main/java/com/google/appengine/api/users/UserService.java
index c8e5e1e32..31c48ce32 100644
--- a/api/src/main/java/com/google/appengine/api/users/UserService.java
+++ b/api/src/main/java/com/google/appengine/api/users/UserService.java
@@ -17,7 +17,7 @@
package com.google.appengine.api.users;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The UserService provides information useful for forcing a user to
diff --git a/api/src/main/java/com/google/appengine/api/users/UserServiceImpl.java b/api/src/main/java/com/google/appengine/api/users/UserServiceImpl.java
index b6c8e4709..d9f2f368c 100644
--- a/api/src/main/java/com/google/appengine/api/users/UserServiceImpl.java
+++ b/api/src/main/java/com/google/appengine/api/users/UserServiceImpl.java
@@ -29,7 +29,7 @@
import com.google.protobuf.MessageLite;
import com.google.protobuf.UninitializedMessageException;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The UserService provides information useful for forcing a user to
diff --git a/api/src/main/java/com/google/appengine/api/utils/FutureWrapper.java b/api/src/main/java/com/google/appengine/api/utils/FutureWrapper.java
index 235132ba7..11a2b17b7 100644
--- a/api/src/main/java/com/google/appengine/api/utils/FutureWrapper.java
+++ b/api/src/main/java/com/google/appengine/api/utils/FutureWrapper.java
@@ -22,7 +22,7 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code FutureWrapper} is a simple {@link Future} that wraps a
diff --git a/api/src/main/java/com/google/appengine/api/utils/SystemProperty.java b/api/src/main/java/com/google/appengine/api/utils/SystemProperty.java
index 25773b51c..87952a044 100644
--- a/api/src/main/java/com/google/appengine/api/utils/SystemProperty.java
+++ b/api/src/main/java/com/google/appengine/api/utils/SystemProperty.java
@@ -16,7 +16,7 @@
package com.google.appengine.api.utils;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Global system properties which are set by App Engine.
diff --git a/api/src/main/java/com/google/appengine/api/utils/jakarta/HttpRequestParser.java b/api/src/main/java/com/google/appengine/api/utils/jakarta/HttpRequestParser.java
new file mode 100644
index 000000000..c138840cc
--- /dev/null
+++ b/api/src/main/java/com/google/appengine/api/utils/jakarta/HttpRequestParser.java
@@ -0,0 +1,150 @@
+/*
+ * 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.api.utils.jakarta;
+
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import javax.activation.DataSource;
+import javax.mail.BodyPart;
+import javax.mail.MessagingException;
+import javax.mail.internet.ContentDisposition;
+import javax.mail.internet.ContentType;
+import javax.mail.internet.MimeMultipart;
+
+/**
+ * {@code HttpRequestParser} encapsulates helper methods used to parse incoming {@code
+ * multipart/form-data} HTTP requests. Subclasses should use these methods to parse specific
+ * requests into useful data structures.
+ *
+ */
+public class HttpRequestParser {
+ /**
+ * Parse input stream of the given request into a MimeMultipart object.
+ *
+ * @params req The HttpServletRequest whose POST data should be parsed.
+ *
+ * @return A MimeMultipart object representing the POST data.
+ *
+ * @throws IOException if the input stream cannot be read.
+ * @throws MessagingException if the input stream cannot be parsed.
+ * @throws IllegalStateException if the request's input stream has already been
+ * read (eg. by calling getReader() or getInputStream()).
+ */
+ protected static MimeMultipart parseMultipartRequest(HttpServletRequest req)
+ throws IOException, MessagingException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ServletInputStream inputStream = req.getInputStream();
+ copy(inputStream, baos);
+ if (baos.size() == 0) {
+ throw new IllegalStateException("Input stream already read, or empty.");
+ }
+
+ return new MimeMultipart(new StaticDataSource(req.getContentType(), baos.toByteArray()));
+ }
+
+ protected static String getFieldName(BodyPart part) throws MessagingException {
+ String[] values = part.getHeader("Content-Disposition");
+ String name = null;
+ if (values != null && values.length > 0) {
+ name = new ContentDisposition(values[0]).getParameter("name");
+ }
+ return (name != null) ? name : "unknown";
+ }
+
+ protected static String getTextContent(BodyPart part) throws MessagingException, IOException {
+ ContentType contentType = new ContentType(part.getContentType());
+ String charset = contentType.getParameter("charset");
+ if (charset == null) {
+ // N.B.: The MIME spec doesn't seem to provide a
+ // default charset, but the default charset for HTTP is
+ // ISO-8859-1. That seems like a reasonable default.
+ charset = "ISO-8859-1";
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ copy(part.getInputStream(), baos);
+ try {
+ return new String(baos.toByteArray(), charset);
+ } catch (UnsupportedEncodingException ex) {
+ return new String(baos.toByteArray());
+ }
+ }
+
+ /**
+ * Copies all bytes from the input stream to the output stream. Does not close or flush either
+ * stream.
+ *
+ * This code is copied from Guava's ByteStreams to avoid direct dependency on the library.
+ * See b/20821034 for details.
+ */
+ private static void copy(InputStream from, OutputStream to) throws IOException {
+ if (from == null) {
+ throw new NullPointerException();
+ }
+ if (to == null) {
+ throw new NullPointerException();
+ }
+ byte[] buf = new byte[8192];
+ while (true) {
+ int r = from.read(buf);
+ if (r == -1) {
+ break;
+ }
+ to.write(buf, 0, r);
+ }
+ }
+
+ /**
+ * A read-only {@link DataSource} backed by a content type and a
+ * fixed byte array.
+ */
+ protected static class StaticDataSource implements DataSource {
+ private final String contentType;
+ private final byte[] bytes;
+
+ public StaticDataSource(String contentType, byte[] bytes) {
+ this.contentType = contentType;
+ this.bytes = bytes;
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getName() {
+ return "request";
+ }
+ }
+}
diff --git a/api/src/main/java/com/google/appengine/tools/compilation/DatastoreCallbacksConfigWriter.java b/api/src/main/java/com/google/appengine/tools/compilation/DatastoreCallbacksConfigWriter.java
index 3b90e586e..f2313fc2a 100644
--- a/api/src/main/java/com/google/appengine/tools/compilation/DatastoreCallbacksConfigWriter.java
+++ b/api/src/main/java/com/google/appengine/tools/compilation/DatastoreCallbacksConfigWriter.java
@@ -35,7 +35,7 @@
import java.util.Map;
import java.util.Properties;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Helper that keeps track of the callbacks we encounter and writes them out in
diff --git a/api/src/main/java/com/google/apphosting/utils/remoteapi/EE10RemoteApiServlet.java b/api/src/main/java/com/google/apphosting/utils/remoteapi/EE10RemoteApiServlet.java
index 8a0bdcba9..04f2cdb1c 100644
--- a/api/src/main/java/com/google/apphosting/utils/remoteapi/EE10RemoteApiServlet.java
+++ b/api/src/main/java/com/google/apphosting/utils/remoteapi/EE10RemoteApiServlet.java
@@ -1,4 +1,3 @@
-
/*
* Copyright 2021 Google LLC
*
@@ -16,484 +15,8 @@
*/
package com.google.apphosting.utils.remoteapi;
-import static com.google.apphosting.datastore.proto2api.DatastoreV3Pb.Error.ErrorCode.BAD_REQUEST;
-import static com.google.apphosting.datastore.proto2api.DatastoreV3Pb.Error.ErrorCode.CONCURRENT_TRANSACTION;
-import static com.google.common.base.Verify.verify;
-
-import com.google.appengine.api.oauth.OAuthRequestException;
-import com.google.appengine.api.oauth.OAuthService;
-import com.google.appengine.api.oauth.OAuthServiceFactory;
-import com.google.appengine.api.users.UserService;
-import com.google.appengine.api.users.UserServiceFactory;
-import com.google.apphosting.api.ApiProxy;
-import com.google.apphosting.base.protos.api.RemoteApiPb.Request;
-import com.google.apphosting.base.protos.api.RemoteApiPb.Response;
-import com.google.apphosting.base.protos.api.RemoteApiPb.TransactionQueryResult;
-import com.google.apphosting.base.protos.api.RemoteApiPb.TransactionRequest;
-import com.google.apphosting.base.protos.api.RemoteApiPb.TransactionRequest.Precondition;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.BeginTransactionRequest;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.DeleteRequest;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.GetRequest;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.GetResponse;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.NextRequest;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.PutRequest;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.Query;
-import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.QueryResult;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistry;
-import com.google.protobuf.Message;
-//
-import com.google.storage.onestore.v3.proto2api.OnestoreEntity;
-import com.google.storage.onestore.v3.proto2api.OnestoreEntity.EntityProto;
-import com.google.storage.onestore.v3.proto2api.OnestoreEntity.Path.Element;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.ObjectOutput;
-import java.io.ObjectOutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.logging.Logger;
-
/**
- * Remote API servlet handler for Jarkata EE APIs.
- *
+ * @deprecated as of version 3.0.0, use {@link JakartaRemoteApiServlet} instead.
*/
-public class EE10RemoteApiServlet extends HttpServlet {
- private static final Logger log = Logger.getLogger(EE10RemoteApiServlet.class.getName());
-
- private static final String[] OAUTH_SCOPES = new String[] {
- "https://www.googleapis.com/auth/appengine.apis",
- "https://www.googleapis.com/auth/cloud-platform",
- };
- private static final String INBOUND_APP_SYSTEM_PROPERTY = "HTTP_X_APPENGINE_INBOUND_APPID";
- private static final String INBOUND_APP_HEADER_NAME = "X-AppEngine-Inbound-AppId";
-
- private HashSet allowedApps = null;
- private final OAuthService oauthService;
-
- public EE10RemoteApiServlet() {
- this(OAuthServiceFactory.getOAuthService());
- }
-
- // @VisibleForTesting
- EE10RemoteApiServlet(OAuthService oauthService) {
- this.oauthService = oauthService;
- }
-
- /** Exception for unknown errors from a Python remote_api handler. */
- public static class UnknownPythonServerException extends RuntimeException {
- public UnknownPythonServerException(String message) {
- super(message);
- }
- }
-
- /**
- * Checks if the inbound request is valid.
- *
- * @param req the {@link HttpServletRequest}
- * @param res the {@link HttpServletResponse}
- * @return true if the application is known.
- */
- boolean checkIsValidRequest(HttpServletRequest req, HttpServletResponse res) throws IOException {
- if (!checkIsKnownInbound(req) && !checkIsAdmin(req, res)) {
- return false;
- }
- return checkIsValidHeader(req, res);
- }
-
- /**
- * Checks if the request is coming from a known application.
- *
- * @param req the {@link HttpServletRequest}
- * @return true if the application is known.
- */
- private synchronized boolean checkIsKnownInbound(HttpServletRequest req) {
- if (allowedApps == null) {
- allowedApps = new HashSet();
- String allowedAppsStr = System.getProperty(INBOUND_APP_SYSTEM_PROPERTY);
- if (allowedAppsStr != null) {
- String[] apps = allowedAppsStr.split(",");
- for (String app : apps) {
- allowedApps.add(app);
- }
- }
- }
- String inboundAppId = req.getHeader(INBOUND_APP_HEADER_NAME);
- return inboundAppId != null && allowedApps.contains(inboundAppId);
- }
-
- /**
- * Checks for the api-version header to prevent XSRF
- *
- * @param req the {@link HttpServletRequest}
- * @param res the {@link HttpServletResponse}
- * @return true if the header exists.
- */
- private boolean checkIsValidHeader(HttpServletRequest req, HttpServletResponse res)
- throws IOException {
- if (req.getHeader("X-appcfg-api-version") == null) {
- res.setStatus(403);
- res.setContentType("text/plain");
- res.getWriter().println("This request did not contain a necessary header");
- return false;
- }
- return true;
- }
-
- /**
- * Check that the current user is signed is with admin access.
- *
- * @return true if the current user is logged in with admin access, false otherwise.
- */
- private boolean checkIsAdmin(HttpServletRequest req, HttpServletResponse res) throws IOException {
- UserService userService = UserServiceFactory.getUserService();
-
- // Check for regular (cookie-based) authentication.
- if (userService.getCurrentUser() != null) {
- if (userService.isUserAdmin()) {
- return true;
- } else {
- respondNotAdmin(res);
- return false;
- }
- }
-
- // Check for OAuth-based authentication.
- try {
- if (oauthService.isUserAdmin(OAUTH_SCOPES)) {
- return true;
- } else {
- respondNotAdmin(res);
- return false;
- }
- } catch (OAuthRequestException e) {
- // Invalid OAuth request; fall through to sending redirect.
- }
-
- res.sendRedirect(userService.createLoginURL(req.getRequestURI()));
- return false;
- }
-
- private void respondNotAdmin(HttpServletResponse res) throws IOException {
- res.setStatus(401);
- res.setContentType("text/plain");
- res.getWriter().println(
- "You must be logged in as an administrator, or access from an approved application.");
- }
-
- /** Serve GET requests with a YAML encoding of the app-id and a validation token. */
- @Override
- public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
- if (!checkIsValidRequest(req, res)) {
- return;
- }
- res.setContentType("text/plain");
- String appId = ApiProxy.getCurrentEnvironment().getAppId();
- StringBuilder outYaml =
- new StringBuilder().append("{rtok: ").append(req.getParameter("rtok")).append(", app_id: ")
- .append(appId).append("}");
- res.getWriter().println(outYaml);
- }
-
- /** Serve POST requests by forwarding calls to ApiProxy. */
- @Override
- public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
- if (!checkIsValidRequest(req, res)) {
- return;
- }
- res.setContentType("application/octet-stream");
- Response.Builder response = Response.newBuilder();
- try {
- byte[] responseData = executeRequest(req);
- response.setResponse(ByteString.copyFrom(responseData));
- res.setStatus(200);
- } catch (Exception e) {
- log.warning("Caught exception while executing remote_api command:\n" + e);
- res.setStatus(200);
- ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
- ObjectOutput out = new ObjectOutputStream(byteStream);
- out.writeObject(e);
- out.close();
- byte[] serializedException = byteStream.toByteArray();
- response.setJavaException(ByteString.copyFrom(serializedException));
- if (e instanceof ApiProxy.ApplicationException) {
- ApiProxy.ApplicationException ae = (ApiProxy.ApplicationException) e;
- response
- .getApplicationErrorBuilder()
- .setCode(ae.getApplicationError())
- .setDetail(ae.getErrorDetail());
- }
- }
- response.build().writeTo(res.getOutputStream());
- }
-
- private byte[] executeRunQuery(Request.Builder request) {
- Query.Builder queryRequest = Query.newBuilder();
- parseFromBytes(queryRequest, request.getRequestIdBytes().toByteArray());
- int batchSize = Math.max(1000, queryRequest.getLimit());
- queryRequest.setCount(batchSize);
- QueryResult.Builder runQueryResponse = QueryResult.newBuilder();
- byte[] res =
- ApiProxy.makeSyncCall("datastore_v3", "RunQuery", request.getRequest().toByteArray());
- parseFromBytes(runQueryResponse, res);
- if (queryRequest.hasLimit()) {
- // Try to pull all results
- while (runQueryResponse.getMoreResults()) {
- NextRequest.Builder nextRequest = NextRequest.newBuilder();
- nextRequest.getCursorBuilder().mergeFrom(runQueryResponse.getCursor());
- nextRequest.setCount(batchSize);
- byte[] nextRes =
- ApiProxy.makeSyncCall("datastore_v3", "Next", nextRequest.build().toByteArray());
- parseFromBytes(runQueryResponse, nextRes);
- }
- }
- return runQueryResponse.build().toByteArray();
- }
-
- private byte[] executeTxQuery(Request.Builder request) {
- TransactionQueryResult.Builder result = TransactionQueryResult.newBuilder();
- Query.Builder query = Query.newBuilder();
- parseFromBytes(query, request.getRequest().toByteArray());
- if (!query.hasAncestor()) {
- throw new ApiProxy.ApplicationException(
- BAD_REQUEST.getNumber(), "No ancestor in transactional query.");
- }
- // Make __entity_group__ key
- OnestoreEntity.Reference.Builder egKey =
- result.getEntityGroupKeyBuilder().mergeFrom(query.getAncestor());
- OnestoreEntity.Path.Element root = egKey.getPath().getElement(0);
- egKey.getPathBuilder().clearElement().addElement(root);
- Element egElement =
- OnestoreEntity.Path.Element.newBuilder().setType("__entity_group__").setId(1).build();
- egKey.getPathBuilder().addElement(egElement);
- // And then perform the transaction with the ancestor query and __entity_group__ fetch.
- byte[] tx = beginTransaction(false);
- parseFromBytes(query.getTransactionBuilder(), tx);
- byte[] queryBytes =
- ApiProxy.makeSyncCall("datastore_v3", "RunQuery", query.build().toByteArray());
- parseFromBytes(result.getResultBuilder(), queryBytes);
- GetRequest.Builder egRequest = GetRequest.newBuilder();
- egRequest.addKey(egKey);
- GetResponse.Builder egResponse = txGet(tx, egRequest);
- if (egResponse.getEntity(0).hasEntity()) {
- result.setEntityGroup(egResponse.getEntity(0).getEntity());
- }
- rollback(tx);
- return result.build().toByteArray();
- }
-
- /**
- * Throws a CONCURRENT_TRANSACTION exception if the entity does not match the precondition.
- */
- private void assertEntityResultMatchesPrecondition(
- GetResponse.Entity entityResult, Precondition precondition) {
- // This handles the case where the Entity was missing in one of the two params.
- if (precondition.hasHash() != entityResult.hasEntity()) {
- throw new ApiProxy.ApplicationException(
- CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition failed");
- }
- if (entityResult.hasEntity()) {
- // Both params have an Entity. Make sure the Entities match using a SHA-1 hash.
- EntityProto entity = entityResult.getEntity();
- if (Arrays.equals(precondition.getHashBytes().toByteArray(), computeSha1(entity))) {
- // They match. We're done.
- return;
- }
- // See javadoc of computeSha1OmittingLastByteForBackwardsCompatibility for explanation.
- byte[] backwardsCompatibleHash = computeSha1OmittingLastByteForBackwardsCompatibility(entity);
- if (!Arrays.equals(precondition.getHashBytes().toByteArray(), backwardsCompatibleHash)) {
- throw new ApiProxy.ApplicationException(
- CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition failed");
- }
- }
- // Else, the Entity was missing from both.
- }
-
- private byte[] executeTx(Request.Builder request) {
- TransactionRequest.Builder txRequest = TransactionRequest.newBuilder();
- parseFromBytes(txRequest, request.getRequest().toByteArray());
- byte[] tx = beginTransaction(txRequest.getAllowMultipleEg());
- List preconditions = txRequest.getPreconditionList();
- // Check transaction preconditions
- if (!preconditions.isEmpty()) {
- GetRequest.Builder getRequest = GetRequest.newBuilder();
- for (Precondition precondition : preconditions) {
- OnestoreEntity.Reference key = precondition.getKey();
- getRequest.addKeyBuilder().mergeFrom(key);
- }
- GetResponse.Builder getResponse = txGet(tx, getRequest);
- List entities = getResponse.getEntityList();
- // Note that this is guaranteed because we don't specify allow_deferred on the GetRequest.
- // TODO: Consider supporting deferred gets here.
- assert (entities.size() == preconditions.size());
- for (int i = 0; i < entities.size(); i++) {
- // Throw an exception if any of the Entities don't match the Precondition specification.
- assertEntityResultMatchesPrecondition(entities.get(i), preconditions.get(i));
- }
- }
- // Preconditions OK.
- // Perform puts.
- byte[] res = new byte[0]; // a serialized VoidProto
- if (txRequest.hasPuts()) {
- PutRequest.Builder putRequest = txRequest.getPutsBuilder();
- parseFromBytes(putRequest.getTransactionBuilder(), tx);
- res = ApiProxy.makeSyncCall("datastore_v3", "Put", putRequest.build().toByteArray());
- }
- // Perform deletes.
- if (txRequest.hasDeletes()) {
- DeleteRequest.Builder deleteRequest = txRequest.getDeletesBuilder();
- parseFromBytes(deleteRequest.getTransactionBuilder(), tx);
- ApiProxy.makeSyncCall("datastore_v3", "Delete", deleteRequest.build().toByteArray());
- }
- // Commit transaction.
- ApiProxy.makeSyncCall("datastore_v3", "Commit", tx);
- return res;
- }
-
- private byte[] executeGetIDs(Request.Builder request, boolean isXg) {
- PutRequest.Builder putRequest = PutRequest.newBuilder();
- parseFromBytes(putRequest, request.getRequest().toByteArray());
- for (EntityProto entity : putRequest.getEntityList()) {
- verify(entity.getPropertyCount() == 0);
- verify(entity.getRawPropertyCount() == 0);
- verify(entity.getEntityGroup().getElementCount() == 0);
- List elementList = entity.getKey().getPath().getElementList();
- Element lastPart = elementList.get(elementList.size() - 1);
- verify(lastPart.getId() == 0);
- verify(!lastPart.hasName());
- }
- // Start a Transaction.
- // TODO: Shouldn't this use allocateIds instead?
- byte[] tx = beginTransaction(isXg);
- parseFromBytes(putRequest.getTransactionBuilder(), tx);
- // Make a put request for a bunch of empty entities with the requisite
- // paths.
- byte[] res = ApiProxy.makeSyncCall("datastore_v3", "Put", putRequest.build().toByteArray());
- // Roll back the transaction so we don't actually insert anything.
- rollback(tx);
- return res;
- }
-
- private byte[] executeRequest(HttpServletRequest req) throws IOException {
- Request.Builder request = Request.newBuilder();
- parseFromInputStream(request, req.getInputStream());
- String service = request.getServiceName();
- String method = request.getMethod();
-
- log.fine("remote API call: " + service + ", " + method);
-
- if (service.equals("remote_datastore")) {
- if (method.equals("RunQuery")) {
- return executeRunQuery(request);
- } else if (method.equals("Transaction")) {
- return executeTx(request);
- } else if (method.equals("TransactionQuery")) {
- return executeTxQuery(request);
- } else if (method.equals("GetIDs")) {
- return executeGetIDs(request, false);
- } else if (method.equals("GetIDsXG")) {
- return executeGetIDs(request, true);
- } else {
- throw new ApiProxy.CallNotFoundException(service, method);
- }
- } else {
- return ApiProxy.makeSyncCall(service, method, request.getRequest().toByteArray());
- }
- }
-
- // Datastore utility functions.
-
- private static byte[] beginTransaction(boolean allowMultipleEg) {
- String appId = ApiProxy.getCurrentEnvironment().getAppId();
- byte[] req =
- BeginTransactionRequest.newBuilder()
- .setApp(appId)
- .setAllowMultipleEg(allowMultipleEg)
- .build()
- .toByteArray();
- return ApiProxy.makeSyncCall("datastore_v3", "BeginTransaction", req);
- }
-
- private static void rollback(byte[] tx) {
- ApiProxy.makeSyncCall("datastore_v3", "Rollback", tx);
- }
-
- private static GetResponse.Builder txGet(byte[] tx, GetRequest.Builder request) {
- parseFromBytes(request.getTransactionBuilder(), tx);
- GetResponse.Builder response = GetResponse.newBuilder();
- byte[] resultBytes = ApiProxy.makeSyncCall("datastore_v3", "Get", request.build().toByteArray());
- parseFromBytes(response, resultBytes);
- return response;
- }
-
- // @VisibleForTesting
- static byte[] computeSha1(EntityProto entity) {
- byte[] entityBytes = entity.toByteArray();
- return computeSha1(entityBytes, entityBytes.length);
- }
-
- /**
- * This is a HACK. There used to be a bug in RemoteDatastore.java in that it would omit the last
- * byte of the Entity when calculating the hash for the Precondition. If an app has not updated
- * that library, we may still receive hashes like this. For backwards compatibility, we'll
- * consider the transaction valid if omitting the last byte of the Entity matches the
- * Precondition.
- */
- // @VisibleForTesting
- static byte[] computeSha1OmittingLastByteForBackwardsCompatibility(EntityProto entity) {
- byte[] entityBytes = entity.toByteArray();
- return computeSha1(entityBytes, entityBytes.length - 1);
- }
-
- //
- private static byte[] computeSha1(byte[] bytes, int length) {
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- throw new ApiProxy.ApplicationException(
- CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition could not be computed");
- }
-
- md.update(bytes, 0, length);
- return md.digest();
- }
-
- private static void parseFromBytes(Message.Builder message, byte[] bytes) {
- boolean parsed = true;
- try {
- message.mergeFrom(bytes, ExtensionRegistry.getEmptyRegistry());
- } catch (IOException e) {
- parsed = false;
- }
- checkParse(message.build(), parsed);
- }
-
- private static void parseFromInputStream(Message.Builder message, InputStream inputStream) {
- boolean parsed = true;
- try {
- message.mergeFrom(inputStream, ExtensionRegistry.getEmptyRegistry());
- } catch (IOException e) {
- parsed = false;
- }
- checkParse(message.build(), parsed);
- }
-
-
- private static void checkParse(Message message, boolean parsed) {
- if (!parsed) {
- throw new ApiProxy.ApiProxyException("Could not parse protobuf");
- }
- List errors = message.findInitializationErrors();
- if (errors != null && !errors.isEmpty()) {
- throw new ApiProxy.ApiProxyException("Could not parse protobuf: " + errors);
- }
- }
-}
+@Deprecated(since = "3.0.0")
+public class EE10RemoteApiServlet extends JakartaRemoteApiServlet {}
diff --git a/api/src/main/java/com/google/apphosting/utils/remoteapi/JakartaRemoteApiServlet.java b/api/src/main/java/com/google/apphosting/utils/remoteapi/JakartaRemoteApiServlet.java
new file mode 100644
index 000000000..536d32a0e
--- /dev/null
+++ b/api/src/main/java/com/google/apphosting/utils/remoteapi/JakartaRemoteApiServlet.java
@@ -0,0 +1,499 @@
+
+/*
+ * 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.apphosting.utils.remoteapi;
+
+import static com.google.apphosting.datastore.proto2api.DatastoreV3Pb.Error.ErrorCode.BAD_REQUEST;
+import static com.google.apphosting.datastore.proto2api.DatastoreV3Pb.Error.ErrorCode.CONCURRENT_TRANSACTION;
+import static com.google.common.base.Verify.verify;
+
+import com.google.appengine.api.oauth.OAuthRequestException;
+import com.google.appengine.api.oauth.OAuthService;
+import com.google.appengine.api.oauth.OAuthServiceFactory;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.base.protos.api.RemoteApiPb.Request;
+import com.google.apphosting.base.protos.api.RemoteApiPb.Response;
+import com.google.apphosting.base.protos.api.RemoteApiPb.TransactionQueryResult;
+import com.google.apphosting.base.protos.api.RemoteApiPb.TransactionRequest;
+import com.google.apphosting.base.protos.api.RemoteApiPb.TransactionRequest.Precondition;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.BeginTransactionRequest;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.DeleteRequest;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.GetRequest;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.GetResponse;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.NextRequest;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.PutRequest;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.Query;
+import com.google.apphosting.datastore.proto2api.DatastoreV3Pb.QueryResult;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+//
+import com.google.storage.onestore.v3.proto2api.OnestoreEntity;
+import com.google.storage.onestore.v3.proto2api.OnestoreEntity.EntityProto;
+import com.google.storage.onestore.v3.proto2api.OnestoreEntity.Path.Element;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Remote API servlet handler for Jarkata EE APIs.
+ *
+ */
+public class JakartaRemoteApiServlet extends HttpServlet {
+ private static final Logger log = Logger.getLogger(JakartaRemoteApiServlet.class.getName());
+
+ private static final String[] OAUTH_SCOPES = new String[] {
+ "https://www.googleapis.com/auth/appengine.apis",
+ "https://www.googleapis.com/auth/cloud-platform",
+ };
+ private static final String INBOUND_APP_SYSTEM_PROPERTY = "HTTP_X_APPENGINE_INBOUND_APPID";
+ private static final String INBOUND_APP_HEADER_NAME = "X-AppEngine-Inbound-AppId";
+
+ private HashSet allowedApps = null;
+ private final OAuthService oauthService;
+
+ public JakartaRemoteApiServlet() {
+ this(OAuthServiceFactory.getOAuthService());
+ }
+
+ // @VisibleForTesting
+ JakartaRemoteApiServlet(OAuthService oauthService) {
+ this.oauthService = oauthService;
+ }
+
+ /** Exception for unknown errors from a Python remote_api handler. */
+ public static class UnknownPythonServerException extends RuntimeException {
+ public UnknownPythonServerException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Checks if the inbound request is valid.
+ *
+ * @param req the {@link HttpServletRequest}
+ * @param res the {@link HttpServletResponse}
+ * @return true if the application is known.
+ */
+ boolean checkIsValidRequest(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ if (!checkIsKnownInbound(req) && !checkIsAdmin(req, res)) {
+ return false;
+ }
+ return checkIsValidHeader(req, res);
+ }
+
+ /**
+ * Checks if the request is coming from a known application.
+ *
+ * @param req the {@link HttpServletRequest}
+ * @return true if the application is known.
+ */
+ private synchronized boolean checkIsKnownInbound(HttpServletRequest req) {
+ if (allowedApps == null) {
+ allowedApps = new HashSet();
+ String allowedAppsStr = System.getProperty(INBOUND_APP_SYSTEM_PROPERTY);
+ if (allowedAppsStr != null) {
+ String[] apps = allowedAppsStr.split(",");
+ for (String app : apps) {
+ allowedApps.add(app);
+ }
+ }
+ }
+ String inboundAppId = req.getHeader(INBOUND_APP_HEADER_NAME);
+ return inboundAppId != null && allowedApps.contains(inboundAppId);
+ }
+
+ /**
+ * Checks for the api-version header to prevent XSRF
+ *
+ * @param req the {@link HttpServletRequest}
+ * @param res the {@link HttpServletResponse}
+ * @return true if the header exists.
+ */
+ private boolean checkIsValidHeader(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ if (req.getHeader("X-appcfg-api-version") == null) {
+ res.setStatus(403);
+ res.setContentType("text/plain");
+ res.getWriter().println("This request did not contain a necessary header");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check that the current user is signed is with admin access.
+ *
+ * @return true if the current user is logged in with admin access, false otherwise.
+ */
+ private boolean checkIsAdmin(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ UserService userService = UserServiceFactory.getUserService();
+
+ // Check for regular (cookie-based) authentication.
+ if (userService.getCurrentUser() != null) {
+ if (userService.isUserAdmin()) {
+ return true;
+ } else {
+ respondNotAdmin(res);
+ return false;
+ }
+ }
+
+ // Check for OAuth-based authentication.
+ try {
+ if (oauthService.isUserAdmin(OAUTH_SCOPES)) {
+ return true;
+ } else {
+ respondNotAdmin(res);
+ return false;
+ }
+ } catch (OAuthRequestException e) {
+ // Invalid OAuth request; fall through to sending redirect.
+ }
+
+ res.sendRedirect(userService.createLoginURL(req.getRequestURI()));
+ return false;
+ }
+
+ private void respondNotAdmin(HttpServletResponse res) throws IOException {
+ res.setStatus(401);
+ res.setContentType("text/plain");
+ res.getWriter().println(
+ "You must be logged in as an administrator, or access from an approved application.");
+ }
+
+ /** Serve GET requests with a YAML encoding of the app-id and a validation token. */
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ if (!checkIsValidRequest(req, res)) {
+ return;
+ }
+ res.setContentType("text/plain");
+ String appId = ApiProxy.getCurrentEnvironment().getAppId();
+ StringBuilder outYaml =
+ new StringBuilder().append("{rtok: ").append(req.getParameter("rtok")).append(", app_id: ")
+ .append(appId).append("}");
+ res.getWriter().println(outYaml);
+ }
+
+ /** Serve POST requests by forwarding calls to ApiProxy. */
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ if (!checkIsValidRequest(req, res)) {
+ return;
+ }
+ res.setContentType("application/octet-stream");
+ Response.Builder response = Response.newBuilder();
+ try {
+ byte[] responseData = executeRequest(req);
+ response.setResponse(ByteString.copyFrom(responseData));
+ res.setStatus(200);
+ } catch (Exception e) {
+ log.warning("Caught exception while executing remote_api command:\n" + e);
+ res.setStatus(200);
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ ObjectOutput out = new ObjectOutputStream(byteStream);
+ out.writeObject(e);
+ out.close();
+ byte[] serializedException = byteStream.toByteArray();
+ response.setJavaException(ByteString.copyFrom(serializedException));
+ if (e instanceof ApiProxy.ApplicationException) {
+ ApiProxy.ApplicationException ae = (ApiProxy.ApplicationException) e;
+ response
+ .getApplicationErrorBuilder()
+ .setCode(ae.getApplicationError())
+ .setDetail(ae.getErrorDetail());
+ }
+ }
+ response.build().writeTo(res.getOutputStream());
+ }
+
+ private byte[] executeRunQuery(Request.Builder request) {
+ Query.Builder queryRequest = Query.newBuilder();
+ parseFromBytes(queryRequest, request.getRequestIdBytes().toByteArray());
+ int batchSize = Math.max(1000, queryRequest.getLimit());
+ queryRequest.setCount(batchSize);
+ QueryResult.Builder runQueryResponse = QueryResult.newBuilder();
+ byte[] res =
+ ApiProxy.makeSyncCall("datastore_v3", "RunQuery", request.getRequest().toByteArray());
+ parseFromBytes(runQueryResponse, res);
+ if (queryRequest.hasLimit()) {
+ // Try to pull all results
+ while (runQueryResponse.getMoreResults()) {
+ NextRequest.Builder nextRequest = NextRequest.newBuilder();
+ nextRequest.getCursorBuilder().mergeFrom(runQueryResponse.getCursor());
+ nextRequest.setCount(batchSize);
+ byte[] nextRes =
+ ApiProxy.makeSyncCall("datastore_v3", "Next", nextRequest.build().toByteArray());
+ parseFromBytes(runQueryResponse, nextRes);
+ }
+ }
+ return runQueryResponse.build().toByteArray();
+ }
+
+ private byte[] executeTxQuery(Request.Builder request) {
+ TransactionQueryResult.Builder result = TransactionQueryResult.newBuilder();
+ Query.Builder query = Query.newBuilder();
+ parseFromBytes(query, request.getRequest().toByteArray());
+ if (!query.hasAncestor()) {
+ throw new ApiProxy.ApplicationException(
+ BAD_REQUEST.getNumber(), "No ancestor in transactional query.");
+ }
+ // Make __entity_group__ key
+ OnestoreEntity.Reference.Builder egKey =
+ result.getEntityGroupKeyBuilder().mergeFrom(query.getAncestor());
+ OnestoreEntity.Path.Element root = egKey.getPath().getElement(0);
+ egKey.getPathBuilder().clearElement().addElement(root);
+ Element egElement =
+ OnestoreEntity.Path.Element.newBuilder().setType("__entity_group__").setId(1).build();
+ egKey.getPathBuilder().addElement(egElement);
+ // And then perform the transaction with the ancestor query and __entity_group__ fetch.
+ byte[] tx = beginTransaction(false);
+ parseFromBytes(query.getTransactionBuilder(), tx);
+ byte[] queryBytes =
+ ApiProxy.makeSyncCall("datastore_v3", "RunQuery", query.build().toByteArray());
+ parseFromBytes(result.getResultBuilder(), queryBytes);
+ GetRequest.Builder egRequest = GetRequest.newBuilder();
+ egRequest.addKey(egKey);
+ GetResponse.Builder egResponse = txGet(tx, egRequest);
+ if (egResponse.getEntity(0).hasEntity()) {
+ result.setEntityGroup(egResponse.getEntity(0).getEntity());
+ }
+ rollback(tx);
+ return result.build().toByteArray();
+ }
+
+ /**
+ * Throws a CONCURRENT_TRANSACTION exception if the entity does not match the precondition.
+ */
+ private void assertEntityResultMatchesPrecondition(
+ GetResponse.Entity entityResult, Precondition precondition) {
+ // This handles the case where the Entity was missing in one of the two params.
+ if (precondition.hasHash() != entityResult.hasEntity()) {
+ throw new ApiProxy.ApplicationException(
+ CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition failed");
+ }
+ if (entityResult.hasEntity()) {
+ // Both params have an Entity. Make sure the Entities match using a SHA-1 hash.
+ EntityProto entity = entityResult.getEntity();
+ if (Arrays.equals(precondition.getHashBytes().toByteArray(), computeSha1(entity))) {
+ // They match. We're done.
+ return;
+ }
+ // See javadoc of computeSha1OmittingLastByteForBackwardsCompatibility for explanation.
+ byte[] backwardsCompatibleHash = computeSha1OmittingLastByteForBackwardsCompatibility(entity);
+ if (!Arrays.equals(precondition.getHashBytes().toByteArray(), backwardsCompatibleHash)) {
+ throw new ApiProxy.ApplicationException(
+ CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition failed");
+ }
+ }
+ // Else, the Entity was missing from both.
+ }
+
+ private byte[] executeTx(Request.Builder request) {
+ TransactionRequest.Builder txRequest = TransactionRequest.newBuilder();
+ parseFromBytes(txRequest, request.getRequest().toByteArray());
+ byte[] tx = beginTransaction(txRequest.getAllowMultipleEg());
+ List preconditions = txRequest.getPreconditionList();
+ // Check transaction preconditions
+ if (!preconditions.isEmpty()) {
+ GetRequest.Builder getRequest = GetRequest.newBuilder();
+ for (Precondition precondition : preconditions) {
+ OnestoreEntity.Reference key = precondition.getKey();
+ getRequest.addKeyBuilder().mergeFrom(key);
+ }
+ GetResponse.Builder getResponse = txGet(tx, getRequest);
+ List entities = getResponse.getEntityList();
+ // Note that this is guaranteed because we don't specify allow_deferred on the GetRequest.
+ // TODO: Consider supporting deferred gets here.
+ assert (entities.size() == preconditions.size());
+ for (int i = 0; i < entities.size(); i++) {
+ // Throw an exception if any of the Entities don't match the Precondition specification.
+ assertEntityResultMatchesPrecondition(entities.get(i), preconditions.get(i));
+ }
+ }
+ // Preconditions OK.
+ // Perform puts.
+ byte[] res = new byte[0]; // a serialized VoidProto
+ if (txRequest.hasPuts()) {
+ PutRequest.Builder putRequest = txRequest.getPutsBuilder();
+ parseFromBytes(putRequest.getTransactionBuilder(), tx);
+ res = ApiProxy.makeSyncCall("datastore_v3", "Put", putRequest.build().toByteArray());
+ }
+ // Perform deletes.
+ if (txRequest.hasDeletes()) {
+ DeleteRequest.Builder deleteRequest = txRequest.getDeletesBuilder();
+ parseFromBytes(deleteRequest.getTransactionBuilder(), tx);
+ ApiProxy.makeSyncCall("datastore_v3", "Delete", deleteRequest.build().toByteArray());
+ }
+ // Commit transaction.
+ ApiProxy.makeSyncCall("datastore_v3", "Commit", tx);
+ return res;
+ }
+
+ private byte[] executeGetIDs(Request.Builder request, boolean isXg) {
+ PutRequest.Builder putRequest = PutRequest.newBuilder();
+ parseFromBytes(putRequest, request.getRequest().toByteArray());
+ for (EntityProto entity : putRequest.getEntityList()) {
+ verify(entity.getPropertyCount() == 0);
+ verify(entity.getRawPropertyCount() == 0);
+ verify(entity.getEntityGroup().getElementCount() == 0);
+ List elementList = entity.getKey().getPath().getElementList();
+ Element lastPart = elementList.get(elementList.size() - 1);
+ verify(lastPart.getId() == 0);
+ verify(!lastPart.hasName());
+ }
+ // Start a Transaction.
+ // TODO: Shouldn't this use allocateIds instead?
+ byte[] tx = beginTransaction(isXg);
+ parseFromBytes(putRequest.getTransactionBuilder(), tx);
+ // Make a put request for a bunch of empty entities with the requisite
+ // paths.
+ byte[] res = ApiProxy.makeSyncCall("datastore_v3", "Put", putRequest.build().toByteArray());
+ // Roll back the transaction so we don't actually insert anything.
+ rollback(tx);
+ return res;
+ }
+
+ private byte[] executeRequest(HttpServletRequest req) throws IOException {
+ Request.Builder request = Request.newBuilder();
+ parseFromInputStream(request, req.getInputStream());
+ String service = request.getServiceName();
+ String method = request.getMethod();
+
+ log.fine("remote API call: " + service + ", " + method);
+
+ if (service.equals("remote_datastore")) {
+ if (method.equals("RunQuery")) {
+ return executeRunQuery(request);
+ } else if (method.equals("Transaction")) {
+ return executeTx(request);
+ } else if (method.equals("TransactionQuery")) {
+ return executeTxQuery(request);
+ } else if (method.equals("GetIDs")) {
+ return executeGetIDs(request, false);
+ } else if (method.equals("GetIDsXG")) {
+ return executeGetIDs(request, true);
+ } else {
+ throw new ApiProxy.CallNotFoundException(service, method);
+ }
+ } else {
+ return ApiProxy.makeSyncCall(service, method, request.getRequest().toByteArray());
+ }
+ }
+
+ // Datastore utility functions.
+
+ private static byte[] beginTransaction(boolean allowMultipleEg) {
+ String appId = ApiProxy.getCurrentEnvironment().getAppId();
+ byte[] req =
+ BeginTransactionRequest.newBuilder()
+ .setApp(appId)
+ .setAllowMultipleEg(allowMultipleEg)
+ .build()
+ .toByteArray();
+ return ApiProxy.makeSyncCall("datastore_v3", "BeginTransaction", req);
+ }
+
+ private static void rollback(byte[] tx) {
+ ApiProxy.makeSyncCall("datastore_v3", "Rollback", tx);
+ }
+
+ private static GetResponse.Builder txGet(byte[] tx, GetRequest.Builder request) {
+ parseFromBytes(request.getTransactionBuilder(), tx);
+ GetResponse.Builder response = GetResponse.newBuilder();
+ byte[] resultBytes = ApiProxy.makeSyncCall("datastore_v3", "Get", request.build().toByteArray());
+ parseFromBytes(response, resultBytes);
+ return response;
+ }
+
+ // @VisibleForTesting
+ static byte[] computeSha1(EntityProto entity) {
+ byte[] entityBytes = entity.toByteArray();
+ return computeSha1(entityBytes, entityBytes.length);
+ }
+
+ /**
+ * This is a HACK. There used to be a bug in RemoteDatastore.java in that it would omit the last
+ * byte of the Entity when calculating the hash for the Precondition. If an app has not updated
+ * that library, we may still receive hashes like this. For backwards compatibility, we'll
+ * consider the transaction valid if omitting the last byte of the Entity matches the
+ * Precondition.
+ */
+ // @VisibleForTesting
+ static byte[] computeSha1OmittingLastByteForBackwardsCompatibility(EntityProto entity) {
+ byte[] entityBytes = entity.toByteArray();
+ return computeSha1(entityBytes, entityBytes.length - 1);
+ }
+
+ //
+ private static byte[] computeSha1(byte[] bytes, int length) {
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new ApiProxy.ApplicationException(
+ CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition could not be computed");
+ }
+
+ md.update(bytes, 0, length);
+ return md.digest();
+ }
+
+ private static void parseFromBytes(Message.Builder message, byte[] bytes) {
+ boolean parsed = true;
+ try {
+ message.mergeFrom(bytes, ExtensionRegistry.getEmptyRegistry());
+ } catch (IOException e) {
+ parsed = false;
+ }
+ checkParse(message.build(), parsed);
+ }
+
+ private static void parseFromInputStream(Message.Builder message, InputStream inputStream) {
+ boolean parsed = true;
+ try {
+ message.mergeFrom(inputStream, ExtensionRegistry.getEmptyRegistry());
+ } catch (IOException e) {
+ parsed = false;
+ }
+ checkParse(message.build(), parsed);
+ }
+
+
+ private static void checkParse(Message message, boolean parsed) {
+ if (!parsed) {
+ throw new ApiProxy.ApiProxyException("Could not parse protobuf");
+ }
+ List errors = message.findInitializationErrors();
+ if (errors != null && !errors.isEmpty()) {
+ throw new ApiProxy.ApiProxyException("Could not parse protobuf: " + errors);
+ }
+ }
+}
diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/DeferredTaskServlet.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/DeferredTaskServlet.java
index 8be000f27..101d64d58 100644
--- a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/DeferredTaskServlet.java
+++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/DeferredTaskServlet.java
@@ -16,222 +16,10 @@
package com.google.apphosting.utils.servlet.ee10;
-import com.google.appengine.api.taskqueue.DeferredTask;
-import com.google.appengine.api.taskqueue.ee10.DeferredTaskContext;
-import com.google.apphosting.api.ApiProxy;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletInputStream;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectStreamClass;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.Proxy;
-import java.net.HttpURLConnection;
-import java.util.Map;
-
/**
- * Implementation of {@link HttpServlet} to dispatch tasks with a {@link DeferredTask} payload; see
- * {@link com.google.appengine.api.taskqueue.TaskOptions#payload(DeferredTask)}.
- *
- *
This servlet is mapped to {@link DeferredTaskContext#DEFAULT_DEFERRED_URL} by default. Below
- * is a snippet of the web.xml configuration.
- *
- *
- *
+ * @deprecated as of version 3.0, use {@link
+ * com.google.apphosting.utils.servlet.jakarta.DeferredTaskServlet} instead.
*/
-public class DeferredTaskServlet extends HttpServlet {
- // Keep this in sync with X_APPENGINE_QUEUENAME and
- // in google3/apphosting/base/http_proto.cc
- static final String X_APPENGINE_QUEUENAME = "X-AppEngine-QueueName";
-
- static final String DEFERRED_TASK_SERVLET_KEY =
- DeferredTaskContext.class.getName() + ".httpServlet";
- static final String DEFERRED_TASK_REQUEST_KEY =
- DeferredTaskContext.class.getName() + ".httpServletRequest";
- static final String DEFERRED_TASK_RESPONSE_KEY =
- DeferredTaskContext.class.getName() + ".httpServletResponse";
- static final String DEFERRED_DO_NOT_RETRY_KEY =
- DeferredTaskContext.class.getName() + ".doNotRetry";
- static final String DEFERRED_MARK_RETRY_KEY = DeferredTaskContext.class.getName() + ".markRetry";
-
- /** Thrown by readRequest when an error occurred during deserialization. */
- protected static class DeferredTaskException extends Exception {
- public DeferredTaskException(Exception e) {
- super(e);
- }
- }
-
- @Override
- protected void service(HttpServletRequest req, HttpServletResponse resp)
- throws ServletException, IOException {
- // See http://b/3479189. All task queue requests have the X-AppEngine-QueueName
- // header set. Non admin users cannot set this header so it's a signal that
- // this came from task queue or an admin smart enough to set the header.
- if (req.getHeader(X_APPENGINE_QUEUENAME) == null) {
- resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Not a taskqueue request.");
- return;
- }
-
- String method = req.getMethod();
- if (!method.equals("POST")) {
- String protocol = req.getProtocol();
- String msg = "DeferredTaskServlet does not support method: " + method;
- if (protocol.endsWith("1.1")) {
- resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
- } else {
- resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
- }
- return;
- }
-
- // Place the current servlet, request and response in the environment for
- // situations where the task may need to get to it.
- Map attributes = ApiProxy.getCurrentEnvironment().getAttributes();
- attributes.put(DEFERRED_TASK_SERVLET_KEY, this);
- attributes.put(DEFERRED_TASK_REQUEST_KEY, req);
- attributes.put(DEFERRED_TASK_RESPONSE_KEY, resp);
- attributes.put(DEFERRED_MARK_RETRY_KEY, false);
-
- try {
- performRequest(req, resp);
- if ((Boolean) attributes.get(DEFERRED_MARK_RETRY_KEY)) {
- resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
- } else {
- resp.setStatus(HttpURLConnection.HTTP_OK);
- }
- } catch (DeferredTaskException e) {
- resp.setStatus(HttpURLConnection.HTTP_UNSUPPORTED_TYPE);
- log("Deferred task failed exception: " + e);
- return;
- } catch (RuntimeException e) {
- Boolean doNotRetry = (Boolean) attributes.get(DEFERRED_DO_NOT_RETRY_KEY);
- if (doNotRetry == null || !doNotRetry) {
- throw new ServletException(e);
- } else if (doNotRetry) {
- resp.setStatus(HttpURLConnection.HTTP_NOT_AUTHORITATIVE); // Alternate success code.
- log(
- DeferredTaskServlet.class.getName()
- + " - Deferred task failed but doNotRetry specified. Exception: "
- + e);
- }
- } finally {
- // Clean out the attributes.
- attributes.remove(DEFERRED_TASK_SERVLET_KEY);
- attributes.remove(DEFERRED_TASK_REQUEST_KEY);
- attributes.remove(DEFERRED_TASK_RESPONSE_KEY);
- attributes.remove(DEFERRED_DO_NOT_RETRY_KEY);
- }
- }
-
- /**
- * Performs a task enqueued with {@link TaskOptions#payload(DeferredTask)} by deserializing the
- * input stream of the {@link HttpServletRequest}.
- *
- * @param req The HTTP request.
- * @param resp The HTTP response.
- * @throws DeferredTaskException If an error occurred while deserializing the task.
- *
Note that other exceptions may be thrown by the {@link DeferredTask#run()} method.
- */
- protected void performRequest(HttpServletRequest req, HttpServletResponse resp)
- throws DeferredTaskException {
- readRequest(req, resp).run();
- }
-
- /**
- * De-serializes the {@link DeferredTask} object from the input stream.
- *
- * @throws DeferredTaskException With the chained exception being one of the following:
- *
{@link IllegalArgumentException}: Indicates a content-type header mismatch.
- *
{@link ClassCastException}: Deserialization failure.
- */
- protected Runnable readRequest(HttpServletRequest req, HttpServletResponse resp)
- throws DeferredTaskException {
- String contentType = req.getHeader("content-type");
- if (contentType == null
- || !contentType.equals(DeferredTaskContext.RUNNABLE_TASK_CONTENT_TYPE)) {
- throw new DeferredTaskException(
- new IllegalArgumentException(
- "Invalid content-type header."
- + " received: '"
- + (String.valueOf(contentType))
- + "' expected: '"
- + DeferredTaskContext.RUNNABLE_TASK_CONTENT_TYPE
- + "'"));
- }
-
- try {
- ServletInputStream stream = req.getInputStream();
- ObjectInputStream objectStream =
- new ObjectInputStream(stream) {
- @Override
- protected Class> resolveClass(ObjectStreamClass desc)
- throws IOException, ClassNotFoundException {
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
- String name = desc.getName();
- try {
- return Class.forName(name, false, classLoader);
- } catch (ClassNotFoundException ex) {
- // This one should also handle primitive types
- return super.resolveClass(desc);
- }
- }
-
- @Override
- protected Class> resolveProxyClass(String[] interfaces)
- throws IOException, ClassNotFoundException {
- // Note This logic was copied from ObjectInputStream.java in the
- // JDK, and then modified to use the thread context class loader instead of the
- // "latest" loader that is used there.
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
- ClassLoader nonPublicLoader = null;
- boolean hasNonPublicInterface = false;
-
- // define proxy in class loader of non-public interface(s), if any
- Class>[] classObjs = new Class>[interfaces.length];
- for (int i = 0; i < interfaces.length; i++) {
- Class> cl = Class.forName(interfaces[i], false, classLoader);
- if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
- if (hasNonPublicInterface) {
- if (nonPublicLoader != cl.getClassLoader()) {
- throw new IllegalAccessError(
- "conflicting non-public interface class loaders");
- }
- } else {
- nonPublicLoader = cl.getClassLoader();
- hasNonPublicInterface = true;
- }
- }
- classObjs[i] = cl;
- }
- try {
- return Proxy.getProxyClass(
- hasNonPublicInterface ? nonPublicLoader : classLoader, classObjs);
- } catch (IllegalArgumentException e) {
- throw new ClassNotFoundException(null, e);
- }
- }
- };
- // Replacing DeferredTask to Runnable as we have DeferredTask in the 2 classloaders
- // (runtime and application), but we cannot cast one with another one.
- return (Runnable) objectStream.readObject();
- } catch (ClassNotFoundException | IOException | ClassCastException e) {
- throw new DeferredTaskException(e);
- }
- }
-}
+@Deprecated(since = "3.0.0")
+public class DeferredTaskServlet
+ extends com.google.apphosting.utils.servlet.jakarta.DeferredTaskServlet {}
diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/JdbcMySqlConnectionCleanupFilter.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/JdbcMySqlConnectionCleanupFilter.java
index a1b6ea897..8430df85f 100644
--- a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/JdbcMySqlConnectionCleanupFilter.java
+++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/JdbcMySqlConnectionCleanupFilter.java
@@ -16,184 +16,10 @@
package com.google.apphosting.utils.servlet.ee10;
-import com.google.apphosting.api.ApiProxy;
-import com.google.apphosting.api.ApiProxy.Environment;
-import jakarta.servlet.Filter;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.FilterConfig;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.ServletResponse;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
/**
- * Filter to cleanup any SQL connections that were opened but not closed during the
- * HTTP-request processing.
+ * @deprecated as of version 3.0, use {@link
+ * com.google.apphosting.utils.servlet.jakarta.JdbcMySqlConnectionCleanupFilter} instead.
*/
-public class JdbcMySqlConnectionCleanupFilter implements Filter {
-
- private static final Logger logger = Logger.getLogger(
- JdbcMySqlConnectionCleanupFilter.class.getCanonicalName());
-
- /**
- * The key for looking up the feature on/off flag.
- */
- static final String CLOUD_SQL_JDBC_CONNECTIVITY_ENABLED_KEY =
- "com.google.appengine.runtime.new_database_connectivity";
-
- private final AppEngineApiWrapper appEngineApiWrapper;
-
- private final ConnectionsCleanupWrapper connectionsCleanupWrapper;
-
- private static final String THROW_ERROR_VARIABLE_NAME = "THROW_ERROR_ON_SQL_CLOSE_ERROR";
- private static final String ABANDONED_CONNECTIONS_CLASSNAME =
- "com.mysql.jdbc.AbandonedConnections";
-
- public JdbcMySqlConnectionCleanupFilter() {
- appEngineApiWrapper = new AppEngineApiWrapper();
- connectionsCleanupWrapper = new ConnectionsCleanupWrapper();
- }
-
- // Visible for testing.
- JdbcMySqlConnectionCleanupFilter(
- AppEngineApiWrapper appEngineApiWrapper,
- ConnectionsCleanupWrapper connectionsCleanupWrapper) {
- this.appEngineApiWrapper = appEngineApiWrapper;
- this.connectionsCleanupWrapper = connectionsCleanupWrapper;
- }
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- // Do Nothing.
- }
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- try {
- chain.doFilter(request, response);
- } finally {
- cleanupConnections();
- }
- }
-
- /**
- * Cleanup any SQL connection that was opened but not closed during the HTTP-request processing.
- */
- void cleanupConnections() {
- Map attributes = appEngineApiWrapper.getRequestEnvironmentAttributes();
- if (attributes == null) {
- return;
- }
-
- Object cloudSqlJdbcConnectivityEnabledValue =
- attributes.get(CLOUD_SQL_JDBC_CONNECTIVITY_ENABLED_KEY);
- if (!(cloudSqlJdbcConnectivityEnabledValue instanceof Boolean)) {
- return;
- }
-
- if (!((Boolean) cloudSqlJdbcConnectivityEnabledValue)) {
- // Act as no-op if the flag indicated by CLOUD_SQL_JDBC_CONNECTIVITY_ENABLED_KEY is false.
- return;
- }
-
- try {
- connectionsCleanupWrapper.cleanup();
- } catch (Exception e) {
- logger.log(Level.WARNING, "Unable to cleanup connections", e);
- if (Boolean.getBoolean(THROW_ERROR_VARIABLE_NAME)) {
- throw new IllegalStateException(e);
- }
- }
- }
-
- @Override
- public void destroy() {
- // Do Nothing.
- }
-
- /**
- * Wrapper for ApiProxy static methods.
- * Refactored for testability.
- */
- static class AppEngineApiWrapper {
- /**
- * Utility method that fetches back the attributes map for the HTTP-request being processed.
- *
- * @return The environment attribute map for the current HTTP request, or null if unable to
- * fetch the map
- */
- Map getRequestEnvironmentAttributes() {
- // Check for the current request environment.
- Environment environment = ApiProxy.getCurrentEnvironment();
- if (environment == null) {
- logger.warning("Unable to fetch the request environment.");
- return null;
- }
-
- // Get the environment attributes.
- Map attributes = environment.getAttributes();
- if (attributes == null) {
- logger.warning("Unable to fetch the request environment attributes.");
- return null;
- }
-
- return attributes;
- }
- }
-
- /**
- * Wrapper for the connections cleanup method.
- * Refactored for testability.
- */
- static class ConnectionsCleanupWrapper {
- /**
- * Abandoned connections cleanup method cache.
- */
- private static Method cleanupMethod;
- private static boolean cleanupMethodInitializationAttempted;
-
- void cleanup() throws Exception {
- synchronized (ConnectionsCleanupWrapper.class) {
- // Due to cr/50477083 the cleanup method was invoked by the applications that do
- // not have the native connectivity enabled. For such applications the filter raised
- // ClassNotFound exception when returning a class object associated with the
- // "com.mysql.jdbc.AbandonedConnections" class. By design this class is not loaded for
- // such applications. The exception was logged as warning and polluted the logs.
- //
- // As a quick fix; we ensure that the initialization for cleanupMethod is attempted
- // only once, avoiding exceptions being raised for every request in case of
- // applications mentioned above. We also suppress the ClassNotFound exception that
- // would be raised for such applications thereby not polluting the logs.
- // For the applications having native connectivity enabled the servlet filter would
- // work as expected.
- //
- // As a long term fix we need to use the "use-google-connector-j" flag that user sets
- // in the appengine-web.xml to decide if we should make an early return from the filter.
- if (!cleanupMethodInitializationAttempted) {
- try {
- if (cleanupMethod == null) {
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- cleanupMethod =
- (loader == null
- ? Class.forName(ABANDONED_CONNECTIONS_CLASSNAME)
- : loader.loadClass(ABANDONED_CONNECTIONS_CLASSNAME))
- .getDeclaredMethod("cleanup");
- }
- } catch (ClassNotFoundException e) {
- // Do nothing.
- } finally {
- cleanupMethodInitializationAttempted = true;
- }
- }
- }
- if (cleanupMethod != null) {
- cleanupMethod.invoke(null);
- }
- }
- }
-}
+@Deprecated(since = "3.0.0")
+public class JdbcMySqlConnectionCleanupFilter
+ extends com.google.apphosting.utils.servlet.jakarta.JdbcMySqlConnectionCleanupFilter {}
\ No newline at end of file
diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/MultipartMimeUtils.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/MultipartMimeUtils.java
index 8c0a0aa9b..f812c258d 100644
--- a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/MultipartMimeUtils.java
+++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/MultipartMimeUtils.java
@@ -16,115 +16,10 @@
package com.google.apphosting.utils.servlet.ee10;
-import com.google.common.io.ByteStreams;
-import jakarta.servlet.http.HttpServletRequest;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import javax.activation.DataSource;
-import javax.mail.BodyPart;
-import javax.mail.MessagingException;
-import javax.mail.internet.ContentDisposition;
-import javax.mail.internet.ContentType;
-import javax.mail.internet.MimeMultipart;
-
/**
- * {@code MultipartMimeUtils} is a collection of static utility clases
- * that facilitate the parsing of multipart/form-data and
- * multipart/mixed requests using the {@link MimeMultipart} class
- * provided by JavaMail.
- *
+ * @deprecated as of version 3.0, use {@link
+ * com.google.apphosting.utils.servlet.jakarta.MultipartMimeUtils} instead.
*/
-public class MultipartMimeUtils {
- /**
- * Parse the request body and return a {@link MimeMultipart}
- * representing the request.
- */
- public static MimeMultipart parseMultipartRequest(HttpServletRequest req)
- throws IOException, MessagingException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ByteStreams.copy(req.getInputStream(), baos);
-
- return new MimeMultipart(createDataSource(req.getContentType(), baos.toByteArray()));
- }
-
- /**
- * Create a read-only {@link DataSource} with the specific content type and body.
- */
- public static DataSource createDataSource(String contentType, byte[] data) {
- return new StaticDataSource(contentType, data);
- }
-
- /**
- * Extract the form name from the Content-Disposition in a
- * multipart/form-data request.
- */
- public static String getFieldName(BodyPart part) throws MessagingException {
- String[] values = part.getHeader("Content-Disposition");
- String name = null;
- if (values != null && values.length > 0) {
- name = new ContentDisposition(values[0]).getParameter("name");
- }
- return (name != null) ? name : "unknown";
- }
-
- /**
- * Extract the text content for a {@link BodyPart}, assuming the default
- * encoding.
- */
- public static String getTextContent(BodyPart part) throws MessagingException, IOException {
- ContentType contentType = new ContentType(part.getContentType());
- String charset = contentType.getParameter("charset");
- if (charset == null) {
- // N.B.: The MIME spec doesn't seem to provide a
- // default charset, but the default charset for HTTP is
- // ISO-8859-1. That seems like a reasonable default.
- charset = "ISO-8859-1";
- }
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ByteStreams.copy(part.getInputStream(), baos);
- try {
- return new String(baos.toByteArray(), charset);
- } catch (UnsupportedEncodingException ex) {
- return new String(baos.toByteArray());
- }
- }
-
- /**
- * A read-only {@link DataSource} backed by a content type and a
- * fixed byte array.
- */
- private static class StaticDataSource implements DataSource {
- private final String contentType;
- private final byte[] bytes;
-
- public StaticDataSource(String contentType, byte[] bytes) {
- this.contentType = contentType;
- this.bytes = bytes;
- }
-
- @Override
- public String getContentType() {
- return contentType;
- }
-
- @Override
- public InputStream getInputStream() {
- return new ByteArrayInputStream(bytes);
- }
-
- @Override
- public OutputStream getOutputStream() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public String getName() {
- return "request";
- }
- }
-}
+@Deprecated(since = "3.0.0")
+public class MultipartMimeUtils
+ extends com.google.apphosting.utils.servlet.jakarta.MultipartMimeUtils {}
diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/ParseBlobUploadFilter.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/ParseBlobUploadFilter.java
index 7f6013be8..e78e257f8 100644
--- a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/ParseBlobUploadFilter.java
+++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/ParseBlobUploadFilter.java
@@ -16,185 +16,10 @@
package com.google.apphosting.utils.servlet.ee10;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import jakarta.servlet.Filter;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.FilterConfig;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.ServletResponse;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletRequestWrapper;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import javax.mail.BodyPart;
-import javax.mail.MessagingException;
-import javax.mail.internet.ContentType;
-import javax.mail.internet.MimeBodyPart;
-import javax.mail.internet.MimeMultipart;
-
/**
- * {@code ParseBlobUploadFilter} is responsible for the parsing
- * multipart/form-data or multipart/mixed requests used to make Blob
- * upload callbacks, and storing a set of string-encoded blob keys as
- * a servlet request attribute. This allows the {@code
- * BlobstoreService.getUploadedBlobs()} method to return the
- * appropriate {@code BlobKey} objects.
- *
- *
This filter automatically runs on all dynamic requests in the
- * production environment. In the DevAppServer, the equivalent work
- * is subsumed by {@code UploadBlobServlet}.
- *
+ * @deprecated as of version 3.0, use {@link
+ * com.google.apphosting.utils.servlet.jakarta.ParseBlobUploadFilter} instead.
*/
-public class ParseBlobUploadFilter implements Filter {
- private static final Logger logger = Logger.getLogger(
- ParseBlobUploadFilter.class.getName());
-
- /**
- * An arbitrary HTTP header that is set on all blob upload
- * callbacks.
- */
- static final String UPLOAD_HEADER = "X-AppEngine-BlobUpload";
-
- static final String UPLOADED_BLOBKEY_ATTR = "com.google.appengine.api.blobstore.upload.blobkeys";
-
- static final String UPLOADED_BLOBINFO_ATTR =
- "com.google.appengine.api.blobstore.upload.blobinfos";
-
- // This field has to be the same as X_APPENGINE_CLOUD_STORAGE_OBJECT in http_proto.cc.
- // This header will have the creation date in the format YYYY-MM-DD HH:mm:ss.SSS.
- static final String UPLOAD_CREATION_HEADER = "X-AppEngine-Upload-Creation";
-
- // This field has to be the same as X_APPENGINE_CLOUD_STORAGE_OBJECT in http_proto.cc.
- // This header will have the filename of created the object in Cloud Storage when appropriate.
- static final String CLOUD_STORAGE_OBJECT_HEADER = "X-AppEngine-Cloud-Storage-Object";
-
- static final String CONTENT_LENGTH_HEADER = "Content-Length";
-
- @Override
- public void init(FilterConfig config) {}
-
- @Override
- public void destroy() {}
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- HttpServletRequest req = (HttpServletRequest) request;
- if (req.getHeader(UPLOAD_HEADER) != null) {
- Map> blobKeys = new HashMap<>();
- Map>> blobInfos = new HashMap<>();
- Map> otherParams = new HashMap<>();
-
- try {
- MimeMultipart multipart = MultipartMimeUtils.parseMultipartRequest(req);
-
- int parts = multipart.getCount();
- for (int i = 0; i < parts; i++) {
- BodyPart part = multipart.getBodyPart(i);
- String fieldName = MultipartMimeUtils.getFieldName(part);
- if (part.getFileName() != null) {
- ContentType contentType = new ContentType(part.getContentType());
- if ("message/external-body".equals(contentType.getBaseType())) {
- String blobKeyString = contentType.getParameter("blob-key");
- List keys = blobKeys.computeIfAbsent(fieldName, k -> new ArrayList<>());
- keys.add(blobKeyString);
- List
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ compile
+
+ jar
+
+
+
+
+ true
+ public
+ false
+ none
+ ${project.basedir}/../api/src/main/java
+ true
+
+
+ com.google.auto.service
+ auto-service
+ 1.1.1
+
+
+ com.google.auto.service
+ auto-service-annotations
+ 1.1.1
+
+
+ com.google.auto
+ auto-common
+ 1.2.2
+
+
+
+
diff --git a/appengine-api-stubs/pom.xml b/appengine-api-stubs/pom.xml
index 444a2a07c..068f58b9c 100644
--- a/appengine-api-stubs/pom.xml
+++ b/appengine-api-stubs/pom.xml
@@ -23,11 +23,12 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: appengine-api-stubs
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
SDK for dev_appserver (local development) with some of the dependencies shaded (repackaged)
@@ -377,6 +378,42 @@
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ compile
+
+ jar
+
+
+
+
+ true
+ public
+ false
+ none
+ ${project.basedir}/../api_dev/src/main/java
+
+
+ com.google.auto.service
+ auto-service
+ 1.1.1
+
+
+ com.google.auto.service
+ auto-service-annotations
+ 1.1.1
+
+
+ com.google.auto
+ auto-common
+ 1.2.2
+
+
+
+
diff --git a/appengine_init/pom.xml b/appengine_init/pom.xml
index c83f602cf..91da51c25 100644
--- a/appengine_init/pom.xml
+++ b/appengine_init/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: appengine-init
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ App Engine initialization.
diff --git a/appengine_init/src/main/java/com/google/appengine/init/AppEngineWebXmlInitialParse.java b/appengine_init/src/main/java/com/google/appengine/init/AppEngineWebXmlInitialParse.java
index aa18c0a7e..ae396209c 100644
--- a/appengine_init/src/main/java/com/google/appengine/init/AppEngineWebXmlInitialParse.java
+++ b/appengine_init/src/main/java/com/google/appengine/init/AppEngineWebXmlInitialParse.java
@@ -35,8 +35,7 @@ public final class AppEngineWebXmlInitialParse {
private static final Logger logger =
Logger.getLogger(AppEngineWebXmlInitialParse.class.getName());
private String runtimeId = "";
- private boolean settingDoneInAppEngineWebXml = false;
- private final String file;
+ private final String appEngineWebXmlFile;
private static final String PROPERTIES = "system-properties";
private static final String PROPERTY = "property";
@@ -66,59 +65,183 @@ public final class AppEngineWebXmlInitialParse {
BUILD_VERSION = BUILD_PROPERTIES.getProperty("version", "unknown");
}
+ /**
+ * Handles the logic for setting Jetty and Jakarta EE versions based on runtime, {@code
+ * appengine-web.xml} properties, and System properties. System properties override {@code
+ * appengine-web.xml} properties. It ensures that only one EE version is active and sets defaults
+ * based on the Java runtime if no explicit version is chosen.
+ *
+ *
Only one of {@code appengine.use.EE8}, {@code appengine.use.EE10}, or {@code
+ * appengine.use.EE11} can be set to {@code true}, otherwise an {@link IllegalArgumentException}
+ * is thrown. If {@code appengine.use.EE11} is true, {@code appengine.use.jetty121} is also forced
+ * to true. If {@code runtime} is {@code java25}, {@code appengine.use.jetty121} is forced to
+ * true. For {@code java17} and {@code java21} runtimes, if {@code appengine.use.EE10=true} and
+ * {@code appengine.use.jetty121=true}, then {@code appengine.use.EE11} is forced to true and a
+ * warning is logged, as EE10 is not supported on Jetty 12.1.
+ *
+ *
If none of {@code appengine.use.EE8}, {@code appengine.use.EE10}, or {@code
+ * appengine.use.EE11} are set to true, defaults are applied as follows:
+ *
+ *
+ *
{@code runtime="java17"}: Defaults to Jetty 9.4 based environment (EE6 / Servlet 3.1).
+ *
{@code runtime="java21"}: Defaults to Jetty 12.0 / EE10, unless {@code
+ * appengine.use.jetty121=true}, in which case it defaults to Jetty 12.1 / EE11.
+ *
{@code runtime="java25"}: Defaults to Jetty 12.1 / EE11.
+ *
");
+
+ out.println("");
+ out.println("");
+ }
+ }
+
+ //
+ /**
+ * Handles the HTTP GET method.
+ *
+ * @param request servlet request
+ * @param response servlet response
+ * @throws ServletException if a servlet-specific error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ processRequest(request, response);
+ } catch (Exception ex) {
+ Logger.getLogger(ServletViewer.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+ /**
+ * Handles the HTTP POST method.
+ *
+ * @param request servlet request
+ * @param response servlet response
+ * @throws ServletException if a servlet-specific error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ processRequest(request, response);
+ } catch (Exception ex) {
+ Logger.getLogger(ServletViewer.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+ /**
+ * Returns a short description of the servlet.
+ *
+ * @return a String containing servlet description
+ */
+ @Override
+ public String getServletInfo() {
+ return "System Viewer";
+ }//
+
+}
diff --git a/applications/guestbook/src/main/java/com/google/appengine/demos/guestbook/SignGuestbookServlet.java b/applications/guestbook/src/main/java/com/google/appengine/demos/guestbook/SignGuestbookServlet.java
new file mode 100644
index 000000000..34d6108bc
--- /dev/null
+++ b/applications/guestbook/src/main/java/com/google/appengine/demos/guestbook/SignGuestbookServlet.java
@@ -0,0 +1,56 @@
+/*
+ * 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.demos.guestbook;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+
+import java.io.IOException;
+import java.util.Date;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class SignGuestbookServlet extends HttpServlet {
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException {
+ UserService userService = UserServiceFactory.getUserService();
+ User user = userService.getCurrentUser();
+
+ String guestbookName = req.getParameter("guestbookName");
+ Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
+ String content = req.getParameter("content");
+ Date date = new Date();
+ Entity greeting = new Entity("Greeting", guestbookKey);
+ greeting.setProperty("user", user);
+ greeting.setProperty("date", date);
+ greeting.setProperty("content", content);
+
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ datastore.put(greeting);
+
+ resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName);
+ }
+}
diff --git a/appengine_init/appengine-web.xml b/applications/guestbook/src/main/webapp/WEB-INF/appengine-web.xml
similarity index 66%
rename from appengine_init/appengine-web.xml
rename to applications/guestbook/src/main/webapp/WEB-INF/appengine-web.xml
index 9d76d3786..df8c61d44 100644
--- a/appengine_init/appengine-web.xml
+++ b/applications/guestbook/src/main/webapp/WEB-INF/appengine-web.xml
@@ -14,17 +14,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+
- java21
- true
- true
-
+ java25
+ true
+
-
-
-
-
+
-
-
-
+
diff --git a/applications/guestbook/src/main/webapp/WEB-INF/datastore-indexes.xml b/applications/guestbook/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 000000000..eeb215ba1
--- /dev/null
+++ b/applications/guestbook/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/applications/guestbook/src/main/webapp/WEB-INF/logging.properties b/applications/guestbook/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 000000000..fe435d2c1
--- /dev/null
+++ b/applications/guestbook/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+#
+#
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = WARNING
diff --git a/applications/guestbook/src/main/webapp/WEB-INF/web.xml b/applications/guestbook/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..65dfe41c6
--- /dev/null
+++ b/applications/guestbook/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+ sign
+ com.google.appengine.demos.guestbook.SignGuestbookServlet
+
+
+ test
+ com.google.appengine.demos.guestbook.GuestbookServlet
+
+
+ sign
+ /sign
+
+
+ test
+ /test
+
+
+ guestbook.jsp
+
+
diff --git a/applications/guestbook/src/main/webapp/guestbook.jsp b/applications/guestbook/src/main/webapp/guestbook.jsp
new file mode 100644
index 000000000..1d4d885a2
--- /dev/null
+++ b/applications/guestbook/src/main/webapp/guestbook.jsp
@@ -0,0 +1,110 @@
+
+
+
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="com.google.appengine.api.datastore.DatastoreService" %>
+<%@ page import="com.google.appengine.api.datastore.DatastoreServiceFactory" %>
+<%@ page import="com.google.appengine.api.datastore.Entity" %>
+<%@ page import="com.google.appengine.api.datastore.FetchOptions" %>
+<%@ page import="com.google.appengine.api.datastore.Key" %>
+<%@ page import="com.google.appengine.api.datastore.KeyFactory" %>
+<%@ page import="com.google.appengine.api.datastore.Query" %>
+<%@ page import="com.google.appengine.api.users.User" %>
+<%@ page import="com.google.appengine.api.users.UserService" %>
+<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
+<%@ page import="java.util.List" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+
+
+
+
+
+
+
+
+<%
+ String guestbookName = request.getParameter("guestbookName");
+ if (guestbookName == null) {
+ guestbookName = "default";
+ }
+ pageContext.setAttribute("guestbookName", guestbookName);
+ UserService userService = UserServiceFactory.getUserService();
+ User user = userService.getCurrentUser();
+ if (user != null) {
+ pageContext.setAttribute("user", user);
+%>
+
Hello, ${fn:escapeXml(user.nickname)}! (You can
+ sign out.)
+<%
+} else {
+%>
+
Hello!
+ Sign in
+ to include your name with greetings you post.
+<%
+ }
+%>
+
+<%
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
+ // Run an ancestor query to ensure we see the most up-to-date
+ // view of the Greetings belonging to the selected Guestbook.
+ Query query = new Query("Greeting", guestbookKey).addSort("date", Query.SortDirection.DESCENDING);
+ List greetings = datastore.prepare(query).asList(FetchOptions.Builder.withLimit(5));
+ if (greetings.isEmpty()) {
+%>
+
Guestbook '${fn:escapeXml(guestbookName)}' has no messages.
+<%
+} else {
+%>
+
Messages in Guestbook '${fn:escapeXml(guestbookName)}'.
+<%
+ for (Entity greeting : greetings) {
+ pageContext.setAttribute("greeting_content",
+ greeting.getProperty("content"));
+ if (greeting.getProperty("user") == null) {
+%>
+
");
+
+ out.println("");
+ out.println("");
+ }
+ }
+
+ //
+ /**
+ * Handles the HTTP GET method.
+ *
+ * @param request servlet request
+ * @param response servlet response
+ * @throws ServletException if a servlet-specific error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ processRequest(request, response);
+ } catch (Exception ex) {
+ Logger.getLogger(ServletViewer.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+ /**
+ * Handles the HTTP POST method.
+ *
+ * @param request servlet request
+ * @param response servlet response
+ * @throws ServletException if a servlet-specific error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ processRequest(request, response);
+ } catch (Exception ex) {
+ Logger.getLogger(ServletViewer.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+ /**
+ * Returns a short description of the servlet.
+ *
+ * @return a String containing servlet description
+ */
+ @Override
+ public String getServletInfo() {
+ return "System Viewer";
+ }//
+
+}
diff --git a/applications/guestbook_jakarta/src/main/java/com/google/appengine/demos/guestbook/SignGuestbookServlet.java b/applications/guestbook_jakarta/src/main/java/com/google/appengine/demos/guestbook/SignGuestbookServlet.java
new file mode 100644
index 000000000..94c5deb0d
--- /dev/null
+++ b/applications/guestbook_jakarta/src/main/java/com/google/appengine/demos/guestbook/SignGuestbookServlet.java
@@ -0,0 +1,56 @@
+/*
+ * 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.demos.guestbook;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+
+import java.io.IOException;
+import java.util.Date;
+
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class SignGuestbookServlet extends HttpServlet {
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException {
+ UserService userService = UserServiceFactory.getUserService();
+ User user = userService.getCurrentUser();
+
+ String guestbookName = req.getParameter("guestbookName");
+ Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
+ String content = req.getParameter("content");
+ Date date = new Date();
+ Entity greeting = new Entity("Greeting", guestbookKey);
+ greeting.setProperty("user", user);
+ greeting.setProperty("date", date);
+ greeting.setProperty("content", content);
+
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ datastore.put(greeting);
+
+ resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName);
+ }
+}
diff --git a/applications/guestbook_jakarta/src/main/webapp/WEB-INF/appengine-web.xml b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..37fe35fc4
--- /dev/null
+++ b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ java25
+ true
+
+
+
+
diff --git a/applications/guestbook_jakarta/src/main/webapp/WEB-INF/datastore-indexes.xml b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 000000000..eeb215ba1
--- /dev/null
+++ b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/applications/guestbook_jakarta/src/main/webapp/WEB-INF/logging.properties b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 000000000..fe435d2c1
--- /dev/null
+++ b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+#
+#
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = WARNING
diff --git a/applications/guestbook_jakarta/src/main/webapp/WEB-INF/web.xml b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..65dfe41c6
--- /dev/null
+++ b/applications/guestbook_jakarta/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+ sign
+ com.google.appengine.demos.guestbook.SignGuestbookServlet
+
+
+ test
+ com.google.appengine.demos.guestbook.GuestbookServlet
+
+
+ sign
+ /sign
+
+
+ test
+ /test
+
+
+ guestbook.jsp
+
+
diff --git a/applications/guestbook_jakarta/src/main/webapp/guestbook.jsp b/applications/guestbook_jakarta/src/main/webapp/guestbook.jsp
new file mode 100644
index 000000000..12618563c
--- /dev/null
+++ b/applications/guestbook_jakarta/src/main/webapp/guestbook.jsp
@@ -0,0 +1,110 @@
+
+
+
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="com.google.appengine.api.datastore.DatastoreService" %>
+<%@ page import="com.google.appengine.api.datastore.DatastoreServiceFactory" %>
+<%@ page import="com.google.appengine.api.datastore.Entity" %>
+<%@ page import="com.google.appengine.api.datastore.FetchOptions" %>
+<%@ page import="com.google.appengine.api.datastore.Key" %>
+<%@ page import="com.google.appengine.api.datastore.KeyFactory" %>
+<%@ page import="com.google.appengine.api.datastore.Query" %>
+<%@ page import="com.google.appengine.api.users.User" %>
+<%@ page import="com.google.appengine.api.users.UserService" %>
+<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
+<%@ page import="java.util.List" %>
+<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
+
+
+
+
+
+
+
+
+<%
+ String guestbookName = request.getParameter("guestbookName");
+ if (guestbookName == null) {
+ guestbookName = "default";
+ }
+ pageContext.setAttribute("guestbookName", guestbookName);
+ UserService userService = UserServiceFactory.getUserService();
+ User user = userService.getCurrentUser();
+ if (user != null) {
+ pageContext.setAttribute("user", user);
+%>
+
Hello, ${fn:escapeXml(user.nickname)}! (You can
+ sign out.)
+<%
+} else {
+%>
+
Hello!
+ Sign in
+ to include your name with greetings you post.
+<%
+ }
+%>
+
+<%
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
+ // Run an ancestor query to ensure we see the most up-to-date
+ // view of the Greetings belonging to the selected Guestbook.
+ Query query = new Query("Greeting", guestbookKey).addSort("date", Query.SortDirection.DESCENDING);
+ List greetings = datastore.prepare(query).asList(FetchOptions.Builder.withLimit(5));
+ if (greetings.isEmpty()) {
+%>
+
Guestbook '${fn:escapeXml(guestbookName)}' has no messages.
+<%
+} else {
+%>
+
Messages in Guestbook '${fn:escapeXml(guestbookName)}'.
+<%
+ for (Entity greeting : greetings) {
+ pageContext.setAttribute("greeting_content",
+ greeting.getProperty("content"));
+ if (greeting.getProperty("user") == null) {
+%>
+
+<%
+ }
+ }
+%>
+
+
+
+
+
+
+
diff --git a/applications/guestbook_jakarta/src/main/webapp/stylesheets/main.css b/applications/guestbook_jakarta/src/main/webapp/stylesheets/main.css
new file mode 100644
index 000000000..9456d7314
--- /dev/null
+++ b/applications/guestbook_jakarta/src/main/webapp/stylesheets/main.css
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+body {
+ font-family: Verdana, Helvetica, sans-serif;
+ background-color: #FFFFCC;
+}
diff --git a/applications/guestbook_jakarta/src/test/java/com/google/appengine/demos/guestbook/GuestbookServletTest.java b/applications/guestbook_jakarta/src/test/java/com/google/appengine/demos/guestbook/GuestbookServletTest.java
new file mode 100644
index 000000000..17cb0e0d4
--- /dev/null
+++ b/applications/guestbook_jakarta/src/test/java/com/google/appengine/demos/guestbook/GuestbookServletTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.demos.guestbook;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class GuestbookServletTest {
+
+ private GuestbookServlet guestbookServlet;
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalUserServiceTestConfig())
+ .setEnvIsLoggedIn(true)
+ .setEnvAuthDomain("localhost")
+ .setEnvEmail("test@localhost");
+
+ @Before
+ public void setupGuestBookServlet() {
+ helper.setUp();
+ guestbookServlet = new GuestbookServlet();
+ }
+
+ @After
+ public void tearDownHelper() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void testDoGet() throws IOException {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ StringWriter stringWriter = new StringWriter();
+
+ when(response.getWriter()).thenReturn(new PrintWriter(stringWriter));
+
+ guestbookServlet.doGet(request, response);
+
+ User currentUser = UserServiceFactory.getUserService().getCurrentUser();
+
+ assertEquals(true, stringWriter.toString().startsWith("Hello"));
+ }
+
+}
diff --git a/applications/guestbook_jakarta/src/test/java/com/google/appengine/demos/guestbook/SignGuestbookServletTest.java b/applications/guestbook_jakarta/src/test/java/com/google/appengine/demos/guestbook/SignGuestbookServletTest.java
new file mode 100644
index 000000000..69b107942
--- /dev/null
+++ b/applications/guestbook_jakarta/src/test/java/com/google/appengine/demos/guestbook/SignGuestbookServletTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.demos.guestbook;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Date;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class SignGuestbookServletTest {
+
+ private SignGuestbookServlet signGuestbookServlet;
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig())
+ .setEnvIsLoggedIn(true)
+ .setEnvAuthDomain("localhost")
+ .setEnvEmail("test@localhost");
+
+ @Before
+ public void setupSignGuestBookServlet() {
+ helper.setUp();
+ signGuestbookServlet = new SignGuestbookServlet();
+ }
+
+ @After
+ public void tearDownHelper() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void testDoPost() throws IOException, EntityNotFoundException {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ String guestbookName = "TestGuestbook";
+ String testContent = "Test Content";
+
+ when(request.getParameter("guestbookName")).thenReturn(guestbookName);
+ when(request.getParameter("content")).thenReturn(testContent);
+
+ Date priorToRequest = new Date();
+
+ signGuestbookServlet.doPost(request, response);
+
+ Date afterRequest = new Date();
+
+ verify(response).sendRedirect("/guestbook.jsp?guestbookName=TestGuestbook");
+
+ User currentUser = UserServiceFactory.getUserService().getCurrentUser();
+
+ Entity greeting = DatastoreServiceFactory.getDatastoreService().prepare(new Query()).asSingleEntity();
+
+ assertEquals(guestbookName, greeting.getKey().getParent().getName());
+ assertEquals(testContent, greeting.getProperty("content"));
+ assertEquals(currentUser, greeting.getProperty("user"));
+
+ Date date = (Date) greeting.getProperty("date");
+ assertTrue("The date in the entity [" + date + "] is prior to the request being performed",
+ priorToRequest.before(date) || priorToRequest.equals(date));
+ assertTrue("The date in the entity [" + date + "] is after to the request completed",
+ afterRequest.after(date) || afterRequest.equals(date));
+ }
+}
diff --git a/applications/pom.xml b/applications/pom.xml
index 6d3a9e9d4..2d21dc1f3 100644
--- a/applications/pom.xml
+++ b/applications/pom.xml
@@ -19,10 +19,12 @@
4.0.0applicationsAppEngine :: application projects
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Parent POM for sample applications.com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTpom
@@ -31,5 +33,9 @@
proberappspringboot
+ guestbook
+ guestbook_jakarta
+ servletasyncapp
+ servletasyncappjakarta
diff --git a/applications/proberapp/pom.xml b/applications/proberapp/pom.xml
index e9860c21b..ca8ad8d26 100644
--- a/applications/proberapp/pom.xml
+++ b/applications/proberapp/pom.xml
@@ -24,10 +24,12 @@
com.google.appengine.demosproberappAppEngine :: proberapp
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ A prober application.com.google.appengineapplications
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOT
@@ -38,7 +40,7 @@
us-central1prober-userprober_connectivity_test_database
- 2.60.0
+ 2.70.2${project.version}UTF-8target/${project.artifactId}-${project.version}
@@ -58,7 +60,7 @@
com.google.cloudgoogle-cloud-spanner
- 6.86.0
+ 6.100.0com.google.appengine
@@ -116,27 +118,27 @@
com.google.cloudgoogle-cloud-bigquery
- 2.47.0
+ 2.55.0com.google.cloudgoogle-cloud-core
- 2.50.0
+ 2.60.2com.google.cloudgoogle-cloud-datastore
- 2.26.1
+ 2.32.0com.google.cloudgoogle-cloud-logging
- 3.21.2
+ 3.23.4com.google.cloudgoogle-cloud-storage
- 2.48.1
+ 2.57.0com.google.cloud.sql
@@ -170,7 +172,7 @@
com.mysqlmysql-connector-j
- 8.2.0
+ 9.4.0org.apache.httpcomponents
@@ -268,7 +270,7 @@
maven-compiler-plugin
- 3.13.0
+ 3.14.08
@@ -276,7 +278,7 @@
org.apache.maven.pluginsmaven-enforcer-plugin
- 3.5.0
+ 3.6.1enforce-maven
diff --git a/applications/servletasyncapp/pom.xml b/applications/servletasyncapp/pom.xml
new file mode 100644
index 000000000..cfbe6d228
--- /dev/null
+++ b/applications/servletasyncapp/pom.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ 4.0.0
+ war
+
+ com.google.appengine
+ applications
+ 3.0.0-SNAPSHOT
+
+ com.google.appengine.demos
+ servletasyncapp
+ AppEngine :: async servlet with javax servlet api
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ An async servlet sample application.
+
+
+ true
+ UTF-8
+ 17
+ 17
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
diff --git a/applications/servletasyncapp/src/main/java/AppAsyncListener.java b/applications/servletasyncapp/src/main/java/AppAsyncListener.java
new file mode 100644
index 000000000..a9e568aa8
--- /dev/null
+++ b/applications/servletasyncapp/src/main/java/AppAsyncListener.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+
+/** Simple Async listener sample */
+public class AppAsyncListener implements AsyncListener {
+ @Override
+ public void onComplete(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onComplete");
+ }
+
+ @Override
+ public void onError(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onError");
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onStartAsync");
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onTimeout");
+ }
+}
diff --git a/applications/servletasyncapp/src/main/java/AppContextListener.java b/applications/servletasyncapp/src/main/java/AppContextListener.java
new file mode 100644
index 000000000..7e89c88eb
--- /dev/null
+++ b/applications/servletasyncapp/src/main/java/AppContextListener.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+/**
+ * Simple App context listener that creates a ThreadPoolExecutor that creates Deamon threads, and
+ * stores it in the ServletContext attribute named "executor".
+ */
+public class AppContextListener implements ServletContextListener {
+
+ @Override
+ public void contextInitialized(ServletContextEvent servletContextEvent) {
+
+ ThreadPoolExecutor executor =
+ new ThreadPoolExecutor(
+ /* corePoolSize= */ 100,
+ /* maximumPoolSize= */ 200,
+ /* keepAliveTime= */ 50000L,
+ TimeUnit.MILLISECONDS,
+ new ArrayBlockingQueue(100),
+ new DaemonThreadFactory());
+ servletContextEvent.getServletContext().setAttribute("executor", executor);
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent servletContextEvent) {
+ ThreadPoolExecutor executor =
+ (ThreadPoolExecutor) servletContextEvent.getServletContext().getAttribute("executor");
+ executor.shutdown();
+ }
+
+ static class DaemonThreadFactory implements ThreadFactory {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = new Thread(r, "created via ThreadPoolExecutor");
+ thread.setDaemon(true);
+ return thread;
+ }
+ }
+}
diff --git a/applications/servletasyncapp/src/main/java/AsyncServlet.java b/applications/servletasyncapp/src/main/java/AsyncServlet.java
new file mode 100644
index 000000000..b666da661
--- /dev/null
+++ b/applications/servletasyncapp/src/main/java/AsyncServlet.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.concurrent.ThreadPoolExecutor;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Test Servlet Async application for AppServer tests. */
+public class AsyncServlet extends HttpServlet {
+
+ /**
+ * Process HTTP request and return simple string.
+ *
+ * @param req is the HTTP servlet request
+ * @param resp is the HTTP servlet response
+ * @exception IOException
+ */
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ long startTime = System.currentTimeMillis();
+ System.out.println(
+ "AsyncServlet Start::Name="
+ + Thread.currentThread().getName()
+ + "::ID="
+ + Thread.currentThread().getId());
+
+ String time = req.getParameter("time");
+ int millisecs = 1000;
+ if (time != null) {
+ millisecs = Integer.parseInt(time);
+ }
+ // max 10 seconds
+ if (millisecs > 10000) {
+ millisecs = 10000;
+ }
+
+ // Puts this request into asynchronous mode, and initializes its AsyncContext.
+ AsyncContext asyncContext = req.startAsync(req, resp);
+ asyncContext.addListener(new AppAsyncListener());
+ ServletRequest servReq = asyncContext.getRequest();
+
+ PrintWriter out = resp.getWriter();
+ out.println("isAsyncStarted : " + servReq.isAsyncStarted());
+ // This excecutor should be created in the init phase of AppContextListener.
+ ThreadPoolExecutor executor =
+ (ThreadPoolExecutor) req.getServletContext().getAttribute("executor");
+
+ executor.execute(new LongProcessingRunnable(asyncContext, millisecs));
+ long endTime = System.currentTimeMillis();
+ System.out.println(
+ "AsyncServlet End::Thread Name="
+ + Thread.currentThread().getName()
+ + "::Thread ID="
+ + Thread.currentThread().getId()
+ + "::Time Taken="
+ + (endTime - startTime)
+ + " ms.");
+ }
+}
diff --git a/applications/servletasyncapp/src/main/java/LongProcessingRunnable.java b/applications/servletasyncapp/src/main/java/LongProcessingRunnable.java
new file mode 100644
index 000000000..b0a9f285a
--- /dev/null
+++ b/applications/servletasyncapp/src/main/java/LongProcessingRunnable.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.AsyncContext;
+
+/**
+ * Runnable executed with the ThreadPoolExecutor created in the AppContextListener. It sleeps a few
+ * milli seconds, then prints a PASS message in the servlet response writer.
+ */
+class LongProcessingRunnable implements Runnable {
+
+ private final AsyncContext asyncContext;
+ private final long millisecs;
+
+ LongProcessingRunnable(AsyncContext asyncCtx, long millisecs) {
+ this.asyncContext = asyncCtx;
+ this.millisecs = millisecs;
+ }
+
+ @Override
+ public void run() {
+ longProcessing(millisecs);
+ try {
+ PrintWriter out = asyncContext.getResponse().getWriter();
+ out.write("PASS: " + millisecs + " milliseconds.");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ // complete the processing
+ asyncContext.complete();
+ }
+
+ private void longProcessing(long millisecs) {
+ // wait for given time before finishing
+ try {
+ Thread.sleep(millisecs);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/runtime/testapps/src/main/resources/com/google/apphosting/runtime/jetty9/gzipapp/jetty94/WEB-INF/appengine-web.xml b/applications/servletasyncapp/src/main/webapp/WEB-INF/appengine-web.xml
similarity index 80%
rename from runtime/testapps/src/main/resources/com/google/apphosting/runtime/jetty9/gzipapp/jetty94/WEB-INF/appengine-web.xml
rename to applications/servletasyncapp/src/main/webapp/WEB-INF/appengine-web.xml
index add8402f3..85fed4e9e 100644
--- a/runtime/testapps/src/main/resources/com/google/apphosting/runtime/jetty9/gzipapp/jetty94/WEB-INF/appengine-web.xml
+++ b/applications/servletasyncapp/src/main/webapp/WEB-INF/appengine-web.xml
@@ -14,12 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
- java8
- gzip
- true
+ servlet-async-java17
+ auto
+ java17
-
+
diff --git a/applications/servletasyncapp/src/main/webapp/WEB-INF/web.xml b/applications/servletasyncapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..22fe1931f
--- /dev/null
+++ b/applications/servletasyncapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ asyncservlet
+ AsyncServlet
+ true
+
+
+ asyncservlet
+ /asyncservlet
+
+
+
+ AppContextListener
+
+
+
+
+ AppAsyncListener
+
+
+
diff --git a/applications/servletasyncappjakarta/pom.xml b/applications/servletasyncappjakarta/pom.xml
new file mode 100644
index 000000000..88a257226
--- /dev/null
+++ b/applications/servletasyncappjakarta/pom.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ 4.0.0
+ war
+
+ com.google.appengine
+ applications
+ 3.0.0-SNAPSHOT
+
+ com.google.appengine.demos
+ servletasyncappjakarta
+ AppEngine :: async servlet with jakarta servlet api
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ An async servlet sample application.
+
+
+ true
+ UTF-8
+ 17
+ 17
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 6.0.0
+ provided
+
+
+
diff --git a/applications/servletasyncappjakarta/src/main/java/AppAsyncListener.java b/applications/servletasyncappjakarta/src/main/java/AppAsyncListener.java
new file mode 100644
index 000000000..86e6ac117
--- /dev/null
+++ b/applications/servletasyncappjakarta/src/main/java/AppAsyncListener.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import jakarta.servlet.AsyncEvent;
+import jakarta.servlet.AsyncListener;
+
+/** Simple Async listener sample */
+public class AppAsyncListener implements AsyncListener {
+ @Override
+ public void onComplete(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onComplete");
+ }
+
+ @Override
+ public void onError(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onError");
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onStartAsync");
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent asyncEvent) throws IOException {
+ System.out.println("AppAsyncListener onTimeout");
+ }
+}
diff --git a/applications/servletasyncappjakarta/src/main/java/AppContextListener.java b/applications/servletasyncappjakarta/src/main/java/AppContextListener.java
new file mode 100644
index 000000000..c224334e7
--- /dev/null
+++ b/applications/servletasyncappjakarta/src/main/java/AppContextListener.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+
+/**
+ * Simple App context listener that creates a ThreadPoolExecutor that creates Deamon threads, and
+ * stores it in the ServletContext attribute named "executor".
+ */
+public class AppContextListener implements ServletContextListener {
+
+ @Override
+ public void contextInitialized(ServletContextEvent servletContextEvent) {
+
+ ThreadPoolExecutor executor =
+ new ThreadPoolExecutor(
+ /* corePoolSize= */ 100,
+ /* maximumPoolSize= */ 200,
+ /* keepAliveTime= */ 50000L,
+ TimeUnit.MILLISECONDS,
+ new ArrayBlockingQueue(100),
+ new DaemonThreadFactory());
+ servletContextEvent.getServletContext().setAttribute("executor", executor);
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent servletContextEvent) {
+ ThreadPoolExecutor executor =
+ (ThreadPoolExecutor) servletContextEvent.getServletContext().getAttribute("executor");
+ executor.shutdown();
+ }
+
+ static class DaemonThreadFactory implements ThreadFactory {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = new Thread(r, "created via ThreadPoolExecutor");
+ thread.setDaemon(true);
+ return thread;
+ }
+ }
+}
diff --git a/applications/servletasyncappjakarta/src/main/java/AsyncServlet.java b/applications/servletasyncappjakarta/src/main/java/AsyncServlet.java
new file mode 100644
index 000000000..692dd3f9f
--- /dev/null
+++ b/applications/servletasyncappjakarta/src/main/java/AsyncServlet.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.concurrent.ThreadPoolExecutor;
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/** Test Servlet Async application for AppServer tests. */
+public class AsyncServlet extends HttpServlet {
+
+ /**
+ * Process HTTP request and return simple string.
+ *
+ * @param req is the HTTP servlet request
+ * @param resp is the HTTP servlet response
+ * @exception IOException
+ */
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ long startTime = System.currentTimeMillis();
+ System.out.println(
+ "AsyncServlet Start::Name="
+ + Thread.currentThread().getName()
+ + "::ID="
+ + Thread.currentThread().getId());
+
+ String time = req.getParameter("time");
+ int millisecs = 1000;
+ if (time != null) {
+ millisecs = Integer.parseInt(time);
+ }
+ // max 10 seconds
+ if (millisecs > 10000) {
+ millisecs = 10000;
+ }
+
+ // Puts this request into asynchronous mode, and initializes its AsyncContext.
+ AsyncContext asyncContext = req.startAsync(req, resp);
+ asyncContext.addListener(new AppAsyncListener());
+ ServletRequest servReq = asyncContext.getRequest();
+
+ PrintWriter out = resp.getWriter();
+ out.println("isAsyncStarted : " + servReq.isAsyncStarted());
+ // This excecutor should be created in the init phase of AppContextListener.
+ ThreadPoolExecutor executor =
+ (ThreadPoolExecutor) req.getServletContext().getAttribute("executor");
+
+ executor.execute(new LongProcessingRunnable(asyncContext, millisecs));
+ long endTime = System.currentTimeMillis();
+ System.out.println(
+ "AsyncServlet End::Thread Name="
+ + Thread.currentThread().getName()
+ + "::Thread ID="
+ + Thread.currentThread().getId()
+ + "::Time Taken="
+ + (endTime - startTime)
+ + " ms.");
+ }
+}
diff --git a/applications/servletasyncappjakarta/src/main/java/LongProcessingRunnable.java b/applications/servletasyncappjakarta/src/main/java/LongProcessingRunnable.java
new file mode 100644
index 000000000..d2d460e79
--- /dev/null
+++ b/applications/servletasyncappjakarta/src/main/java/LongProcessingRunnable.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import jakarta.servlet.AsyncContext;
+
+/**
+ * Runnable executed with the ThreadPoolExecutor created in the AppContextListener. It sleeps a few
+ * milli seconds, then prints a PASS message in the servlet response writer.
+ */
+class LongProcessingRunnable implements Runnable {
+
+ private final AsyncContext asyncContext;
+ private final long millisecs;
+
+ LongProcessingRunnable(AsyncContext asyncCtx, long millisecs) {
+ this.asyncContext = asyncCtx;
+ this.millisecs = millisecs;
+ }
+
+ @Override
+ public void run() {
+ longProcessing(millisecs);
+ try {
+ PrintWriter out = asyncContext.getResponse().getWriter();
+ out.write("PASS: " + millisecs + " milliseconds.");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ // complete the processing
+ asyncContext.complete();
+ }
+
+ private void longProcessing(long millisecs) {
+ // wait for given time before finishing
+ try {
+ Thread.sleep(millisecs);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/applications/servletasyncappjakarta/src/main/webapp/WEB-INF/appengine-web.xml b/applications/servletasyncappjakarta/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..c2f8453bc
--- /dev/null
+++ b/applications/servletasyncappjakarta/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,24 @@
+
+
+
+ servlet-async-java21
+ auto
+ java21
+
+
+
+
diff --git a/applications/servletasyncappjakarta/src/main/webapp/WEB-INF/web.xml b/applications/servletasyncappjakarta/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..22fe1931f
--- /dev/null
+++ b/applications/servletasyncappjakarta/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ asyncservlet
+ AsyncServlet
+ true
+
+
+ asyncservlet
+ /asyncservlet
+
+
+
+ AppContextListener
+
+
+
+
+ AppAsyncListener
+
+
+
diff --git a/applications/springboot/pom.xml b/applications/springboot/pom.xml
index eb1776817..f242e67eb 100644
--- a/applications/springboot/pom.xml
+++ b/applications/springboot/pom.xml
@@ -24,11 +24,12 @@
com.google.appengineapplications
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTwarAppEngine :: springboot
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/Demo project for Spring Boot
diff --git a/e2etests/devappservertests/pom.xml b/e2etests/devappservertests/pom.xml
index c4f284e7a..4bf8ce6dd 100644
--- a/e2etests/devappservertests/pom.xml
+++ b/e2etests/devappservertests/pom.xml
@@ -22,11 +22,13 @@
com.google.appenginee2etests
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: e2e devappserver tests
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Tests for the development app server.true
@@ -58,7 +60,12 @@
httpclienttest
-
+
+ com.google.appengine
+ appengine-tools-sdk
+ test
+
+
diff --git a/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/DevAppServerMainTest.java b/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/DevAppServerMainTest.java
index ddeb12a6f..cc5e65002 100644
--- a/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/DevAppServerMainTest.java
+++ b/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/DevAppServerMainTest.java
@@ -15,88 +15,134 @@
*/
package com.google.appengine.tools.development;
-import static com.google.common.base.StandardSystemProperty.JAVA_HOME;
-import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
+import static com.google.common.truth.Truth.assertThat;
-import com.google.apphosting.testing.PortPicker;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.net.HostAndPort;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.util.EntityUtils;
import org.junit.Before;
+import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class DevAppServerMainTest extends DevAppServerTestBase {
- private static final String TOOLS_JAR =
- getSdkRoot().getAbsolutePath() + "/lib/appengine-tools-api.jar";
- @Parameterized.Parameters
- public static List
@@ -207,6 +214,45 @@
+
+ jdk21
+
+ 21
+ 21
+ 21
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${env.JAVA_HOME_21}/bin/javac
+
+
+
+
+
+
+ jdk25
+
+
+ 23
+ 23
+ 23
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${env.JAVA_HOME_25}/bin/javac
+
+
+
+
+
@@ -325,12 +371,12 @@
com.google.api-clientgoogle-api-client-appengine
- 2.7.2
+ 2.8.1com.google.api-clientgoogle-api-client
- 2.7.2
+ 2.8.1com.google.appengine
@@ -366,7 +412,7 @@
org.easymockeasymock
- 5.5.0
+ 5.6.0com.google.appengine
@@ -434,33 +480,33 @@
com.google.code.gsongson
- 2.12.1
+ 2.13.2com.google.floggerflogger-system-backend
- 0.8
+ 0.9runtimecom.google.floggergoogle-extensions
- 0.8
+ 0.9com.google.guavaguava
- 33.4.0-jre
+ 33.5.0-jrecom.google.errorproneerror_prone_annotations
- 2.36.0
+ 2.42.0com.google.http-clientgoogle-http-client
- 1.46.1
+ 1.47.1com.google.http-client
@@ -471,7 +517,7 @@
com.google.oauth-clientgoogle-oauth-client
- 1.37.0
+ 1.39.0com.google.protobuf
@@ -501,7 +547,7 @@
jakarta.servletjakarta.servlet-api
- 6.0.0
+ 6.1.0javax.servlet.jsp.jstl
@@ -516,7 +562,7 @@
org.apache.mavenmaven-core
- 3.9.9
+ 3.9.11org.apache.ant
@@ -532,13 +578,12 @@
org.apache.mavenmaven-plugin-api
- 3.9.9
+ 3.9.11
- org.checkerframework
- checker-qual
- 3.49.0
- provided
+ org.jspecify
+ jspecify
+ 1.0.0org.eclipse.jetty
@@ -571,7 +616,7 @@
org.jsoupjsoup
- 1.18.3
+ 1.21.2org.apache.lucene
@@ -591,73 +636,12 @@
com.google.http-clientgoogle-http-client-appengine
- 1.46.1
+ 1.47.1com.google.oauth-clientgoogle-oauth-client-java6
- 1.37.0
-
-
- io.grpc
- grpc-api
- ${io.grpc}
-
-
- io.grpc
- grpc-stub
- ${io.grpc}
-
-
- io.grpc
- grpc-protobuf
- ${io.grpc}
-
-
- io.grpc
- grpc-netty
- ${io.grpc}
-
-
-
- io.netty
- netty-buffer
- ${io.netty}
-
-
- io.netty
- netty-codec
- ${io.netty}
-
-
- io.netty
- netty-codec-http
- ${io.netty}
-
-
- io.netty
- netty-codec-http2
- ${io.netty}
-
-
- io.netty
- netty-common
- ${io.netty}
-
-
- io.netty
- netty-handler
- ${io.netty}
-
-
- io.netty
- netty-transport
- ${io.netty}
-
-
- io.netty
- netty-transport-native-unix-common
- ${io.netty}
+ 1.39.0org.apache.tomcat
@@ -667,40 +651,40 @@
com.fasterxml.jackson.corejackson-core
- 2.18.2
+ 2.20.0joda-timejoda-time
- 2.13.1
+ 2.14.0org.jsonjson
- 20240303
+ 20250107commons-codeccommons-codec
- 1.18.0
+ 1.19.0com.google.guavaguava-testlib
- 33.4.0-jre
+ 33.5.0-jretestcom.google.truthtruth
- 1.4.4
+ 1.4.5testcom.google.truth.extensionstruth-java8-extension
- 1.4.4
+ 1.4.5test
@@ -713,7 +697,7 @@
org.mockitomockito-bom
- 5.15.2
+ 5.20.0importpom
@@ -726,7 +710,7 @@
com.google.cloudgoogle-cloud-logging
- 3.21.2
+ 3.23.4
@@ -736,7 +720,7 @@
com.google.cloud.artifactregistryartifactregistry-maven-wagon
- 2.2.4
+ 2.2.5
@@ -744,7 +728,7 @@
org.apache.maven.pluginsmaven-surefire-plugin
- 3.5.2
+ 3.5.4../deployment/target/runtime-deployment-${project.version}
@@ -766,7 +750,7 @@
org.codehaus.mojoversions-maven-plugin
- 2.18.0
+ 2.19.0file:///${session.executionRootDirectory}/maven-version-rules.xmlfalse
@@ -775,7 +759,7 @@
org.apache.maven.pluginsmaven-enforcer-plugin
- 3.5.0
+ 3.6.1enforce-maven
@@ -803,7 +787,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.13.0
+ 3.14.0org.apache.maven.plugins
@@ -813,7 +797,7 @@
org.apache.maven.pluginsmaven-javadoc-plugin
- 3.11.2
+ 3.11.3falsenone
@@ -841,7 +825,7 @@
org.apache.maven.pluginsmaven-shade-plugin
- 3.6.0
+ 3.6.1com.github.os72
@@ -868,12 +852,12 @@
org.codehaus.mojojavacc-maven-plugin
- 3.1.0
+ 3.1.1org.codehaus.mojolicense-maven-plugin
- 2.5.0
+ 2.6.0com.google.appenginetrue
@@ -921,7 +905,7 @@
org.codehaus.mojoversions-maven-plugin
- 2.18.0
+ 2.19.0
diff --git a/protobuf/pom.xml b/protobuf/pom.xml
index 435e73494..6abaa2d71 100644
--- a/protobuf/pom.xml
+++ b/protobuf/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: protos
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Protocol buffers.
diff --git a/protobuf/span_details.proto b/protobuf/span_details.proto
index be1699960..8665395b2 100644
--- a/protobuf/span_details.proto
+++ b/protobuf/span_details.proto
@@ -22,7 +22,6 @@ syntax = "proto2";
package cloud_trace;
-import "label_options.proto";
import "span_kind.proto";
option java_package = "com.google.apphosting.base.protos";
diff --git a/quickstartgenerator/pom.xml b/quickstartgenerator/pom.xml
index 7044be480..9dc4b3c28 100644
--- a/quickstartgenerator/pom.xml
+++ b/quickstartgenerator/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: quickstartgenerator Jetty9
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Quickstart generator.org.eclipse.jetty
diff --git a/quickstartgenerator_jetty12/pom.xml b/quickstartgenerator_jetty12/pom.xml
index 792056091..f82abb156 100644
--- a/quickstartgenerator_jetty12/pom.xml
+++ b/quickstartgenerator_jetty12/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: quickstartgenerator Jetty12
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Quickstart generator for Jetty 12.org.eclipse.jetty.ee8
diff --git a/quickstartgenerator_jetty121_ee11/pom.xml b/quickstartgenerator_jetty121_ee11/pom.xml
new file mode 100644
index 000000000..842d97b26
--- /dev/null
+++ b/quickstartgenerator_jetty121_ee11/pom.xml
@@ -0,0 +1,75 @@
+
+
+
+
+ 4.0.0
+
+ quickstartgenerator-jetty121-ee11
+
+
+ com.google.appengine
+ parent
+ 3.0.0-SNAPSHOT
+
+
+ jar
+ AppEngine :: quickstartgenerator Jetty121 EE11
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Quickstart generator for Jetty 12.1 EE11.
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-quickstart
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-xml
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-security
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-jndi
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-io
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-http
+ ${jetty121.version}
+
+
+
diff --git a/quickstartgenerator_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/QuickStartGenerator.java b/quickstartgenerator_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/QuickStartGenerator.java
new file mode 100644
index 000000000..bf9ea2a8e
--- /dev/null
+++ b/quickstartgenerator_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/QuickStartGenerator.java
@@ -0,0 +1,98 @@
+/*
+ * 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.development.jetty;
+
+import java.io.File;
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * Simple generator of the Jetty quickstart-web.xml based on an exploded War directory. The file, if
+ * present will be deleted before being regenerated.
+ */
+public class QuickStartGenerator {
+
+ /**
+ * 2 arguments are expected: the path to a Web Application Archive root directory. and the path to
+ * a webdefault.xml file.
+ */
+ public static void main(String[] args) {
+ if (args.length != 2) {
+ System.out.println("Usage: pass 2 arguments:");
+ System.out.println(" first argument contains the path to a web application");
+ System.out.println(" second argument contains the path to a webdefault.xml file.");
+ System.exit(1);
+ }
+ String path = args[0];
+ String webDefault = args[1];
+ File fpath = new File(path);
+ if (!fpath.exists()) {
+ System.out.println("Error: Web Application directory does not exist: " + fpath);
+ System.exit(1);
+ }
+ File fWebDefault = new File(webDefault);
+ if (!fWebDefault.exists()) {
+ System.out.println("Error: webdefault.xml file does not exist: " + fWebDefault);
+ System.exit(1);
+ }
+ fpath = new File(fpath, "WEB-INF");
+ if (!fpath.exists()) {
+ System.out.println("Error: Path does not exist: " + fpath);
+ System.exit(1);
+ }
+ // Keep Jetty silent for INFO messages.
+ System.setProperty("org.eclipse.jetty.server.LEVEL", "WARN");
+ System.setProperty("org.eclipse.jetty.quickstart.LEVEL", "WARN");
+ boolean success = generate(path, fWebDefault);
+ System.exit(success ? 0 : 1);
+ }
+
+ public static boolean generate(String appDir, File webDefault) {
+ // We delete possible previously generated quickstart-web.xml
+ File qs = new File(appDir, "WEB-INF/quickstart-web.xml");
+ if (qs.exists()) {
+ boolean deleted = IO.delete(qs);
+ if (!deleted) {
+ System.err.println("Error: File exists and cannot be deleted: " + qs);
+ return false;
+ }
+ }
+ try {
+ final Server server = new Server();
+ WebAppContext webapp = new WebAppContext();
+ webapp.setBaseResource(ResourceFactory.root().newResource(appDir));
+ webapp.addConfiguration(new QuickStartConfiguration());
+ webapp.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.GENERATE);
+ webapp.setDefaultsDescriptor(webDefault.getCanonicalPath());
+ server.setHandler(webapp);
+ server.start();
+ server.stop();
+ if (qs.exists()) {
+ return true;
+ } else {
+ System.out.println("Failed to generate " + qs);
+ return false;
+ }
+ } catch (Exception e) {
+ System.out.println("Error during quick start generation: " + e);
+ return false;
+ }
+ }
+}
diff --git a/quickstartgenerator_jetty121_ee8/pom.xml b/quickstartgenerator_jetty121_ee8/pom.xml
new file mode 100644
index 000000000..47fe19cd3
--- /dev/null
+++ b/quickstartgenerator_jetty121_ee8/pom.xml
@@ -0,0 +1,75 @@
+
+
+
+
+ 4.0.0
+
+ quickstartgenerator-jetty121-ee8
+
+
+ com.google.appengine
+ parent
+ 3.0.0-SNAPSHOT
+
+
+ jar
+ AppEngine :: quickstartgenerator Jetty121 EE8
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Quickstart generator for Jetty 12.1 EE8.
+
+
+ org.eclipse.jetty.ee8
+ jetty-ee8-quickstart
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-xml
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-security
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-jndi
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-io
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-http
+ ${jetty121.version}
+
+
+
diff --git a/quickstartgenerator_jetty121_ee8/src/main/java/com/google/appengine/tools/development/jetty/QuickStartGenerator.java b/quickstartgenerator_jetty121_ee8/src/main/java/com/google/appengine/tools/development/jetty/QuickStartGenerator.java
new file mode 100644
index 000000000..d5cf0ceb0
--- /dev/null
+++ b/quickstartgenerator_jetty121_ee8/src/main/java/com/google/appengine/tools/development/jetty/QuickStartGenerator.java
@@ -0,0 +1,98 @@
+/*
+ * 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.development.jetty;
+
+import java.io.File;
+import org.eclipse.jetty.ee8.quickstart.QuickStartConfiguration;
+import org.eclipse.jetty.ee8.webapp.WebAppContext;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * Simple generator of the Jetty quickstart-web.xml based on an exploded War directory. The file, if
+ * present will be deleted before being regenerated.
+ */
+public class QuickStartGenerator {
+
+ /**
+ * 2 arguments are expected: the path to a Web Application Archive root directory. and the path to
+ * a webdefault.xml file.
+ */
+ public static void main(String[] args) {
+ if (args.length != 2) {
+ System.out.println("Usage: pass 2 arguments:");
+ System.out.println(" first argument contains the path to a web application");
+ System.out.println(" second argument contains the path to a webdefault.xml file.");
+ System.exit(1);
+ }
+ String path = args[0];
+ String webDefault = args[1];
+ File fpath = new File(path);
+ if (!fpath.exists()) {
+ System.out.println("Error: Web Application directory does not exist: " + fpath);
+ System.exit(1);
+ }
+ File fWebDefault = new File(webDefault);
+ if (!fWebDefault.exists()) {
+ System.out.println("Error: webdefault.xml file does not exist: " + fWebDefault);
+ System.exit(1);
+ }
+ fpath = new File(fpath, "WEB-INF");
+ if (!fpath.exists()) {
+ System.out.println("Error: Path does not exist: " + fpath);
+ System.exit(1);
+ }
+ // Keep Jetty silent for INFO messages.
+ System.setProperty("org.eclipse.jetty.server.LEVEL", "WARN");
+ System.setProperty("org.eclipse.jetty.quickstart.LEVEL", "WARN");
+ boolean success = generate(path, fWebDefault);
+ System.exit(success ? 0 : 1);
+ }
+
+ public static boolean generate(String appDir, File webDefault) {
+ // We delete possible previously generated quickstart-web.xml
+ File qs = new File(appDir, "WEB-INF/quickstart-web.xml");
+ if (qs.exists()) {
+ boolean deleted = IO.delete(qs);
+ if (!deleted) {
+ System.err.println("Error: File exists and cannot be deleted: " + qs);
+ return false;
+ }
+ }
+ try {
+ final Server server = new Server();
+ WebAppContext webapp = new WebAppContext();
+ webapp.setBaseResource(ResourceFactory.root().newResource(appDir));
+ webapp.addConfiguration(new QuickStartConfiguration());
+ webapp.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.GENERATE);
+ webapp.setDefaultsDescriptor(webDefault.getCanonicalPath());
+ server.setHandler(webapp);
+ server.start();
+ server.stop();
+ if (qs.exists()) {
+ return true;
+ } else {
+ System.out.println("Failed to generate " + qs);
+ return false;
+ }
+ } catch (Exception e) {
+ System.out.println("Error during quick start generation: " + e);
+ return false;
+ }
+ }
+}
diff --git a/quickstartgenerator_jetty12_ee10/pom.xml b/quickstartgenerator_jetty12_ee10/pom.xml
index 9dcc83008..9c29c6fd8 100644
--- a/quickstartgenerator_jetty12_ee10/pom.xml
+++ b/quickstartgenerator_jetty12_ee10/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: quickstartgenerator Jetty12 EE10
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ Quickstart generator for Jetty 12 and EE10.org.eclipse.jetty.ee10
diff --git a/remoteapi/pom.xml b/remoteapi/pom.xml
index 37dc206ff..14325cd24 100644
--- a/remoteapi/pom.xml
+++ b/remoteapi/pom.xml
@@ -20,10 +20,12 @@
com.google.appengineparent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: appengine-remote-api
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ App Engine remote API.com.google.api-client
@@ -67,18 +69,6 @@
-
- ccom.google.api.client.googleapis.extensions.appengine.auth.oauth2
- com.google.appengine.repackaged.com.google.api.client.googleapis.extensions.appengine.auth.oauth2
-
-
- ccom.google.api.client.googleapis.extensions.appengine.testing.auth.oauth2
- com.google.appengine.repackaged.com.google.api.client.googleapis.extensions.appengine.testing.auth.oauth2
-
-
- ccom.google.api.client.googleapis.extensions.appengine.notifications
- com.google.appengine.repackaged.com.google.api.client.googleapis.extensions.appengine.notifications
- com.fasterxml.jacksoncom.google.appengine.repackaged.com.fasterxml.jackson
@@ -114,15 +104,19 @@
org.codehaus.jacksoncom.google.appengine.repackaged.org.codehaus.jackson
-
-
- com.google.apphosting.datastore.DatastoreV3Pb
- com.google.apphosting.api.DatastorePbcom.google.apphosting.datastore.proto2api.DatastoreV3Pbcom.google.apphosting.api.proto2api.DatastorePb
+
+ com.google.storage.onestore.v3.proto2api
+ com.google.appengine.repackaged.com.google.storage.onestore.v3.proto2api
+
+
+ com.google.protobuf
+ com.google.appengine.repackaged.com.google.protobuf
+
diff --git a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/AppEngineClient.java b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/AppEngineClient.java
index a92e50f39..43f3c72c4 100644
--- a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/AppEngineClient.java
+++ b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/AppEngineClient.java
@@ -21,7 +21,7 @@
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.http.cookie.Cookie;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Abstract class that handles making HTTP requests to App Engine using
diff --git a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/BaseRemoteApiClient.java b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/BaseRemoteApiClient.java
index 4897e893b..11a1656c8 100644
--- a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/BaseRemoteApiClient.java
+++ b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/BaseRemoteApiClient.java
@@ -18,7 +18,7 @@
import java.util.ArrayList;
import java.util.List;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Base implementation for Remote API clients.
diff --git a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiDelegate.java b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiDelegate.java
index aab583e28..376da2a1c 100644
--- a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiDelegate.java
+++ b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiDelegate.java
@@ -18,7 +18,7 @@
import com.google.apphosting.api.ApiProxy.Delegate;
import com.google.apphosting.api.ApiProxy.Environment;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Handles App Engine API calls by making HTTP requests to a remote server.
diff --git a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiInstaller.java b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiInstaller.java
index 80189bd91..a5c9d3a90 100644
--- a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiInstaller.java
+++ b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/RemoteApiInstaller.java
@@ -38,7 +38,7 @@
import java.util.regex.Pattern;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.cookie.BasicClientCookie;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Installs and uninstalls the remote API. While the RemoteApi is installed, all App Engine calls
diff --git a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/TransactionBuilder.java b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/TransactionBuilder.java
index 83d708890..8b28a2555 100644
--- a/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/TransactionBuilder.java
+++ b/remoteapi/src/main/java/com/google/appengine/tools/remoteapi/TransactionBuilder.java
@@ -29,7 +29,7 @@
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Map;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An in-progress transaction that will be sent via the remote API on commit.
@@ -88,8 +88,8 @@ public void addEntityAbsenceToCache(OnestoreEntity.Reference key) {
/**
* Returns a cached entity, or null if the entity's absence was cached.
*/
- @Nullable
- public OnestoreEntity.EntityProto getCachedEntity(OnestoreEntity.Reference key) {
+
+ public OnestoreEntity.@Nullable EntityProto getCachedEntity(OnestoreEntity.Reference key) {
ByteString keyBytes = key.toByteString();
if (!getCache.containsKey(keyBytes)) {
throw new IllegalStateException("entity's status unexpectedly not in cache");
@@ -151,7 +151,7 @@ public RemoteApiPb.TransactionRequest makeCommitRequest() {
Message.Builder newKey = result.getDeletesBuilder().addKeyBuilder();
boolean parsed = true;
try {
- newKey.mergeFrom(entry.getKey(), ExtensionRegistry.getGeneratedRegistry());
+ newKey.mergeFrom(entry.getKey(), ExtensionRegistry.getEmptyRegistry());
} catch (InvalidProtocolBufferException e) {
parsed = false;
}
@@ -170,7 +170,7 @@ private static RemoteApiPb.TransactionRequest.Precondition makeEntityNotFoundPre
OnestoreEntity.Reference.Builder ref = OnestoreEntity.Reference.newBuilder();
boolean parsed = true;
try {
- ref.mergeFrom(key, ExtensionRegistry.getGeneratedRegistry());
+ ref.mergeFrom(key, ExtensionRegistry.getEmptyRegistry());
} catch (InvalidProtocolBufferException e) {
parsed = false;
}
diff --git a/renovate.json b/renovate.json
index 50a0af031..38039bf44 100644
--- a/renovate.json
+++ b/renovate.json
@@ -8,20 +8,19 @@
"com.google.googlejavaformat:google-java-format",
"javax.servlet:javax.servlet-api",
"jakarta.servlet:jakarta.servlet-api",
- "io.grpc:grpc-stub",
"org.mortbay.jasper:apache-el",
"org.mortbay.jasper:apache-jsp",
"org.apache.lucene:lucene-core",
"javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api",
"com.google.protobuf:protobuf-java",
"com.google.protobuf:protobuf-java-util",
- "org.eclipse.jetty.toolchain:jetty-schemas",
"com.google.api.grpc:proto-google-common-protos",
"com.google.api.grpc:proto-google-cloud-datastore-v1",
"com.google.cloud.datastore:datastore-v1-proto-client",
"com.google.appengine:geronimo-javamail_1.4_spec",
"com.google.cloud.sql:mysql-socket-factory",
- "org.eclipse.jetty:",
+ "org.eclipse.jetty:*",
+ "org.eclipse.jetty.*:*",
"org.springframework.boot:spring-boot-starter-parent",
"org.springframework.boot:spring-boot-starter-web"
],
diff --git a/runtime/annotationscanningwebapp/pom.xml b/runtime/annotationscanningwebapp/pom.xml
index 9bc760d94..eba1fa5d9 100644
--- a/runtime/annotationscanningwebapp/pom.xml
+++ b/runtime/annotationscanningwebapp/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineruntime-parent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTcom.google.appengine.demosannotationscanningwebappAppEngine :: annotationscanningwebapp
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ A web application for annotation scanning.true
@@ -64,7 +66,7 @@
maven-compiler-plugin
- 3.13.0
+ 3.14.08
diff --git a/runtime/annotationscanningwebappjakarta/pom.xml b/runtime/annotationscanningwebappjakarta/pom.xml
new file mode 100644
index 000000000..b603f499f
--- /dev/null
+++ b/runtime/annotationscanningwebappjakarta/pom.xml
@@ -0,0 +1,77 @@
+
+
+
+
+ 4.0.0
+ war
+
+ com.google.appengine
+ runtime-parent
+ 3.0.0-SNAPSHOT
+
+ com.google.appengine.demos
+ annotationscanningwebappjakarta
+ AppEngine :: annotationscanningwebapp jakarta
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ A web application for annotation scanning (Jakarta).
+
+
+ true
+ 1
+ UTF-8
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 6.0.0
+ provided
+
+
+
+ target/${project.artifactId}-${project.version}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.4.0
+
+ true
+
+
+
+ ${basedir}/src/main/webapp/WEB-INF
+ true
+ WEB-INF
+
+
+
+
+
+ maven-compiler-plugin
+ 3.14.0
+
+ 8
+
+
+
+
+
+
diff --git a/runtime/annotationscanningwebappjakarta/src/main/java/AnnotationScanningServlet.java b/runtime/annotationscanningwebappjakarta/src/main/java/AnnotationScanningServlet.java
new file mode 100644
index 000000000..451758c8c
--- /dev/null
+++ b/runtime/annotationscanningwebappjakarta/src/main/java/AnnotationScanningServlet.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.
+ */
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/** Servlet that detects if the GAE APIs are in the app classpath. */
+@WebServlet(
+ name = "AnnotationScanningServlet",
+ urlPatterns = {"/"})
+public class AnnotationScanningServlet extends HttpServlet {
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ resp.setContentType("text/plain");
+ PrintWriter out = resp.getWriter();
+ // Testing that appengine-api-1.0-sdk.jar is not on the application classpath
+ // if the app does not define it.
+ try {
+ Class.forName("com.google.appengine.api.utils.SystemProperty");
+ throw new IllegalArgumentException("com.google.appengine.api.utils.SystemProperty");
+
+ } catch (ClassNotFoundException expected) {
+ out.println("ok, com.google.appengine.api.utils.SystemProperty not seen.");
+ }
+ }
+}
diff --git a/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine-generated/app.yaml b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine-generated/app.yaml
new file mode 100644
index 000000000..905010d4d
--- /dev/null
+++ b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine-generated/app.yaml
@@ -0,0 +1,27 @@
+# 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.
+#
+runtime: java21
+inbound_services:
+- warmup
+derived_file_type:
+- java_precompiled
+threadsafe: True
+auto_id_policy: default
+api_version: 'user_defined'
+handlers:
+- url: /.*
+ script: unused
+ login: optional
+ secure: optional
diff --git a/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine-web.xml b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..897e167e2
--- /dev/null
+++ b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,19 @@
+
+
+
+ java21
+
diff --git a/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine_optional.properties b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine_optional.properties
new file mode 100644
index 000000000..9306fa2f8
--- /dev/null
+++ b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/appengine_optional.properties
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+use.annotationscanning=true
\ No newline at end of file
diff --git a/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/web.xml b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..492979e92
--- /dev/null
+++ b/runtime/annotationscanningwebappjakarta/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/runtime/deployment/pom.xml b/runtime/deployment/pom.xml
index 47d1c3805..bb12d2437 100644
--- a/runtime/deployment/pom.xml
+++ b/runtime/deployment/pom.xml
@@ -22,11 +22,12 @@
com.google.appengineruntime-parent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTpomAppEngine :: runtime-deployment
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/Produces an output directory in the format expected by the Java runtime.
@@ -40,6 +41,11 @@
runtime-impl-jetty12${project.version}
+
+ com.google.appengine
+ runtime-impl-jetty121
+ ${project.version}
+ com.google.appengineruntime-main
@@ -59,7 +65,17 @@
runtime-shared-jetty12-ee10${project.version}
-
+
+ com.google.appengine
+ runtime-shared-jetty121-ee8
+ ${project.version}
+
+
+ com.google.appengine
+ runtime-shared-jetty121-ee11
+ ${project.version}
+
+
diff --git a/runtime/deployment/src/assembly/component.xml b/runtime/deployment/src/assembly/component.xml
index dee2fef22..ebfe37f7d 100644
--- a/runtime/deployment/src/assembly/component.xml
+++ b/runtime/deployment/src/assembly/component.xml
@@ -25,10 +25,13 @@
com.google.appengine:runtime-impl-jetty9com.google.appengine:runtime-impl-jetty12
+ com.google.appengine:runtime-impl-jetty121com.google.appengine:runtime-maincom.google.appengine:runtime-shared-jetty9com.google.appengine:runtime-shared-jetty12com.google.appengine:runtime-shared-jetty12-ee10
+ com.google.appengine:runtime-shared-jetty121-ee8
+ com.google.appengine:runtime-shared-jetty121-ee11
diff --git a/runtime/failinitfilterwebapp/pom.xml b/runtime/failinitfilterwebapp/pom.xml
index 25c9b8428..d24ab2e15 100644
--- a/runtime/failinitfilterwebapp/pom.xml
+++ b/runtime/failinitfilterwebapp/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineruntime-parent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTcom.google.appengine.demosfailinitfilterwebappAppEngine :: failinitfilterwebapp
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ A web application with a failing init filter.true
@@ -64,7 +66,7 @@
maven-compiler-plugin
- 3.13.0
+ 3.14.08
diff --git a/runtime/failinitfilterwebappjakarta/pom.xml b/runtime/failinitfilterwebappjakarta/pom.xml
new file mode 100644
index 000000000..513419ae4
--- /dev/null
+++ b/runtime/failinitfilterwebappjakarta/pom.xml
@@ -0,0 +1,88 @@
+
+
+
+
+ 4.0.0
+ war
+
+ com.google.appengine
+ runtime-parent
+ 3.0.0-SNAPSHOT
+
+ com.google.appengine.demos
+ failinitfilterwebappjakarta
+ AppEngine :: failinitfilterwebapp jakarta
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ A web application with a failing init filter (Jakarta).
+
+
+ true
+ 1
+ UTF-8
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 6.0.0
+ provided
+
+
+
+ target/${project.artifactId}-${project.version}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.4.0
+
+ true
+
+
+
+ ${basedir}/src/main/webapp/WEB-INF
+ true
+ WEB-INF
+
+
+
+
+
+ maven-compiler-plugin
+ 3.14.0
+
+ 8
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.8.3
+
+ ludo-in-in
+ failinitfilter
+ false
+ true
+
+
+
+
+
+
diff --git a/runtime/failinitfilterwebappjakarta/src/main/java/FailInitializationFilter.java b/runtime/failinitfilterwebappjakarta/src/main/java/FailInitializationFilter.java
new file mode 100644
index 000000000..4c830bb98
--- /dev/null
+++ b/runtime/failinitfilterwebappjakarta/src/main/java/FailInitializationFilter.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+public class FailInitializationFilter implements Filter {
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ throw new ServletException("Intentionally failing to initialize.");
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+ throws ServletException {
+ throw new ServletException("Unexpectedly got a request.");
+ }
+
+ @Override
+ public void destroy() {}
+}
diff --git a/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/appengine-generated/app.yaml b/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/appengine-generated/app.yaml
new file mode 100644
index 000000000..f0fd69eed
--- /dev/null
+++ b/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/appengine-generated/app.yaml
@@ -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.
+#
+runtime: java21
+inbound_services:
+- warmup
+threadsafe: True
+auto_id_policy: default
+api_version: 'user_defined'
+handlers:
+- url: /.*
+ script: unused
+ login: optional
+ secure: optional
diff --git a/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/appengine-web.xml b/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000..897e167e2
--- /dev/null
+++ b/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,19 @@
+
+
+
+ java21
+
diff --git a/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/web.xml b/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..281a06aee
--- /dev/null
+++ b/runtime/failinitfilterwebappjakarta/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ FailInitializationFilter
+ FailInitializationFilter
+
+
+ FailInitializationFilter
+ /*
+
+
diff --git a/runtime/impl/pom.xml b/runtime/impl/pom.xml
index 4ffddd813..7a10b8637 100644
--- a/runtime/impl/pom.xml
+++ b/runtime/impl/pom.xml
@@ -23,11 +23,13 @@
com.google.appengineruntime-parent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: runtime-impl
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ App Engine runtime implementation.
@@ -114,26 +116,6 @@
shared-sdktrue
-
- io.grpc
- grpc-api
- true
-
-
- io.grpc
- grpc-stub
- true
-
-
- io.grpc
- grpc-protobuf
- true
-
-
- io.grpc
- grpc-netty
- true
- com.fasterxml.jackson.corejackson-core
@@ -175,47 +157,6 @@
true
-
-
- io.netty
- netty-buffer
- true
-
-
- io.netty
- netty-codec
- true
-
-
- io.netty
- netty-codec-http
- true
-
-
- io.netty
- netty-codec-http2
- true
-
-
- io.netty
- netty-common
- true
-
-
- io.netty
- netty-handler
- true
-
-
- io.netty
- netty-transport
- true
-
-
- io.netty
- netty-transport-native-unix-common
- true
- com.google.appengine
diff --git a/runtime/impl/src/main/java/com/google/apphosting/base/VersionId.java b/runtime/impl/src/main/java/com/google/apphosting/base/VersionId.java
index c6e2f8daf..38a260d1f 100644
--- a/runtime/impl/src/main/java/com/google/apphosting/base/VersionId.java
+++ b/runtime/impl/src/main/java/com/google/apphosting/base/VersionId.java
@@ -19,7 +19,7 @@
import com.google.auto.value.AutoValue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A parsed Version Id.
diff --git a/runtime/impl/src/main/java/com/google/apphosting/base/protos/CloneControllerGrpc.java b/runtime/impl/src/main/java/com/google/apphosting/base/protos/CloneControllerGrpc.java
deleted file mode 100755
index 9c712ff0f..000000000
--- a/runtime/impl/src/main/java/com/google/apphosting/base/protos/CloneControllerGrpc.java
+++ /dev/null
@@ -1,706 +0,0 @@
-/*
- * 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.apphosting.base.protos;
-
-import static io.grpc.MethodDescriptor.generateFullMethodName;
-
-/** */
-@io.grpc.stub.annotations.GrpcGenerated
-public final class CloneControllerGrpc {
-
- private CloneControllerGrpc() {}
-
- public static final String SERVICE_NAME = "apphosting.CloneController";
-
- // Static method descriptors that strictly reflect the proto.
- private static volatile io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.EmptyMessage,
- com.google.apphosting.base.protos.EmptyMessage>
- getWaitForSandboxMethod;
-
- @io.grpc.stub.annotations.RpcMethod(
- fullMethodName = SERVICE_NAME + '/' + "WaitForSandbox",
- requestType = com.google.apphosting.base.protos.EmptyMessage.class,
- responseType = com.google.apphosting.base.protos.EmptyMessage.class,
- methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
- public static io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.EmptyMessage,
- com.google.apphosting.base.protos.EmptyMessage>
- getWaitForSandboxMethod() {
- io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.EmptyMessage,
- com.google.apphosting.base.protos.EmptyMessage>
- getWaitForSandboxMethod;
- if ((getWaitForSandboxMethod = CloneControllerGrpc.getWaitForSandboxMethod) == null) {
- synchronized (CloneControllerGrpc.class) {
- if ((getWaitForSandboxMethod = CloneControllerGrpc.getWaitForSandboxMethod) == null) {
- CloneControllerGrpc.getWaitForSandboxMethod =
- getWaitForSandboxMethod =
- io.grpc.MethodDescriptor
- .
- newBuilder()
- .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
- .setFullMethodName(generateFullMethodName(SERVICE_NAME, "WaitForSandbox"))
- .setSampledToLocalTracing(true)
- .setRequestMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.EmptyMessage.getDefaultInstance()))
- .setResponseMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.EmptyMessage.getDefaultInstance()))
- .setSchemaDescriptor(
- new CloneControllerMethodDescriptorSupplier("WaitForSandbox"))
- .build();
- }
- }
- }
- return getWaitForSandboxMethod;
- }
-
- private static volatile io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ClonePb.CloneSettings,
- com.google.apphosting.base.protos.EmptyMessage>
- getApplyCloneSettingsMethod;
-
- @io.grpc.stub.annotations.RpcMethod(
- fullMethodName = SERVICE_NAME + '/' + "ApplyCloneSettings",
- requestType = com.google.apphosting.base.protos.ClonePb.CloneSettings.class,
- responseType = com.google.apphosting.base.protos.EmptyMessage.class,
- methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
- public static io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ClonePb.CloneSettings,
- com.google.apphosting.base.protos.EmptyMessage>
- getApplyCloneSettingsMethod() {
- io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ClonePb.CloneSettings,
- com.google.apphosting.base.protos.EmptyMessage>
- getApplyCloneSettingsMethod;
- if ((getApplyCloneSettingsMethod = CloneControllerGrpc.getApplyCloneSettingsMethod) == null) {
- synchronized (CloneControllerGrpc.class) {
- if ((getApplyCloneSettingsMethod = CloneControllerGrpc.getApplyCloneSettingsMethod)
- == null) {
- CloneControllerGrpc.getApplyCloneSettingsMethod =
- getApplyCloneSettingsMethod =
- io.grpc.MethodDescriptor
- .
- newBuilder()
- .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
- .setFullMethodName(generateFullMethodName(SERVICE_NAME, "ApplyCloneSettings"))
- .setSampledToLocalTracing(true)
- .setRequestMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.ClonePb.CloneSettings
- .getDefaultInstance()))
- .setResponseMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.EmptyMessage.getDefaultInstance()))
- .setSchemaDescriptor(
- new CloneControllerMethodDescriptorSupplier("ApplyCloneSettings"))
- .build();
- }
- }
- }
- return getApplyCloneSettingsMethod;
- }
-
- private static volatile io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ModelClonePb.DeadlineInfo,
- com.google.apphosting.base.protos.EmptyMessage>
- getSendDeadlineMethod;
-
- @io.grpc.stub.annotations.RpcMethod(
- fullMethodName = SERVICE_NAME + '/' + "SendDeadline",
- requestType = com.google.apphosting.base.protos.ModelClonePb.DeadlineInfo.class,
- responseType = com.google.apphosting.base.protos.EmptyMessage.class,
- methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
- public static io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ModelClonePb.DeadlineInfo,
- com.google.apphosting.base.protos.EmptyMessage>
- getSendDeadlineMethod() {
- io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ModelClonePb.DeadlineInfo,
- com.google.apphosting.base.protos.EmptyMessage>
- getSendDeadlineMethod;
- if ((getSendDeadlineMethod = CloneControllerGrpc.getSendDeadlineMethod) == null) {
- synchronized (CloneControllerGrpc.class) {
- if ((getSendDeadlineMethod = CloneControllerGrpc.getSendDeadlineMethod) == null) {
- CloneControllerGrpc.getSendDeadlineMethod =
- getSendDeadlineMethod =
- io.grpc.MethodDescriptor
- .
- newBuilder()
- .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
- .setFullMethodName(generateFullMethodName(SERVICE_NAME, "SendDeadline"))
- .setSampledToLocalTracing(true)
- .setRequestMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.ModelClonePb.DeadlineInfo
- .getDefaultInstance()))
- .setResponseMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.EmptyMessage.getDefaultInstance()))
- .setSchemaDescriptor(
- new CloneControllerMethodDescriptorSupplier("SendDeadline"))
- .build();
- }
- }
- }
- return getSendDeadlineMethod;
- }
-
- private static volatile io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ModelClonePb.PerformanceDataRequest,
- com.google.apphosting.base.protos.ClonePb.PerformanceData>
- getGetPerformanceDataMethod;
-
- @io.grpc.stub.annotations.RpcMethod(
- fullMethodName = SERVICE_NAME + '/' + "GetPerformanceData",
- requestType = com.google.apphosting.base.protos.ModelClonePb.PerformanceDataRequest.class,
- responseType = com.google.apphosting.base.protos.ClonePb.PerformanceData.class,
- methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
- public static io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ModelClonePb.PerformanceDataRequest,
- com.google.apphosting.base.protos.ClonePb.PerformanceData>
- getGetPerformanceDataMethod() {
- io.grpc.MethodDescriptor<
- com.google.apphosting.base.protos.ModelClonePb.PerformanceDataRequest,
- com.google.apphosting.base.protos.ClonePb.PerformanceData>
- getGetPerformanceDataMethod;
- if ((getGetPerformanceDataMethod = CloneControllerGrpc.getGetPerformanceDataMethod) == null) {
- synchronized (CloneControllerGrpc.class) {
- if ((getGetPerformanceDataMethod = CloneControllerGrpc.getGetPerformanceDataMethod)
- == null) {
- CloneControllerGrpc.getGetPerformanceDataMethod =
- getGetPerformanceDataMethod =
- io.grpc.MethodDescriptor
- .
- newBuilder()
- .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
- .setFullMethodName(generateFullMethodName(SERVICE_NAME, "GetPerformanceData"))
- .setSampledToLocalTracing(true)
- .setRequestMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.ModelClonePb.PerformanceDataRequest
- .getDefaultInstance()))
- .setResponseMarshaller(
- io.grpc.protobuf.ProtoUtils.marshaller(
- com.google.apphosting.base.protos.ClonePb.PerformanceData
- .getDefaultInstance()))
- .setSchemaDescriptor(
- new CloneControllerMethodDescriptorSupplier("GetPerformanceData"))
- .build();
- }
- }
- }
- return getGetPerformanceDataMethod;
- }
-
- /** Creates a new async stub that supports all call types for the service */
- public static CloneControllerStub newStub(io.grpc.Channel channel) {
- io.grpc.stub.AbstractStub.StubFactory factory =
- new io.grpc.stub.AbstractStub.StubFactory() {
- @java.lang.Override
- public CloneControllerStub newStub(
- io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
- return new CloneControllerStub(channel, callOptions);
- }
- };
- return CloneControllerStub.newStub(factory, channel);
- }
-
- /**
- * Creates a new blocking-style stub that supports unary and streaming output calls on the service
- */
- public static CloneControllerBlockingStub newBlockingStub(io.grpc.Channel channel) {
- io.grpc.stub.AbstractStub.StubFactory factory =
- new io.grpc.stub.AbstractStub.StubFactory() {
- @java.lang.Override
- public CloneControllerBlockingStub newStub(
- io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
- return new CloneControllerBlockingStub(channel, callOptions);
- }
- };
- return CloneControllerBlockingStub.newStub(factory, channel);
- }
-
- /** Creates a new ListenableFuture-style stub that supports unary calls on the service */
- public static CloneControllerFutureStub newFutureStub(io.grpc.Channel channel) {
- io.grpc.stub.AbstractStub.StubFactory factory =
- new io.grpc.stub.AbstractStub.StubFactory() {
- @java.lang.Override
- public CloneControllerFutureStub newStub(
- io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
- return new CloneControllerFutureStub(channel, callOptions);
- }
- };
- return CloneControllerFutureStub.newStub(factory, channel);
- }
-
- /** */
- public abstract static class CloneControllerImplBase implements io.grpc.BindableService {
-
- /**
- *
- *
- *
- * Asks the Clone to put itself into the stopped state, by sending
- * itself a SIGSTOP when it is safe to do so. The Clone will be
- * Sandboxed and resume from this point.
- *
- * Asks the Clone to put itself into the stopped state, by sending
- * itself a SIGSTOP when it is safe to do so. The Clone will be
- * Sandboxed and resume from this point.
- *
- * Asks the Clone to put itself into the stopped state, by sending
- * itself a SIGSTOP when it is safe to do so. The Clone will be
- * Sandboxed and resume from this point.
- *
- * Asks the Clone to put itself into the stopped state, by sending
- * itself a SIGSTOP when it is safe to do so. The Clone will be
- * Sandboxed and resume from this point.
- *
- */
- public com.google.common.util.concurrent.ListenableFuture<
- com.google.apphosting.base.protos.ClonePb.PerformanceData>
- getPerformanceData(
- com.google.apphosting.base.protos.ModelClonePb.PerformanceDataRequest request) {
- return io.grpc.stub.ClientCalls.futureUnaryCall(
- getChannel().newCall(getGetPerformanceDataMethod(), getCallOptions()), request);
- }
- }
-
- private static final int METHODID_WAIT_FOR_SANDBOX = 0;
- private static final int METHODID_APPLY_CLONE_SETTINGS = 1;
- private static final int METHODID_SEND_DEADLINE = 2;
- private static final int METHODID_GET_PERFORMANCE_DATA = 3;
-
- private static final class MethodHandlers
- implements io.grpc.stub.ServerCalls.UnaryMethod,
- io.grpc.stub.ServerCalls.ServerStreamingMethod,
- io.grpc.stub.ServerCalls.ClientStreamingMethod,
- io.grpc.stub.ServerCalls.BidiStreamingMethod {
- private final CloneControllerImplBase serviceImpl;
- private final int methodId;
-
- MethodHandlers(CloneControllerImplBase serviceImpl, int methodId) {
- this.serviceImpl = serviceImpl;
- this.methodId = methodId;
- }
-
- @java.lang.Override
- @java.lang.SuppressWarnings("unchecked")
- public void invoke(Req request, io.grpc.stub.StreamObserver responseObserver) {
- switch (methodId) {
- case METHODID_WAIT_FOR_SANDBOX:
- serviceImpl.waitForSandbox(
- (com.google.apphosting.base.protos.EmptyMessage) request,
- (io.grpc.stub.StreamObserver)
- responseObserver);
- break;
- case METHODID_APPLY_CLONE_SETTINGS:
- serviceImpl.applyCloneSettings(
- (com.google.apphosting.base.protos.ClonePb.CloneSettings) request,
- (io.grpc.stub.StreamObserver)
- responseObserver);
- break;
- case METHODID_SEND_DEADLINE:
- serviceImpl.sendDeadline(
- (com.google.apphosting.base.protos.ModelClonePb.DeadlineInfo) request,
- (io.grpc.stub.StreamObserver)
- responseObserver);
- break;
- case METHODID_GET_PERFORMANCE_DATA:
- serviceImpl.getPerformanceData(
- (com.google.apphosting.base.protos.ModelClonePb.PerformanceDataRequest) request,
- (io.grpc.stub.StreamObserver<
- com.google.apphosting.base.protos.ClonePb.PerformanceData>)
- responseObserver);
- break;
- default:
- throw new AssertionError();
- }
- }
-
- @java.lang.Override
- @java.lang.SuppressWarnings("unchecked")
- public io.grpc.stub.StreamObserver invoke(
- io.grpc.stub.StreamObserver responseObserver) {
- switch (methodId) {
- default:
- throw new AssertionError();
- }
- }
- }
-
- private abstract static class CloneControllerBaseDescriptorSupplier
- implements io.grpc.protobuf.ProtoFileDescriptorSupplier,
- io.grpc.protobuf.ProtoServiceDescriptorSupplier {
- CloneControllerBaseDescriptorSupplier() {}
-
- @java.lang.Override
- public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() {
- return com.google.apphosting.base.protos.ModelClonePb.getDescriptor();
- }
-
- @java.lang.Override
- public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() {
- return getFileDescriptor().findServiceByName("CloneController");
- }
- }
-
- private static final class CloneControllerFileDescriptorSupplier
- extends CloneControllerBaseDescriptorSupplier {
- CloneControllerFileDescriptorSupplier() {}
- }
-
- private static final class CloneControllerMethodDescriptorSupplier
- extends CloneControllerBaseDescriptorSupplier
- implements io.grpc.protobuf.ProtoMethodDescriptorSupplier {
- private final String methodName;
-
- CloneControllerMethodDescriptorSupplier(String methodName) {
- this.methodName = methodName;
- }
-
- @java.lang.Override
- public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() {
- return getServiceDescriptor().findMethodByName(methodName);
- }
- }
-
- private static volatile io.grpc.ServiceDescriptor serviceDescriptor;
-
- public static io.grpc.ServiceDescriptor getServiceDescriptor() {
- io.grpc.ServiceDescriptor result = serviceDescriptor;
- if (result == null) {
- synchronized (CloneControllerGrpc.class) {
- result = serviceDescriptor;
- if (result == null) {
- serviceDescriptor =
- result =
- io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME)
- .setSchemaDescriptor(new CloneControllerFileDescriptorSupplier())
- .addMethod(getWaitForSandboxMethod())
- .addMethod(getApplyCloneSettingsMethod())
- .addMethod(getSendDeadlineMethod())
- .addMethod(getGetPerformanceDataMethod())
- .build();
- }
- }
- }
- return result;
- }
-}
diff --git a/runtime/impl/src/main/java/com/google/apphosting/base/protos/EvaluationRuntimeGrpc.java b/runtime/impl/src/main/java/com/google/apphosting/base/protos/EvaluationRuntimeGrpc.java
deleted file mode 100755
index f291ad514..000000000
--- a/runtime/impl/src/main/java/com/google/apphosting/base/protos/EvaluationRuntimeGrpc.java
+++ /dev/null
@@ -1,656 +0,0 @@
-/*
- * 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.apphosting.base.protos;
-
-import static io.grpc.MethodDescriptor.generateFullMethodName;
-
-/**
- *
- *
- *
- * A service for evaluating HTTP requests. This service is implemented by
- * all the App Engine runtimes. Note that all our existing sandbox/VM
- * environments only support a single app version at a time, despite the
- * multi-app-version capability implied by this interface.
- * TODO: Consider changing the interface to not suggest that it can
- * support multiple app versions. This would probably make the code less
- * confusing. Related to that, there's no reason why the AppServer-side of
- * the runtime needs to inherit from this interface. To the extent that it
- * really does need similar methods, it can define its own local (non-RPC)
- * versions of those interfaces.
- *
- * A service for evaluating HTTP requests. This service is implemented by
- * all the App Engine runtimes. Note that all our existing sandbox/VM
- * environments only support a single app version at a time, despite the
- * multi-app-version capability implied by this interface.
- * TODO: Consider changing the interface to not suggest that it can
- * support multiple app versions. This would probably make the code less
- * confusing. Related to that, there's no reason why the AppServer-side of
- * the runtime needs to inherit from this interface. To the extent that it
- * really does need similar methods, it can define its own local (non-RPC)
- * versions of those interfaces.
- *
- */
- public abstract static class EvaluationRuntimeImplBase implements io.grpc.BindableService {
-
- /**
- *
- *
- *
- * Given information an application and an HTTP request, execute the
- * request and prepare a response for the user.
- *
- * A service for evaluating HTTP requests. This service is implemented by
- * all the App Engine runtimes. Note that all our existing sandbox/VM
- * environments only support a single app version at a time, despite the
- * multi-app-version capability implied by this interface.
- * TODO: Consider changing the interface to not suggest that it can
- * support multiple app versions. This would probably make the code less
- * confusing. Related to that, there's no reason why the AppServer-side of
- * the runtime needs to inherit from this interface. To the extent that it
- * really does need similar methods, it can define its own local (non-RPC)
- * versions of those interfaces.
- *
- * A service for evaluating HTTP requests. This service is implemented by
- * all the App Engine runtimes. Note that all our existing sandbox/VM
- * environments only support a single app version at a time, despite the
- * multi-app-version capability implied by this interface.
- * TODO: Consider changing the interface to not suggest that it can
- * support multiple app versions. This would probably make the code less
- * confusing. Related to that, there's no reason why the AppServer-side of
- * the runtime needs to inherit from this interface. To the extent that it
- * really does need similar methods, it can define its own local (non-RPC)
- * versions of those interfaces.
- *
- * A service for evaluating HTTP requests. This service is implemented by
- * all the App Engine runtimes. Note that all our existing sandbox/VM
- * environments only support a single app version at a time, despite the
- * multi-app-version capability implied by this interface.
- * TODO: Consider changing the interface to not suggest that it can
- * support multiple app versions. This would probably make the code less
- * confusing. Related to that, there's no reason why the AppServer-side of
- * the runtime needs to inherit from this interface. To the extent that it
- * really does need similar methods, it can define its own local (non-RPC)
- * versions of those interfaces.
- *
- *
- * where the {@code methodName}, {@code RequestType}, and {@code ResponseType} depend on the
- * method and the other types are fixed.
- *
- *
We construct a map from {@code methodName} to {@code RequestType} for the gRPC class and
- * the AnyRpc interface, and check that the two maps are the same.
- *
- *
The {@code FooImplBase} class is the one that a gRPC server for the service in question is
- * supposed to implement, and if this test fails it probably means that the service has acquired
- * additional methods that we haven't added to the AnyRpc interface yet.
- *
- *
The {@code ResponseType} isn't referenced in the AnyRpc interface so we don't check it. But
- * if it did change then our gRPC server implementation would no longer compile.
- */
- private static void check(Class> gRpcClass, Class> anyRpcInterface) {
- assertThat(anyRpcInterface.isInterface()).isTrue();
- ImmutableSortedMap> gRpcMethods =
- stream(gRpcClass.getMethods())
- .filter(
- m ->
- m.getParameterTypes().length == 2
- && m.getParameterTypes()[1] == StreamObserver.class)
- .collect(
- toImmutableSortedMap(
- naturalOrder(), Method::getName, m -> m.getParameterTypes()[0]));
- ImmutableSortedMap> anyRpcMethods =
- stream(anyRpcInterface.getMethods())
- .filter(
- m ->
- m.getParameterTypes().length == 2
- && m.getParameterTypes()[0] == AnyRpcServerContext.class)
- .collect(
- toImmutableSortedMap(
- naturalOrder(), Method::getName, m -> m.getParameterTypes()[1]));
- assertThat(anyRpcMethods).isNotEmpty();
- assertThat(anyRpcMethods).isEqualTo(gRpcMethods);
- }
-}
diff --git a/runtime/impl/src/test/java/com/google/apphosting/runtime/anyrpc/GrpcClients.java b/runtime/impl/src/test/java/com/google/apphosting/runtime/anyrpc/GrpcClients.java
deleted file mode 100644
index c7ce7d92f..000000000
--- a/runtime/impl/src/test/java/com/google/apphosting/runtime/anyrpc/GrpcClients.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.apphosting.runtime.anyrpc;
-
-import com.google.apphosting.base.protos.AppinfoPb;
-import com.google.apphosting.base.protos.CloneControllerGrpc;
-import com.google.apphosting.base.protos.ClonePb;
-import com.google.apphosting.base.protos.EmptyMessage;
-import com.google.apphosting.base.protos.EvaluationRuntimeGrpc;
-import com.google.apphosting.base.protos.ModelClonePb;
-import com.google.apphosting.base.protos.RuntimePb;
-import com.google.apphosting.runtime.anyrpc.ClientInterfaces.CloneControllerClient;
-import com.google.apphosting.runtime.anyrpc.ClientInterfaces.EvaluationRuntimeClient;
-import com.google.apphosting.runtime.grpc.CallbackStreamObserver;
-import com.google.apphosting.runtime.grpc.GrpcClientContext;
-import io.grpc.Channel;
-import io.grpc.stub.StreamObserver;
-
-/**
- * gRPC implementations of the RPC client interfaces in {@link ClientInterfaces}. These are purely
- * for test purposes, since the real runtime is never a client of these RPC services. But having
- * both client and server implementations allows us to test round-trip behaviour.
- *
- */
-class GrpcClients {
- // There are no instances of this class.
- private GrpcClients() {}
-
- static class GrpcEvaluationRuntimeClient implements EvaluationRuntimeClient {
- private final Channel channel;
-
- GrpcEvaluationRuntimeClient(Channel channel) {
- this.channel = channel;
- }
-
- @Override
- public void handleRequest(
- AnyRpcClientContext ctx,
- RuntimePb.UPRequest req,
- AnyRpcCallback callback) {
- GrpcClientContext grpcContext = (GrpcClientContext) ctx;
- StreamObserver streamObserver =
- CallbackStreamObserver.of(grpcContext, callback);
- grpcContext.call(
- channel, EvaluationRuntimeGrpc.getHandleRequestMethod(), req, streamObserver);
- }
-
- @Override
- public void addAppVersion(
- AnyRpcClientContext ctx, AppinfoPb.AppInfo req, AnyRpcCallback callback) {
- GrpcClientContext grpcContext = (GrpcClientContext) ctx;
- StreamObserver streamObserver =
- CallbackStreamObserver.of(grpcContext, callback);
- grpcContext.call(
- channel, EvaluationRuntimeGrpc.getAddAppVersionMethod(), req, streamObserver);
- }
-
- @Override
- public void deleteAppVersion(
- AnyRpcClientContext ctx, AppinfoPb.AppInfo req, AnyRpcCallback callback) {
- GrpcClientContext grpcContext = (GrpcClientContext) ctx;
- StreamObserver streamObserver =
- CallbackStreamObserver.of(grpcContext, callback);
- grpcContext.call(
- channel, EvaluationRuntimeGrpc.getDeleteAppVersionMethod(), req, streamObserver);
- }
- }
-
- static class GrpcCloneControllerClient implements CloneControllerClient {
- private static final EmptyMessage GRPC_EMPTY_MESSAGE =
- EmptyMessage.getDefaultInstance();
-
- private final Channel channel;
-
- GrpcCloneControllerClient(Channel channel) {
- this.channel = channel;
- }
-
- @Override
- public void waitForSandbox(
- AnyRpcClientContext ctx,
- EmptyMessage req,
- AnyRpcCallback callback) {
- GrpcClientContext grpcContext = (GrpcClientContext) ctx;
- StreamObserver streamObserver =
- CallbackStreamObserver.of(grpcContext, callback);
- grpcContext.call(
- channel,
- CloneControllerGrpc.getWaitForSandboxMethod(),
- GRPC_EMPTY_MESSAGE,
- streamObserver);
- }
-
- @Override
- public void applyCloneSettings(
- AnyRpcClientContext ctx,
- ClonePb.CloneSettings req,
- AnyRpcCallback callback) {
- GrpcClientContext grpcContext = (GrpcClientContext) ctx;
- StreamObserver streamObserver =
- CallbackStreamObserver.of(grpcContext, callback);
- grpcContext.call(
- channel, CloneControllerGrpc.getApplyCloneSettingsMethod(), req, streamObserver);
- }
-
- @Override
- public void sendDeadline(
- AnyRpcClientContext ctx,
- ModelClonePb.DeadlineInfo req,
- AnyRpcCallback callback) {
- GrpcClientContext grpcContext = (GrpcClientContext) ctx;
- StreamObserver streamObserver =
- CallbackStreamObserver.of(grpcContext, callback);
- grpcContext.call(
- channel, CloneControllerGrpc.getSendDeadlineMethod(), req, streamObserver);
- }
-
- @Override
- public void getPerformanceData(
- AnyRpcClientContext ctx,
- ModelClonePb.PerformanceDataRequest req,
- AnyRpcCallback callback) {
- GrpcClientContext grpcContext = (GrpcClientContext) ctx;
- StreamObserver streamObserver =
- CallbackStreamObserver.of(grpcContext, callback);
- grpcContext.call(
- channel,
- CloneControllerGrpc.getGetPerformanceDataMethod(),
- req,
- streamObserver);
- }
- }
-}
diff --git a/runtime/impl/src/test/java/com/google/apphosting/runtime/anyrpc/GrpcTest.java b/runtime/impl/src/test/java/com/google/apphosting/runtime/anyrpc/GrpcTest.java
deleted file mode 100644
index 5ce856ce2..000000000
--- a/runtime/impl/src/test/java/com/google/apphosting/runtime/anyrpc/GrpcTest.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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.apphosting.runtime.anyrpc;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.apphosting.runtime.grpc.GrpcClientContext;
-import com.google.apphosting.runtime.grpc.GrpcPlugin;
-import com.google.apphosting.testing.PortPicker;
-import io.grpc.ManagedChannel;
-import io.grpc.netty.NegotiationType;
-import io.grpc.netty.NettyChannelBuilder;
-import java.io.IOException;
-import java.time.Clock;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.MockitoAnnotations;
-
-/** Loopback GRPC-to-GRPC test. */
-@RunWith(JUnit4.class)
-public class GrpcTest extends AbstractRpcCompatibilityTest {
- private static Logger grpcManagedChannelLogger;
- private static Logger grpcDnsNameResolverLogger;
-
- // Disable gRPC Logging for lost channels, and dns name resover.
- // Save a ref to avoid garbage collection.
- // Ignore automated suggests to make those fields be local variables in this method!
- @BeforeClass
- public static void beforeClass() {
- grpcManagedChannelLogger = Logger.getLogger("io.grpc.internal.ManagedChannelOrphanWrapper");
- grpcManagedChannelLogger.setLevel(Level.OFF);
- grpcDnsNameResolverLogger = Logger.getLogger("io.grpc.internal.DnsNameResolver");
- grpcDnsNameResolverLogger.setLevel(Level.OFF);
- }
-
- private GrpcPlugin rpcPlugin;
-
- @Override
- AnyRpcPlugin getClientPlugin() {
- return rpcPlugin;
- }
-
- @Override
- AnyRpcPlugin getServerPlugin() {
- return rpcPlugin;
- }
-
- @Override
- int getPacketSize() {
- return 65536;
- }
-
- @Before
- public void setUp() throws IOException, InterruptedException {
- MockitoAnnotations.initMocks(this);
- rpcPlugin = new GrpcPlugin();
- int serverPort = PortPicker.create().pickUnusedPort();
- rpcPlugin.initialize(serverPort);
- }
-
- @Override
- AnyRpcClientContextFactory newRpcClientContextFactory() {
- return () -> new GrpcClientContext(getClockHandler().clock);
- }
-
- @Override
- ClientInterfaces.EvaluationRuntimeClient newEvaluationRuntimeClient() {
- int serverPort = rpcPlugin.getServerPort();
- ManagedChannel channel =
- NettyChannelBuilder.forAddress("localhost", serverPort)
- .negotiationType(NegotiationType.PLAINTEXT)
- .build();
- return new GrpcClients.GrpcEvaluationRuntimeClient(channel);
- }
-
- @Override
- ClientInterfaces.CloneControllerClient newCloneControllerClient() {
- int serverPort = rpcPlugin.getServerPort();
- ManagedChannel channel =
- NettyChannelBuilder.forAddress("localhost", serverPort)
- .negotiationType(NegotiationType.PLAINTEXT)
- .build();
- return new GrpcClients.GrpcCloneControllerClient(channel);
- }
-
- @Override
- ClockHandler getClockHandler() {
- return new GrpcClockHandler(new FakeClock());
- }
-
- private static class GrpcClockHandler extends ClockHandler {
- GrpcClockHandler(Clock clock) {
- super(clock);
- }
-
- @Override
- void advanceClock() {
- ((FakeClock) clock).incrementTime(1000);
- }
-
- @Override
- void assertStartTime(long expectedStartTime, long reportedStartTime) {
- assertThat(reportedStartTime).isEqualTo(expectedStartTime);
- }
- }
-
- private static class FakeClock extends Clock {
- private final AtomicLong nowMillis = new AtomicLong(1000000000L);
-
- @Override
- public Instant instant() {
- return Instant.ofEpochMilli(nowMillis.get());
- }
-
- void incrementTime(long millis) {
- nowMillis.addAndGet(millis);
- }
-
- @Override
- public ZoneId getZone() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Clock withZone(ZoneId zone) {
- throw new UnsupportedOperationException();
- }
- }
-}
diff --git a/runtime/impl/src/test/java/com/google/apphosting/runtime/grpc/GrpcPluginTest.java b/runtime/impl/src/test/java/com/google/apphosting/runtime/grpc/GrpcPluginTest.java
deleted file mode 100644
index dbfc20023..000000000
--- a/runtime/impl/src/test/java/com/google/apphosting/runtime/grpc/GrpcPluginTest.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.apphosting.runtime.grpc;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
-import com.google.apphosting.runtime.anyrpc.CloneControllerServerInterface;
-import com.google.apphosting.runtime.anyrpc.EvaluationRuntimeServerInterface;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-/**
- * Test for {@link GrpcPlugin}.
- *
- */
-@RunWith(JUnit4.class)
-public class GrpcPluginTest {
- @Test
- public void serverNeedsServerPort() {
- GrpcPlugin plugin = new GrpcPlugin();
- plugin.initialize(0);
- EvaluationRuntimeServerInterface evaluationRuntimeServer =
- Mockito.mock(EvaluationRuntimeServerInterface.class);
- CloneControllerServerInterface cloneControllerServer =
- Mockito.mock(CloneControllerServerInterface.class);
- IllegalStateException expected =
- assertThrows(
- IllegalStateException.class,
- () -> plugin.startServer(evaluationRuntimeServer, cloneControllerServer));
- assertThat(expected).hasMessageThat().isEqualTo("No server port has been specified");
- }
-}
diff --git a/runtime/lite/pom.xml b/runtime/lite/pom.xml
index cb7af9477..20140b328 100644
--- a/runtime/lite/pom.xml
+++ b/runtime/lite/pom.xml
@@ -23,7 +23,7 @@
com.google.appengineruntime-parent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjar
@@ -61,7 +61,7 @@
com.google.testparameterinjectortest-parameter-injector
- 1.18
+ 1.19test
diff --git a/runtime/lite/src/main/java/com/google/appengine/runtime/lite/RequestManager.java b/runtime/lite/src/main/java/com/google/appengine/runtime/lite/RequestManager.java
index 1ed7294dd..c35051127 100644
--- a/runtime/lite/src/main/java/com/google/appengine/runtime/lite/RequestManager.java
+++ b/runtime/lite/src/main/java/com/google/appengine/runtime/lite/RequestManager.java
@@ -70,7 +70,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.Semaphore;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* {@code RequestManager} is responsible for setting up and tearing down any state associated with
diff --git a/runtime/local_jetty12/pom.xml b/runtime/local_jetty12/pom.xml
index 7362b65d9..904154111 100644
--- a/runtime/local_jetty12/pom.xml
+++ b/runtime/local_jetty12/pom.xml
@@ -23,17 +23,13 @@
com.google.appengineruntime-parent
- 2.0.34-SNAPSHOT
+ 3.0.0-SNAPSHOTjarAppEngine :: appengine-local-runtime Jetty12
- App Engine Local devappserver.
-
- 11
- 1.11
- 1.11
-
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ App Engine Local devappserver Jetty 12.
@@ -158,29 +154,6 @@
-
- com.google.appengine
- appengine-local-runtime-jetty12-ee10
- ${project.version}
-
-
- org.eclipse.jetty.ee10
- jetty-ee10-annotations
-
-
- org.eclipse.jetty.ee10
- jetty-ee10-apache-jsp
-
-
- org.eclipse.jetty.ee10
- jetty-ee10-webapp
-
-
- org.eclipse.jetty.ee10
- jetty-ee10-servlet
-
-
-
@@ -245,12 +218,6 @@
com/google/borg/borgcron/**
-
- com.google.appengine:appengine-tools-sdk:*
-
- com/google/appengine/tools/development/proto/**
-
- com.google.appengine:proto1:*
@@ -313,7 +280,7 @@
com.google.appengine:sessiondatacom.google.appengine:shared-sdkcom.google.appengine:shared-sdk-jetty12
- com.google.appengine:appengine-local-runtime-jetty12-ee10
+ com.google.appengine:appengine-local-runtime-jetty12com.google.flogger:google-extensionscom.google.flogger:flogger-system-backendcom.google.flogger:flogger
diff --git a/runtime/local_jetty12/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java b/runtime/local_jetty12/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java
index 4ff98f06d..c96653905 100644
--- a/runtime/local_jetty12/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java
+++ b/runtime/local_jetty12/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java
@@ -621,87 +621,86 @@ public void doScope(
HttpServletResponse response)
throws IOException, ServletException {
- org.eclipse.jetty.server.Request.addCompletionListener(
- baseRequest.getCoreRequest(),
- t -> {
- try {
- // a special hook with direct access to the container instance
- // we invoke this only after the normal request processing,
- // in order to generate a valid response
- if (request.getRequestURI().startsWith(AH_URL_RELOAD)) {
- try {
- reloadWebApp();
- log.info("Reloaded the webapp context: " + request.getParameter("info"));
- } catch (Exception ex) {
- log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
- }
- }
- } finally {
-
- LocalEnvironment env =
- (LocalEnvironment) request.getAttribute(LocalEnvironment.class.getName());
- if (env != null) {
- environments.remove(env);
-
- // Acquire all of the semaphores back, which will block if any are outstanding.
- Semaphore semaphore =
- (Semaphore) env.getAttributes().get(LocalEnvironment.API_CALL_SEMAPHORE);
- try {
- System.err.println("=========== acquire semaphore ===========");
- semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- log.log(
- Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
- }
-
- try {
- ApiProxy.setEnvironmentForCurrentThread(env);
-
- // Invoke all of the registered RequestEndListeners.
- env.callRequestEndListeners();
-
- if (apiProxyDelegate instanceof ApiProxyLocal) {
- // If apiProxyDelegate is not instanceof ApiProxyLocal, we are presumably
- // running in
- // the devappserver2 environment, where the master web server in Python will
- // take care
- // of logging requests.
- ApiProxyLocal apiProxyLocal = (ApiProxyLocal) apiProxyDelegate;
- String appId = env.getAppId();
- String versionId = env.getVersionId();
- String requestId = DevLogHandler.getRequestId();
-
- LocalLogService logService =
- (LocalLogService) apiProxyLocal.getService(LocalLogService.PACKAGE);
-
- @SuppressWarnings("NowMillis")
- long nowMillis = System.currentTimeMillis();
- logService.addRequestInfo(
- appId,
- versionId,
- requestId,
- request.getRemoteAddr(),
- request.getRemoteUser(),
- baseRequest.getTimeStamp() * 1000,
- nowMillis * 1000,
- request.getMethod(),
- request.getRequestURI(),
- request.getProtocol(),
- request.getHeader("User-Agent"),
- true,
- response.getStatus(),
- request.getHeader("Referrer"));
- logService.clearResponseSize();
+ if (baseRequest.getDispatcherType() == DispatcherType.REQUEST) {
+ org.eclipse.jetty.server.Request.addCompletionListener(
+ baseRequest.getCoreRequest(),
+ t -> {
+ try {
+ // a special hook with direct access to the container instance
+ // we invoke this only after the normal request processing,
+ // in order to generate a valid response
+ if (request.getRequestURI().startsWith(AH_URL_RELOAD)) {
+ try {
+ reloadWebApp();
+ log.info("Reloaded the webapp context: " + request.getParameter("info"));
+ } catch (Exception ex) {
+ log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
+ }
+ }
+ } finally {
+
+ LocalEnvironment env =
+ (LocalEnvironment) request.getAttribute(LocalEnvironment.class.getName());
+ if (env != null) {
+ environments.remove(env);
+
+ // Acquire all of the semaphores back, which will block if any are outstanding.
+ Semaphore semaphore =
+ (Semaphore) env.getAttributes().get(LocalEnvironment.API_CALL_SEMAPHORE);
+ try {
+ semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ log.log(
+ Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
+ }
+
+ try {
+ ApiProxy.setEnvironmentForCurrentThread(env);
+
+ // Invoke all of the registered RequestEndListeners.
+ env.callRequestEndListeners();
+
+ if (apiProxyDelegate instanceof ApiProxyLocal) {
+ // If apiProxyDelegate is not instanceof ApiProxyLocal, we are presumably
+ // running in
+ // the devappserver2 environment, where the master web server in Python will
+ // take care
+ // of logging requests.
+ ApiProxyLocal apiProxyLocal = (ApiProxyLocal) apiProxyDelegate;
+ String appId = env.getAppId();
+ String versionId = env.getVersionId();
+ String requestId = DevLogHandler.getRequestId();
+
+ LocalLogService logService =
+ (LocalLogService) apiProxyLocal.getService(LocalLogService.PACKAGE);
+
+ @SuppressWarnings("NowMillis")
+ long nowMillis = System.currentTimeMillis();
+ logService.addRequestInfo(
+ appId,
+ versionId,
+ requestId,
+ request.getRemoteAddr(),
+ request.getRemoteUser(),
+ baseRequest.getTimeStamp() * 1000,
+ nowMillis * 1000,
+ request.getMethod(),
+ request.getRequestURI(),
+ request.getProtocol(),
+ request.getHeader("User-Agent"),
+ true,
+ response.getStatus(),
+ request.getHeader("Referrer"));
+ logService.clearResponseSize();
+ }
+ } finally {
+ ApiProxy.clearEnvironmentForCurrentThread();
+ }
+ }
}
- } finally {
- ApiProxy.clearEnvironmentForCurrentThread();
- }
- }
- }
- });
+ });
- if (baseRequest.getDispatcherType() == DispatcherType.REQUEST) {
Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_API_CALLS);
LocalEnvironment env =
diff --git a/runtime/local_jetty121/pom.xml b/runtime/local_jetty121/pom.xml
new file mode 100644
index 000000000..81a927d7c
--- /dev/null
+++ b/runtime/local_jetty121/pom.xml
@@ -0,0 +1,295 @@
+
+
+
+
+ 4.0.0
+
+ appengine-local-runtime-jetty121
+
+
+ com.google.appengine
+ runtime-parent
+ 3.0.0-SNAPSHOT
+
+
+ jar
+ AppEngine :: appengine-local-runtime Jetty121
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ App Engine Local devappserver Jetty 12.1.
+
+
+
+ com.google.appengine
+ appengine-api-stubs
+
+
+ com.google.appengine
+ appengine-remote-api
+
+
+ com.google.appengine
+ appengine-tools-sdk
+
+
+ com.google.appengine
+ sessiondata
+
+
+
+ com.google.auto.value
+ auto-value
+
+
+ com.google.appengine
+ shared-sdk
+
+
+ com.google.appengine
+ appengine-utils
+
+
+ com.google.flogger
+ flogger-system-backend
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+ com.google.appengine
+ proto1
+
+
+ org.eclipse.jetty.ee8
+ jetty-ee8-webapp
+ ${jetty121.version}
+
+
+ org.eclipse.jetty.ee8
+ jetty-ee8-security
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty121.version}
+
+
+ org.eclipse.jetty.ee8
+ jetty-ee8-annotations
+ ${jetty121.version}
+
+
+ org.mortbay.jasper
+ apache-jsp
+ 9.0.52
+
+
+
+ org.eclipse.jetty.ee8
+ jetty-ee8-apache-jsp
+ ${jetty121.version}
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+
+
+ org.eclipse.jetty
+ jetty-http
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-io
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-jndi
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-security
+ ${jetty121.version}
+
+
+ org.eclipse.jetty.toolchain
+ jetty-servlet-api
+ 4.0.6
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-xml
+ ${jetty121.version}
+
+
+ com.google.appengine
+ shared-sdk-jetty121
+ ${project.version}
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+
+
+
+
+
+
+
+
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ com.google.common
+ com.google.appengine.repackaged.com.google.common
+
+
+ com.google.io
+ com.google.appengine.repackaged.com.google.io
+
+
+ com.google.protobuf
+ com.google.appengine.repackaged.com.google.protobuf
+
+
+ com.google.gaia.mint.proto2api
+ com.google.appengine.repackaged.com.google.gaia.mint.proto2api
+
+
+ com.esotericsoftware.yamlbeans
+ com.google.appengine.repackaged.com.esotericsoftware.yamlbeans
+
+
+ com.google.borg.borgcron
+ com.google.appengine.repackaged.com.google.cron
+
+
+
+
+ com.google.appengine:appengine-apis-dev:*
+
+ com/google/appengine/tools/development/**
+
+
+ com/google/appengine/tools/development/testing/**
+
+
+
+ com.google.appengine:appengine-apis:*
+
+ com/google/apphosting/utils/security/urlfetch/**
+
+
+
+ com.google.appengine:appengine-utils
+
+ com/google/apphosting/utils/config/**
+ com/google/apphosting/utils/io/**
+ com/google/apphosting/utils/security/urlfetch/**
+ com/google/borg/borgcron/**
+
+
+
+ com.google.appengine:proto1:*
+
+ com/google/common/flags/*
+ com/google/common/flags/ext/*
+ com/google/io/protocol/**
+ com/google/protobuf/**
+
+
+ com/google/io/protocol/proto2/*
+
+
+
+ com.google.appengine:shared-sdk-jetty121:*
+
+ com/google/apphosting/runtime/**
+ com/google/appengine/tools/development/**
+
+
+
+ com.google.guava:guava
+
+ com/google/common/base/**
+ com/google/common/cache/**
+ com/google/common/collect/**
+ com/google/common/escape/**
+ com/google/common/flags/**
+ com/google/common/flogger/**
+ com/google/common/graph/**
+ com/google/common/hash/**
+ com/google/common/html/**
+ com/google/common/io/**
+ com/google/common/math/**
+ com/google/common/net/HostAndPort.class
+ com/google/common/net/InetAddresses.class
+ com/google/common/primitives/**
+ com/google/common/time/**
+ com/google/common/util/concurrent/**
+ com/google/common/xml/**
+
+
+
+ com.contrastsecurity:yamlbeans
+
+
+ com/esotericsoftware/yamlbeans/**
+
+
+
+ com.google.appengine:sessiondata
+
+ com/**
+
+
+
+
+
+ com.google.appengine:appengine-tools-sdk
+ com.google.appengine:appengine-utils
+ com.google.appengine:sessiondata
+ com.google.appengine:shared-sdk
+ com.google.appengine:shared-sdk-jetty121
+ com.google.appengine:appengine-local-runtime-jetty121
+ com.google.flogger:google-extensions
+ com.google.flogger:flogger-system-backend
+ com.google.flogger:flogger
+
+
+
+
+
+
+
+
+
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/AppEngineAnnotationConfiguration.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/AppEngineAnnotationConfiguration.java
new file mode 100644
index 000000000..aae9160e1
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/AppEngineAnnotationConfiguration.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 com.google.appengine.tools.development.jetty;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.ServletContainerInitializer;
+import org.eclipse.jetty.ee8.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.ee8.apache.jsp.JettyJasperInitializer;
+import org.eclipse.jetty.ee8.webapp.WebAppContext;
+
+/**
+ * Customization of AnnotationConfiguration which correctly configures the JSP Jasper initializer.
+ * For more context, see b/37513903
+ */
+public class AppEngineAnnotationConfiguration extends AnnotationConfiguration {
+ @Override
+ public List getNonExcludedInitializers(WebAppContext context)
+ throws Exception {
+ ArrayList nonExcludedInitializers =
+ new ArrayList<>(super.getNonExcludedInitializers(context));
+ for (ServletContainerInitializer sci : nonExcludedInitializers) {
+ if (sci instanceof JettyJasperInitializer) {
+ // Jasper is already there, no need to add it.
+ return nonExcludedInitializers;
+ }
+ }
+ nonExcludedInitializers.add(new JettyJasperInitializer());
+
+ return nonExcludedInitializers;
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/AppEngineWebAppContext.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/AppEngineWebAppContext.java
new file mode 100644
index 000000000..b62e5e587
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/AppEngineWebAppContext.java
@@ -0,0 +1,169 @@
+/*
+ * 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.development.jetty;
+
+import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.api.ApiProxy.LogRecord;
+import com.google.apphosting.runtime.jetty.AppEngineAuthentication;
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import org.eclipse.jetty.ee8.nested.Request;
+import org.eclipse.jetty.ee8.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee8.security.RoleInfo;
+import org.eclipse.jetty.ee8.security.SecurityHandler;
+import org.eclipse.jetty.ee8.security.UserDataConstraint;
+import org.eclipse.jetty.ee8.webapp.WebAppContext;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * {@code AppEngineWebAppContext} is a customization of Jetty's {@link WebAppContext} that is aware
+ * of the {@link ApiProxy} and can provide custom logging and authentication.
+ */
+public class AppEngineWebAppContext extends WebAppContext {
+
+ // TODO: This should be some sort of Prometheus-wide
+ // constant. If it's much larger than this we may need to
+ // restructure the code a bit.
+ private static final int MAX_RESPONSE_SIZE = 32 * 1024 * 1024;
+
+ private final String serverInfo;
+
+ public AppEngineWebAppContext(File appDir, String serverInfo) {
+ // We set the contextPath to / for all applications.
+ super(appDir.getPath(), "/");
+ Resource webApp = null;
+ try {
+ webApp = ResourceFactory.root().newResource(appDir.getAbsolutePath());
+
+ if (appDir.isDirectory()) {
+ setWar(appDir.getPath());
+ setBaseResource(webApp);
+ } else {
+ // Real war file, not exploded , so we explode it in tmp area.
+ File extractedWebAppDir = createTempDir();
+ extractedWebAppDir.mkdir();
+ extractedWebAppDir.deleteOnExit();
+ Resource jarWebWpp = ResourceFactory.root().newJarFileResource(webApp.getURI());
+ jarWebWpp.copyTo(extractedWebAppDir.toPath());
+ setBaseResource(ResourceFactory.root().newResource(extractedWebAppDir.getAbsolutePath()));
+ setWar(extractedWebAppDir.getPath());
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("cannot create AppEngineWebAppContext:", e);
+ }
+
+ this.serverInfo = serverInfo;
+
+ // Configure the Jetty SecurityHandler to understand our method of
+ // authentication (via the UserService).
+ AppEngineAuthentication.configureSecurityHandler(
+ (ConstraintSecurityHandler) getSecurityHandler());
+
+ setMaxFormContentSize(MAX_RESPONSE_SIZE);
+ }
+
+ @Override
+ public APIContext getServletContext() {
+ // TODO: Override the default HttpServletContext implementation (for logging)?.
+ AppEngineServletContext appEngineServletContext = new AppEngineServletContext();
+ return super.getServletContext();
+ }
+
+ private static File createTempDir() {
+ File baseDir = new File(System.getProperty("java.io.tmpdir"));
+ String baseName = System.currentTimeMillis() + "-";
+
+ for (int counter = 0; counter < 10; counter++) {
+ File tempDir = new File(baseDir, baseName + counter);
+ if (tempDir.mkdir()) {
+ return tempDir;
+ }
+ }
+ throw new IllegalStateException("Failed to create directory ");
+ }
+
+ @Override
+ protected SecurityHandler newSecurityHandler() {
+ return new AppEngineConstraintSecurityHandler();
+ }
+
+ /**
+ * Override to make sure all RoleInfos do not have security constraints to avoid a Jetty failure
+ * when not running with https.
+ */
+ private static class AppEngineConstraintSecurityHandler extends ConstraintSecurityHandler {
+ @Override
+ protected RoleInfo prepareConstraintInfo(String pathInContext, Request request) {
+ RoleInfo ri = super.prepareConstraintInfo(pathInContext, request);
+ // Remove constraints so that we can emulate HTTPS locally.
+ ri.setUserDataConstraint(UserDataConstraint.None);
+ return ri;
+ }
+ }
+
+ // N.B.: Yuck. Jetty hardcodes all of this logic into an
+ // inner class of ContextHandler. We need to subclass WebAppContext
+ // (which extends ContextHandler) and then subclass the SContext
+ // inner class to modify its behavior.
+
+ /** Context extension that allows logs to be written to the App Engine log APIs. */
+ public class AppEngineServletContext extends Context {
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return AppEngineWebAppContext.this.getClassLoader();
+ }
+
+ @Override
+ public String getServerInfo() {
+ return serverInfo;
+ }
+
+ @Override
+ public void log(String message) {
+ log(message, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param throwable an exception associated with this log message, or {@code null}.
+ */
+ @Override
+ public void log(String message, Throwable throwable) {
+ StringWriter writer = new StringWriter();
+ writer.append("javax.servlet.ServletContext log: ");
+ writer.append(message);
+
+ if (throwable != null) {
+ writer.append("\n");
+ throwable.printStackTrace(new PrintWriter(writer));
+ }
+
+ LogRecord.Level logLevel = throwable == null ? LogRecord.Level.info : LogRecord.Level.error;
+ ApiProxy.log(
+ new ApiProxy.LogRecord(logLevel, System.currentTimeMillis() * 1000L, writer.toString()));
+ }
+
+ @Override
+ public void log(Exception exception, String msg) {
+ log(msg, exception);
+ }
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/DevAppEngineWebAppContext.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/DevAppEngineWebAppContext.java
new file mode 100644
index 000000000..4fa7579ea
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/DevAppEngineWebAppContext.java
@@ -0,0 +1,193 @@
+/*
+ * 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.development.jetty;
+
+import com.google.appengine.tools.development.DevAppServer;
+import com.google.appengine.tools.info.AppengineSdk;
+import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.utils.io.IoUtil;
+import java.io.File;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.logging.Logger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee8.nested.Request;
+import org.eclipse.jetty.ee8.security.ConstraintMapping;
+import org.eclipse.jetty.ee8.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.util.resource.Resource;
+
+/** An AppEngineWebAppContext for the DevAppServer. */
+public class DevAppEngineWebAppContext extends AppEngineWebAppContext {
+
+ private static final Logger logger = Logger.getLogger(DevAppEngineWebAppContext.class.getName());
+
+ // Copied from org.apache.jasper.Constants.SERVLET_CLASSPATH
+ // to remove compile-time dependency on Jasper
+ private static final String JASPER_SERVLET_CLASSPATH = "org.apache.catalina.jsp_classpath";
+
+ // Header that allows arbitrary requests to bypass jetty's security
+ // mechanisms. Useful for things like the dev task queue, which needs
+ // to hit secure urls without an authenticated user.
+ private static final String X_GOOGLE_DEV_APPSERVER_SKIPADMINCHECK =
+ "X-Google-DevAppserver-SkipAdminCheck";
+
+ // Keep in sync with com.google.apphosting.utils.jetty.AppEngineAuthentication.
+ private static final String SKIP_ADMIN_CHECK_ATTR =
+ "com.google.apphosting.internal.SkipAdminCheck";
+
+ private final Object transportGuaranteeLock = new Object();
+ private boolean transportGuaranteesDisabled = false;
+
+ public DevAppEngineWebAppContext(
+ File appDir,
+ File externalResourceDir,
+ String serverInfo,
+ ApiProxy.Delegate> apiProxyDelegate,
+ DevAppServer devAppServer) {
+ super(appDir, serverInfo);
+
+ // Set up the classpath required to compile JSPs. This is specific to Jasper.
+ setAttribute(JASPER_SERVLET_CLASSPATH, buildClasspath());
+
+ // Make ApiProxyLocal available via the servlet context. This allows
+ // servlets that are part of the dev appserver (like those that render the
+ // dev console for example) to get access to this resource even in the
+ // presence of libraries that install their own custom Delegates (like
+ // Remote api and Appstats for example).
+ getServletContext()
+ .setAttribute("com.google.appengine.devappserver.ApiProxyLocal", apiProxyDelegate);
+
+ // Make the dev appserver available via the servlet context as well.
+ getServletContext().setAttribute("com.google.appengine.devappserver.Server", devAppServer);
+ }
+
+ /**
+ * By default, the context is created with alias checkers for symlinks: {@link
+ * org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker}.
+ *
+ *
Note: this is a dangerous configuration and should not be used in production.
+ */
+ @Override
+ public boolean checkAlias(String path, Resource resource) {
+ return true;
+ }
+
+ @Override
+ protected ClassLoader configureClassLoader(ClassLoader loader) {
+ // Avoid wrapping the provided classloader with WebAppClassLoader.
+ return loader;
+ }
+
+ @Override
+ public void doScope(
+ String target,
+ Request baseRequest,
+ HttpServletRequest httpServletRequest,
+ HttpServletResponse httpServletResponse)
+ throws IOException, ServletException {
+
+ if (hasSkipAdminCheck(baseRequest)) {
+ baseRequest.setAttribute(SKIP_ADMIN_CHECK_ATTR, Boolean.TRUE);
+ }
+
+ disableTransportGuarantee();
+
+ // TODO An extremely heinous way of helping the DevAppServer's
+ // SecurityManager determine if a DevAppServer request thread is executing.
+ // Find something better.
+ // See DevAppServerFactory.CustomSecurityManager.
+ System.setProperty("devappserver-thread-" + Thread.currentThread().getName(), "true");
+ try {
+ super.doScope(target, baseRequest, httpServletRequest, httpServletResponse);
+ } finally {
+ System.clearProperty("devappserver-thread-" + Thread.currentThread().getName());
+ }
+ }
+
+ /**
+ * Returns true if the X-Google-Internal-SkipAdminCheck header is present. There is nothing
+ * preventing usercode from setting this header and circumventing dev appserver security, but the
+ * dev appserver was not designed to be secure.
+ */
+ private boolean hasSkipAdminCheck(HttpServletRequest request) {
+ // wow, old school java
+ for (Enumeration> headerNames = request.getHeaderNames(); headerNames.hasMoreElements(); ) {
+ String name = (String) headerNames.nextElement();
+ // We don't care about the header value, its presence is sufficient.
+ if (name.equalsIgnoreCase(X_GOOGLE_DEV_APPSERVER_SKIPADMINCHECK)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Builds a classpath up for the webapp for JSP compilation. */
+ private String buildClasspath() {
+ StringBuilder classpath = new StringBuilder();
+
+ // Shared servlet container classes
+ for (File f : AppengineSdk.getSdk().getSharedLibFiles()) {
+ classpath.append(f.getAbsolutePath());
+ classpath.append(File.pathSeparatorChar);
+ }
+
+ String webAppPath = getWar();
+
+ // webapp classes
+ classpath.append(webAppPath + File.separator + "classes" + File.pathSeparatorChar);
+
+ List files = IoUtil.getFilesAndDirectories(new File(webAppPath, "lib"));
+ for (File f : files) {
+ if (f.isFile() && f.getName().endsWith(".jar")) {
+ classpath.append(f.getAbsolutePath());
+ classpath.append(File.pathSeparatorChar);
+ }
+ }
+
+ return classpath.toString();
+ }
+
+ /**
+ * The first time this method is called it will walk through the constraint mappings on the
+ * current SecurityHandler and disable any transport guarantees that have been set. This is
+ * required to disable SSL requirements in the DevAppServer because it does not support SSL.
+ */
+ private void disableTransportGuarantee() {
+ synchronized (transportGuaranteeLock) {
+ if (!transportGuaranteesDisabled && getSecurityHandler() != null) {
+ List mappings =
+ ((ConstraintSecurityHandler) getSecurityHandler()).getConstraintMappings();
+ if (mappings != null) {
+ for (ConstraintMapping mapping : mappings) {
+ if (mapping.getConstraint().getDataConstraint() > 0) {
+ logger.info(
+ "Ignoring for "
+ + mapping.getPathSpec()
+ + " as the SDK does not support HTTPS. It will still be used"
+ + " when you upload your application.");
+ mapping.getConstraint().setDataConstraint(0);
+ }
+ }
+ }
+ }
+ transportGuaranteesDisabled = true;
+ }
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/FixupJspServlet.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/FixupJspServlet.java
new file mode 100644
index 000000000..b180e9133
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/FixupJspServlet.java
@@ -0,0 +1,130 @@
+/*
+ * 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.development.jetty;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.jasper.servlet.JspServlet;
+import org.apache.tomcat.InstanceManager;
+
+/** {@code FixupJspServlet} adds some logic to work around bugs in the Jasper {@link JspServlet}. */
+public class FixupJspServlet extends JspServlet {
+
+ /**
+ * The request attribute that contains the name of the JSP file, when the request path doesn't
+ * refer directly to the JSP file (for example, it's instead a servlet mapping).
+ */
+ private static final String JASPER_JSP_FILE = "org.apache.catalina.jsp_file";
+
+ private static final String WEB31XML =
+ ""
+ + ""
+ + "";
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ config
+ .getServletContext()
+ .setAttribute(InstanceManager.class.getName(), new InstanceManagerImpl());
+ config.getServletContext().setAttribute("org.apache.tomcat.util.scan.MergedWebXml", WEB31XML);
+ super.init(config);
+ }
+
+ @Override
+ public void service(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ fixupJspFileAttribute(request);
+ super.service(request, response);
+ }
+
+ private static class InstanceManagerImpl implements InstanceManager {
+ @Override
+ public Object newInstance(String className)
+ throws IllegalAccessException,
+ InvocationTargetException,
+ InstantiationException,
+ ClassNotFoundException {
+ return newInstance(className, this.getClass().getClassLoader());
+ }
+
+ @Override
+ public Object newInstance(String fqcn, ClassLoader classLoader)
+ throws IllegalAccessException,
+ InvocationTargetException,
+ InstantiationException,
+ ClassNotFoundException {
+ Class> cl = classLoader.loadClass(fqcn);
+ return newInstance(cl);
+ }
+
+ @Override
+ @SuppressWarnings("ClassNewInstance")
+ // We would prefer clazz.getConstructor().newInstance() here, but that throws
+ // NoSuchMethodException. It would also lead to a change in behaviour, since an exception
+ // thrown by the constructor would be wrapped in InvocationTargetException rather than being
+ // propagated from newInstance(). Although that's funky, and the reason for preferring
+ // getConstructor().newInstance(), we don't know if something is relying on the current
+ // behaviour.
+ public Object newInstance(Class> clazz)
+ throws IllegalAccessException, InvocationTargetException, InstantiationException {
+ return clazz.newInstance();
+ }
+
+ @Override
+ public void newInstance(Object o) {}
+
+ @Override
+ public void destroyInstance(Object o)
+ throws IllegalAccessException, InvocationTargetException {}
+ }
+
+ // NB This method is here, because there appears to be
+ // a bug in either Jetty or Jasper where entries in web.xml
+ // don't get handled correctly. This interaction between Jetty and Jasper
+ // appears to have always been broken, irrespective of App Engine
+ // integration.
+ //
+ // Jetty hands the name of the JSP file to Jasper (via a request attribute)
+ // without a leading slash. This seems to cause all sorts of problems.
+ // - Jasper turns around and asks Jetty to lookup that same file
+ // (using ServletContext.getResourceAsStream). Jetty rejects, out-of-hand,
+ // any resource requests that don't start with a leading slash.
+ // - Jasper seems to plain blow up on jsp paths that don't have a leading
+ // slash.
+ //
+ // If we enforce a leading slash, Jetty and Jasper seem to co-operate
+ // correctly.
+ private void fixupJspFileAttribute(HttpServletRequest request) {
+ String jspFile = (String) request.getAttribute(JASPER_JSP_FILE);
+
+ if (jspFile != null) {
+ if (jspFile.length() == 0) {
+ jspFile = "/";
+ } else if (jspFile.charAt(0) != '/') {
+ jspFile = "/" + jspFile;
+ }
+ request.setAttribute(JASPER_JSP_FILE, jspFile);
+ }
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java
new file mode 100644
index 000000000..73dad03ad
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java
@@ -0,0 +1,740 @@
+/*
+ * 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.development.jetty;
+
+import static com.google.appengine.tools.development.LocalEnvironment.DEFAULT_VERSION_HOSTNAME;
+
+import com.google.appengine.api.log.dev.DevLogHandler;
+import com.google.appengine.api.log.dev.LocalLogService;
+import com.google.appengine.tools.development.AbstractContainerService;
+import com.google.appengine.tools.development.ApiProxyLocal;
+import com.google.appengine.tools.development.AppContext;
+import com.google.appengine.tools.development.ContainerService;
+import com.google.appengine.tools.development.ContainerServiceEE8;
+import com.google.appengine.tools.development.DevAppServer;
+import com.google.appengine.tools.development.DevAppServerModulesFilter;
+import com.google.appengine.tools.development.IsolatedAppClassLoader;
+import com.google.appengine.tools.development.LocalEnvironment;
+import com.google.appengine.tools.development.LocalHttpRequestEnvironment;
+import com.google.appengine.tools.info.AppengineSdk;
+import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.runtime.jetty.SessionManagerHandler;
+import com.google.apphosting.utils.config.AppEngineConfigException;
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.apphosting.utils.config.WebModule;
+import com.google.common.base.Predicates;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.security.Permissions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee8.nested.ContextHandler;
+import org.eclipse.jetty.ee8.nested.Request;
+import org.eclipse.jetty.ee8.nested.ScopedHandler;
+import org.eclipse.jetty.ee8.servlet.ServletHolder;
+import org.eclipse.jetty.ee8.webapp.Configuration;
+import org.eclipse.jetty.ee8.webapp.JettyWebXmlConfiguration;
+import org.eclipse.jetty.ee8.webapp.WebAppContext;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.NetworkTrafficServerConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.Scanner;
+import org.eclipse.jetty.util.VirtualThreads;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+
+/** Implements a Jetty backed {@link ContainerService}. */
+public class JettyContainerService extends AbstractContainerService implements ContainerServiceEE8 {
+
+ private static final Logger log = Logger.getLogger(JettyContainerService.class.getName());
+
+ private static final String JETTY_TAG_LIB_JAR_PREFIX = "org.apache.taglibs.taglibs-";
+ private static final Pattern JSP_REGEX = Pattern.compile(".*\\.jspx?");
+
+ public static final String WEB_DEFAULTS_XML =
+ "com/google/appengine/tools/development/jetty/webdefault.xml";
+
+ // This should match the value of the --clone_max_outstanding_api_rpcs flag.
+ private static final int MAX_SIMULTANEOUS_API_CALLS = 100;
+
+ // The soft deadline for requests. It is defined here, as the normal way to
+ // get this deadline is through JavaRuntimeFactory, which is part of the
+ // runtime and not really part of the devappserver.
+ private static final Long SOFT_DEADLINE_DELAY_MS = 60000L;
+
+ /**
+ * Specify which {@link Configuration} objects should be invoked when configuring a web
+ * application.
+ *
+ *
This is a subset of: org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
+ *
+ *
Specifically, we've removed {@link JettyWebXmlConfiguration} which allows users to use
+ * {@code jetty-web.xml} files.
+ */
+ private static final String[] CONFIG_CLASSES =
+ new String[] {
+ org.eclipse.jetty.ee8.webapp.WebInfConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.ee8.webapp.WebXmlConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.ee8.webapp.MetaInfConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.ee8.webapp.FragmentConfiguration.class.getCanonicalName(),
+ // Special annotationConfiguration to deal with Jasper ServletContainerInitializer.
+ AppEngineAnnotationConfiguration.class.getCanonicalName()
+ };
+
+ private static final String WEB_XML_ATTR = "com.google.appengine.tools.development.webXml";
+ private static final String APPENGINE_WEB_XML_ATTR =
+ "com.google.appengine.tools.development.appEngineWebXml";
+
+ private static final int SCAN_INTERVAL_SECONDS = 5;
+
+ /** Jetty webapp context. */
+ private WebAppContext context;
+
+ /** Our webapp context. */
+ private AppContext appContext;
+
+ /** The Jetty server. */
+ private Server server;
+
+ /** Hot deployment support. */
+ private Scanner scanner;
+
+ /** Collection of current LocalEnvironments */
+ private final Set environments = ConcurrentHashMap.newKeySet();
+
+ private class JettyAppContext implements AppContext {
+ @Override
+ public ClassLoader getClassLoader() {
+ return context.getClassLoader();
+ }
+
+ @Override
+ public Permissions getUserPermissions() {
+ return JettyContainerService.this.getUserPermissions();
+ }
+
+ @Override
+ public Permissions getApplicationPermissions() {
+ // Should not be called in Java8/Jetty9.
+ throw new RuntimeException("No permissions needed for this runtime.");
+ }
+
+ @Override
+ public Object getContainerContext() {
+ return context;
+ }
+ }
+
+ public JettyContainerService() {}
+
+ @Override
+ protected File initContext() throws IOException {
+ // Register our own slight modification of Jetty's WebAppContext,
+ // which maintains ApiProxy's environment ThreadLocal.
+ this.context =
+ new DevAppEngineWebAppContext(
+ appDir, externalResourceDir, devAppServerVersion, apiProxyDelegate, devAppServer);
+
+ context.addEventListener(
+ new ContextHandler.ContextScopeListener() {
+ @Override
+ public void enterScope(
+ ContextHandler.APIContext context, Request request, Object reason) {
+ JettyContainerService.this.enterScope(request);
+ }
+
+ @Override
+ public void exitScope(ContextHandler.APIContext context, Request request) {
+ JettyContainerService.this.exitScope(null);
+ }
+ });
+ this.appContext = new JettyAppContext();
+
+ // Set the location of deployment descriptor. This value might be null,
+ // which is fine, it just means Jetty will look for it in the default
+ // location (WEB-INF/web.xml).
+ context.setDescriptor(webXmlLocation == null ? null : webXmlLocation.getAbsolutePath());
+
+ // Override the web.xml that Jetty automatically prepends to other
+ // web.xml files. This is where the DefaultServlet is registered,
+ // which serves static files. We override it to disable some
+ // other magic (e.g. JSP compilation), and to turn off some static
+ // file functionality that Prometheus won't support
+ // (e.g. directory listings) and turn on others (e.g. symlinks).
+ String webDefaultXml =
+ devAppServer
+ .getServiceProperties()
+ .getOrDefault("appengine.webdefault.xml", WEB_DEFAULTS_XML);
+ context.setDefaultsDescriptor(webDefaultXml);
+
+ // Disable support for jetty-web.xml.
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(WebAppContext.class.getClassLoader());
+ context.setConfigurationClasses(CONFIG_CLASSES);
+ } finally {
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+ // Create the webapp ClassLoader.
+ // We need to load appengine-web.xml to initialize the class loader.
+ File appRoot = determineAppRoot();
+ installLocalInitializationEnvironment();
+
+ // Create the webapp ClassLoader.
+ // ADD TLDs that must be under WEB-INF for Jetty9.
+ // We make it non fatal, and emit a warning when it fails, as the user can add this dependency
+ // in the application itself.
+ if (applicationContainsJSP(appDir, JSP_REGEX)) {
+ for (File file : AppengineSdk.getSdk().getUserJspLibFiles()) {
+ if (file.getName().startsWith(JETTY_TAG_LIB_JAR_PREFIX)) {
+ // Jetty provided tag lib jars are currently
+ // org.apache.taglibs.taglibs-standard-spec-1.2.5.jar and
+ // org.apache.taglibs.taglibs-standard-impl-1.2.5.jar.
+ // For jars provided by a Maven or Gradle builder, the prefix org.apache.taglibs.taglibs-
+ // is not present, so the jar names are:
+ // standard-spec-1.2.5.jar and
+ // standard-impl-1.2.5.jar.
+ // We check if these jars are provided by the web app, or we copy them from Jetty distro.
+ File jettyProvidedDestination = new File(appDir + "/WEB-INF/lib/" + file.getName());
+ if (!jettyProvidedDestination.exists()) {
+ File mavenProvidedDestination =
+ new File(
+ appDir
+ + "/WEB-INF/lib/"
+ + file.getName().substring(JETTY_TAG_LIB_JAR_PREFIX.length()));
+ if (!mavenProvidedDestination.exists()) {
+ log.log(
+ Level.WARNING,
+ "Adding jar "
+ + file.getName()
+ + " to WEB-INF/lib."
+ + " You might want to add a dependency in your project build system to avoid"
+ + " this warning.");
+ try {
+ Files.copy(file, jettyProvidedDestination);
+ } catch (IOException e) {
+ log.log(
+ Level.WARNING,
+ "Cannot copy org.apache.taglibs.taglibs jar file to WEB-INF/lib.",
+ e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ URL[] classPath = getClassPathForApp(appRoot);
+
+ IsolatedAppClassLoader isolatedClassLoader =
+ new IsolatedAppClassLoader(
+ appRoot, externalResourceDir, classPath, JettyContainerService.class.getClassLoader());
+ context.setClassLoader(isolatedClassLoader);
+ if (Boolean.parseBoolean(System.getProperty("appengine.allowRemoteShutdown"))) {
+ context.addServlet(new ServletHolder(new ServerShutdownServlet()), "/_ah/admin/quit");
+ }
+
+ return appRoot;
+ }
+
+ private ApiProxy.Environment enterScope(HttpServletRequest request) {
+ ApiProxy.Environment oldEnv = ApiProxy.getCurrentEnvironment();
+
+ // We should have a request that use its associated environment, if there is no request
+ // we cannot select a local environment as picking the wrong one could result in
+ // waiting on the LocalEnvironment API call semaphore forever.
+ LocalEnvironment env =
+ request == null
+ ? null
+ : (LocalEnvironment) request.getAttribute(LocalEnvironment.class.getName());
+ if (env != null) {
+ ApiProxy.setEnvironmentForCurrentThread(env);
+ DevAppServerModulesFilter.injectBackendServiceCurrentApiInfo(
+ backendName, backendInstance, portMappingProvider.getPortMapping());
+ }
+
+ return oldEnv;
+ }
+
+ private void exitScope(ApiProxy.Environment environment) {
+ ApiProxy.setEnvironmentForCurrentThread(environment);
+ }
+
+ /** Check if the application contains a JSP file. */
+ private static boolean applicationContainsJSP(File dir, Pattern jspPattern) {
+ for (File file :
+ FluentIterable.from(Files.fileTraverser().depthFirstPreOrder(dir))
+ .filter(Predicates.not(Files.isDirectory()))) {
+ if (jspPattern.matcher(file.getName()).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static class ServerShutdownServlet extends HttpServlet {
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.getWriter().println("Shutting down local server.");
+ resp.flushBuffer();
+ DevAppServer server =
+ (DevAppServer)
+ getServletContext().getAttribute("com.google.appengine.devappserver.Server");
+ // don't shut down until outstanding requests (like this one) have finished
+ server.gracefulShutdown();
+ }
+ }
+
+ @Override
+ protected void connectContainer() throws Exception {
+ moduleConfigurationHandle.checkEnvironmentVariables();
+
+ // Jetty uses the thread context ClassLoader to find things
+ // This needs to be null for the DevAppClassLoader to
+ // work correctly. There have been clients that set this to
+ // something else.
+ Thread currentThread = Thread.currentThread();
+ ClassLoader previousCcl = currentThread.getContextClassLoader();
+
+ HttpConfiguration configuration = new HttpConfiguration();
+ configuration.setSendDateHeader(false);
+ configuration.setSendServerVersion(false);
+ configuration.setSendXPoweredBy(false);
+ // Try to enable virtual threads if requested on java21:
+ if (Boolean.getBoolean("appengine.use.virtualthreads")) {
+ QueuedThreadPool threadPool = new QueuedThreadPool();
+ threadPool.setVirtualThreadsExecutor(VirtualThreads.getDefaultVirtualThreadsExecutor());
+ server = new Server(threadPool);
+ } else {
+ server = new Server();
+ }
+ try {
+ NetworkTrafficServerConnector connector =
+ new NetworkTrafficServerConnector(
+ server,
+ null,
+ null,
+ null,
+ 0,
+ Runtime.getRuntime().availableProcessors(),
+ new HttpConnectionFactory(configuration));
+ connector.setHost(address);
+ connector.setPort(port);
+ // Linux keeps the port blocked after shutdown if we don't disable this.
+ // TODO: WHAT IS THIS connector.setSoLingerTime(0);
+ connector.open();
+
+ server.addConnector(connector);
+
+ port = connector.getLocalPort();
+ } finally {
+ currentThread.setContextClassLoader(previousCcl);
+ }
+ }
+
+ @Override
+ protected void startContainer() throws Exception {
+ context.setAttribute(WEB_XML_ATTR, webXml);
+ context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
+
+ // Jetty uses the thread context ClassLoader to find things
+ // This needs to be null for the DevAppClassLoader to
+ // work correctly. There have been clients that set this to
+ // something else.
+ Thread currentThread = Thread.currentThread();
+ ClassLoader previousCcl = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(null);
+
+ try {
+ // Wrap context in a handler that manages the ApiProxy ThreadLocal.
+ ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
+ context.insertHandler(apiHandler);
+ server.setHandler(context);
+ SessionManagerHandler unused =
+ SessionManagerHandler.create(
+ SessionManagerHandler.Config.builder()
+ .setEnableSession(isSessionsEnabled())
+ .setServletContextHandler(context)
+ .build());
+
+ server.start();
+ } finally {
+ currentThread.setContextClassLoader(previousCcl);
+ }
+ }
+
+ @Override
+ protected void stopContainer() throws Exception {
+ server.stop();
+ }
+
+ /**
+ * If the property "appengine.fullscan.seconds" is set to a positive integer, the web app content
+ * (deployment descriptors, classes/ and lib/) is scanned for changes that will trigger the
+ * reloading of the application. If the property is not set (default), we monitor the webapp war
+ * file or the appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp
+ * whenever an update is detected, i.e. a newer timestamp for the monitored file. As a
+ * single-context deployment, add/delete is not applicable here.
+ *
+ *
appengine-web.xml will be reloaded too. However, changes that require a module instance
+ * restart, e.g. address/port, will not be part of the reload.
+ */
+ @Override
+ protected void startHotDeployScanner() throws Exception {
+ String fullScanInterval = System.getProperty("appengine.fullscan.seconds");
+ if (fullScanInterval != null) {
+ try {
+ int interval = Integer.parseInt(fullScanInterval);
+ if (interval < 1) {
+ log.info("Full scan of the web app for changes is disabled.");
+ return;
+ }
+ log.info("Full scan of the web app in place every " + interval + "s.");
+ fullWebAppScanner(interval);
+ return;
+ } catch (NumberFormatException ex) {
+ log.log(Level.WARNING, "appengine.fullscan.seconds property is not an integer:", ex);
+ log.log(Level.WARNING, "Using the default scanning method.");
+ }
+ }
+ scanner = new Scanner();
+ scanner.setReportExistingFilesOnStartup(false);
+ scanner.setScanInterval(SCAN_INTERVAL_SECONDS);
+ scanner.setScanDirs(ImmutableList.of(getScanTarget().toPath()));
+ scanner.setFilenameFilter(
+ new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ try {
+ if (name.equals(getScanTarget().getName())) {
+ return true;
+ }
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ });
+ scanner.addListener(new ScannerListener());
+ scanner.start();
+ }
+
+ @Override
+ protected void stopHotDeployScanner() throws Exception {
+ if (scanner != null) {
+ scanner.stop();
+ }
+ scanner = null;
+ }
+
+ private class ScannerListener implements Scanner.DiscreteListener {
+ @Override
+ public void fileAdded(String filename) throws Exception {
+ // trigger a reload
+ fileChanged(filename);
+ }
+
+ @Override
+ public void fileChanged(String filename) throws Exception {
+ log.info(filename + " updated, reloading the webapp!");
+ reloadWebApp();
+ }
+
+ @Override
+ public void fileRemoved(String filename) throws Exception {
+ // ignored
+ }
+ }
+
+ /** To minimize the overhead, we point the scanner right to the single file in question. */
+ private File getScanTarget() throws Exception {
+ if (appDir.isFile() || context.getWebInf() == null) {
+ // war or running without a WEB-INF
+ return appDir;
+ } else {
+ // by this point, we know the WEB-INF must exist
+ // TODO: consider scanning the whole web-inf
+ return new File(context.getWebInf().getPath() + File.separator + "appengine-web.xml");
+ }
+ }
+
+ private void fullWebAppScanner(int interval) throws IOException {
+ String webInf = context.getWebInf().getPath().toString();
+ List scanList = new ArrayList<>();
+ Collections.addAll(
+ scanList,
+ new File(webInf, "classes").toPath(),
+ new File(webInf, "lib").toPath(),
+ new File(webInf, "web.xml").toPath(),
+ new File(webInf, "appengine-web.xml").toPath());
+
+ scanner = new Scanner();
+ scanner.setScanInterval(interval);
+ scanner.setScanDirs(scanList);
+ scanner.setReportExistingFilesOnStartup(false);
+ scanner.setScanDepth(3);
+
+ scanner.addListener(
+ new Scanner.BulkListener() {
+ @Override
+ public void pathsChanged(Map changeSet) throws Exception {
+ log.info("A file has changed, reloading the web application.");
+ reloadWebApp();
+ }
+ });
+
+ LifeCycle.start(scanner);
+ }
+
+ /**
+ * Assuming Jetty handles race conditions nicely, as this is how Jetty handles a hot deploy too.
+ */
+ @Override
+ protected void reloadWebApp() throws Exception {
+ // Tell Jetty to stop caching jar files, because the changed app may invalidate that
+ // caching.
+ // TODO: Resource.setDefaultUseCaches(false);
+
+ // stop the context
+ server.getHandler().stop();
+ server.stop();
+ moduleConfigurationHandle.restoreSystemProperties();
+ moduleConfigurationHandle.readConfiguration();
+ moduleConfigurationHandle.checkEnvironmentVariables();
+ extractFieldsFromWebModule(moduleConfigurationHandle.getModule());
+
+ /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
+ Thread currentThread = Thread.currentThread();
+ ClassLoader previousCcl = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(null);
+ try {
+ // reinit the context
+ initContext();
+ installLocalInitializationEnvironment();
+ context.setAttribute(WEB_XML_ATTR, webXml);
+ context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
+
+ // reset the handler
+ ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
+ context.insertHandler(apiHandler);
+ server.setHandler(context);
+ SessionManagerHandler unused =
+ SessionManagerHandler.create(
+ SessionManagerHandler.Config.builder()
+ .setEnableSession(isSessionsEnabled())
+ .setServletContextHandler(context)
+ .build());
+ // restart the context (on the same module instance)
+ server.start();
+ } finally {
+ currentThread.setContextClassLoader(previousCcl);
+ }
+ }
+
+ @Override
+ public AppContext getAppContext() {
+ return appContext;
+ }
+
+ @Override
+ public void forwardToServer(HttpServletRequest hrequest, HttpServletResponse hresponse)
+ throws IOException, ServletException {
+ log.finest("forwarding request to module: " + appEngineWebXml.getModule() + "." + instance);
+ RequestDispatcher requestDispatcher =
+ context.getServletContext().getRequestDispatcher(hrequest.getRequestURI());
+ requestDispatcher.forward(hrequest, hresponse);
+ }
+
+ private File determineAppRoot() throws IOException {
+ // Use the context's WEB-INF location instead of appDir since the latter
+ // might refer to a WAR whereas the former gets updated by Jetty when it
+ // extracts a WAR to a temporary directory.
+ Resource webInf = context.getWebInf();
+ if (webInf == null) {
+ if (userCodeClasspathManager.requiresWebInf()) {
+ throw new AppEngineConfigException(
+ "Supplied application has to contain WEB-INF directory.");
+ }
+ return appDir;
+ }
+ return webInf.getPath().toFile().getParentFile();
+ }
+
+ /**
+ * {@code ApiProxyHandler} wraps around an existing {@link Handler} and creates a {@link
+ * com.google.apphosting.api.ApiProxy.Environment} which is stored as a request Attribute and then
+ * set/cleared on a ThreadLocal by the ContextScopeListener {@link ThreadLocal}.
+ */
+ private class ApiProxyHandler extends ScopedHandler {
+ @SuppressWarnings("hiding") // Hides AbstractContainerService.appEngineWebXml
+ private final AppEngineWebXml appEngineWebXml;
+
+ public ApiProxyHandler(AppEngineWebXml appEngineWebXml) {
+ this.appEngineWebXml = appEngineWebXml;
+ }
+
+ @Override
+ public void doHandle(
+ String target,
+ Request baseRequest,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+ nextHandle(target, baseRequest, request, response);
+ }
+
+ @Override
+ public void doScope(
+ String target,
+ Request baseRequest,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+
+ if (baseRequest.getDispatcherType() == DispatcherType.REQUEST) {
+ org.eclipse.jetty.server.Request.addCompletionListener(
+ baseRequest.getCoreRequest(),
+ t -> {
+ try {
+ // a special hook with direct access to the container instance
+ // we invoke this only after the normal request processing,
+ // in order to generate a valid response
+ if (request.getRequestURI().startsWith(AH_URL_RELOAD)) {
+ try {
+ reloadWebApp();
+ log.info("Reloaded the webapp context: " + request.getParameter("info"));
+ } catch (Exception ex) {
+ log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
+ }
+ }
+ } finally {
+
+ LocalEnvironment env =
+ (LocalEnvironment) request.getAttribute(LocalEnvironment.class.getName());
+ if (env != null) {
+ environments.remove(env);
+
+ // Acquire all of the semaphores back, which will block if any are outstanding.
+ Semaphore semaphore =
+ (Semaphore) env.getAttributes().get(LocalEnvironment.API_CALL_SEMAPHORE);
+ try {
+ semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ log.log(
+ Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
+ }
+
+ try {
+ ApiProxy.setEnvironmentForCurrentThread(env);
+
+ // Invoke all of the registered RequestEndListeners.
+ env.callRequestEndListeners();
+
+ if (apiProxyDelegate instanceof ApiProxyLocal) {
+ // If apiProxyDelegate is not instanceof ApiProxyLocal, we are presumably
+ // running in
+ // the devappserver2 environment, where the master web server in Python will
+ // take care
+ // of logging requests.
+ ApiProxyLocal apiProxyLocal = (ApiProxyLocal) apiProxyDelegate;
+ String appId = env.getAppId();
+ String versionId = env.getVersionId();
+ String requestId = DevLogHandler.getRequestId();
+
+ LocalLogService logService =
+ (LocalLogService) apiProxyLocal.getService(LocalLogService.PACKAGE);
+
+ @SuppressWarnings("NowMillis")
+ long nowMillis = System.currentTimeMillis();
+ logService.addRequestInfo(
+ appId,
+ versionId,
+ requestId,
+ request.getRemoteAddr(),
+ request.getRemoteUser(),
+ baseRequest.getTimeStamp() * 1000,
+ nowMillis * 1000,
+ request.getMethod(),
+ request.getRequestURI(),
+ request.getProtocol(),
+ request.getHeader("User-Agent"),
+ true,
+ response.getStatus(),
+ request.getHeader("Referrer"));
+ logService.clearResponseSize();
+ }
+ } finally {
+ ApiProxy.clearEnvironmentForCurrentThread();
+ }
+ }
+ }
+ });
+
+ Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_API_CALLS);
+
+ LocalEnvironment env =
+ new LocalHttpRequestEnvironment(
+ appEngineWebXml.getAppId(),
+ WebModule.getModuleName(appEngineWebXml),
+ appEngineWebXml.getMajorVersionId(),
+ instance,
+ getPort(),
+ request,
+ SOFT_DEADLINE_DELAY_MS,
+ modulesFilterHelper);
+ env.getAttributes().put(LocalEnvironment.API_CALL_SEMAPHORE, semaphore);
+ env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + devAppServer.getPort());
+
+ request.setAttribute(LocalEnvironment.class.getName(), env);
+ environments.add(env);
+ }
+
+ // We need this here because the ContextScopeListener is invoked before
+ // this and so the Environment has not yet been created.
+ ApiProxy.Environment oldEnv = enterScope(request);
+ try {
+ super.doScope(target, baseRequest, request, response);
+ } finally {
+ exitScope(oldEnv);
+ }
+ }
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/JettyResponseRewriterFilter.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/JettyResponseRewriterFilter.java
new file mode 100644
index 000000000..3e1905fa4
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/JettyResponseRewriterFilter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.development.jetty;
+
+import com.google.appengine.tools.development.ResponseRewriterFilter;
+import com.google.common.base.Preconditions;
+import java.io.OutputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A filter that rewrites the response headers and body from the user's application.
+ *
+ *
This sanitises the headers to ensure that they are sensible and the user is not setting
+ * sensitive headers, such as Content-Length, incorrectly. It also deletes the body if the response
+ * status code indicates a non-body status.
+ *
+ *
This also strips out some request headers before passing the request to the application.
+ */
+public class JettyResponseRewriterFilter extends ResponseRewriterFilter {
+
+ public JettyResponseRewriterFilter() {
+ super();
+ }
+
+ /**
+ * Creates a JettyResponseRewriterFilter for testing purposes, which mocks the current time.
+ *
+ * @param mockTimestamp Indicates that the current time will be emulated with this timestamp.
+ */
+ public JettyResponseRewriterFilter(long mockTimestamp) {
+ super(mockTimestamp);
+ }
+
+ @Override
+ protected ResponseWrapper getResponseWrapper(HttpServletResponse response) {
+ return new ResponseWrapper(response);
+ }
+
+ private static class ResponseWrapper extends ResponseRewriterFilter.ResponseWrapper {
+
+ public ResponseWrapper(HttpServletResponse response) {
+ super(response);
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ // The user can write directly into our private buffer.
+ // The response will not be committed until all rewriting is complete.
+ if (bodyServletStream != null) {
+ return bodyServletStream;
+ } else {
+ Preconditions.checkState(bodyPrintWriter == null, "getWriter has already been called");
+ bodyServletStream = new ServletOutputStreamWrapper(body);
+ return bodyServletStream;
+ }
+ }
+
+ /** A ServletOutputStream that wraps some other OutputStream. */
+ private static class ServletOutputStreamWrapper
+ extends ResponseRewriterFilter.ResponseWrapper.ServletOutputStreamWrapper {
+
+ ServletOutputStreamWrapper(OutputStream stream) {
+ super(stream);
+ }
+
+ // New method and new new class WriteListener only in Servlet 3.1.
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ // Not used for us.
+ }
+ }
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/LocalJspC.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/LocalJspC.java
new file mode 100644
index 000000000..7eb92b002
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/LocalJspC.java
@@ -0,0 +1,96 @@
+/*
+ * 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.development.jetty;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+import org.apache.jasper.JasperException;
+import org.apache.jasper.JspC;
+import org.apache.jasper.compiler.AntCompiler;
+import org.apache.jasper.compiler.Localizer;
+import org.apache.jasper.compiler.SmapStratum;
+
+/**
+ * Simple wrapper around the Apache JSP compiler. It defines a Java compiler only to compile the
+ * user defined tag files, as it seems that this cannot be avoided. For the regular JSPs, the
+ * compilation phase is not done here but in single compiler invocation during deployment, to speed
+ * up compilation (See cr/37599187.)
+ */
+public class LocalJspC {
+
+ // Cannot use System.getProperty("java.class.path") anymore
+ // as this process can run embedded in the GAE tools JVM. so we cache
+ // the classpath parameter passed to the JSP compiler to be used to compile
+ // the generated java files for user tag libs.
+ static String classpath;
+
+ public static void main(String[] args) throws JasperException {
+ if (args.length == 0) {
+ System.out.println(Localizer.getMessage("jspc.usage"));
+ } else {
+ JspC jspc =
+ new JspC() {
+ @Override
+ public String getCompilerClassName() {
+ return LocalCompiler.class.getName();
+ }
+ };
+ jspc.setArgs(args);
+ jspc.setCompiler("extJavac");
+ jspc.setAddWebXmlMappings(true);
+ classpath = jspc.getClassPath();
+ jspc.execute();
+ }
+ }
+
+ /**
+ * Very simple compiler for JSPc that is behaving like the ANT compiler, but uses the Tools System
+ * Java compiler to speed compilation process. Only the generated code for *.tag files is compiled
+ * by JSPc even with the "-compile" flag not set.
+ */
+ public static class LocalCompiler extends AntCompiler {
+
+ // Cache the compiler and the file manager:
+ static JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+
+ @Override
+ protected void generateClass(Map smaps) {
+ // Lazily check for the existence of the compiler:
+ if (compiler == null) {
+ throw new RuntimeException(
+ "Cannot get the System Java Compiler. Please use a JDK, not a JRE.");
+ }
+ StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
+ ArrayList files = new ArrayList<>();
+ files.add(new File(ctxt.getServletJavaFileName()));
+ List optionList = new ArrayList<>();
+ // Set compiler's classpath to be same as the jspc main class's
+ optionList.addAll(Arrays.asList("-classpath", LocalJspC.classpath));
+ optionList.addAll(Arrays.asList("-encoding", ctxt.getOptions().getJavaEncoding()));
+ Iterable extends JavaFileObject> compilationUnits =
+ fileManager.getJavaFileObjectsFromFiles(files);
+ compiler.getTask(null, fileManager, null, optionList, null, compilationUnits).call();
+ }
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/LocalResourceFileServlet.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/LocalResourceFileServlet.java
new file mode 100644
index 000000000..5ac1b63e3
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/LocalResourceFileServlet.java
@@ -0,0 +1,296 @@
+/*
+ * 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.development.jetty;
+
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.apphosting.utils.config.WebXml;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee8.nested.ContextHandler;
+import org.eclipse.jetty.ee8.servlet.ServletHandler;
+import org.eclipse.jetty.ee8.servlet.ServletHandler.MappedServlet;
+import org.eclipse.jetty.ee8.webapp.WebAppContext;
+import org.eclipse.jetty.http.pathmap.MappedResource;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * {@code ResourceFileServlet} is a copy of {@code org.mortbay.jetty.servlet.DefaultServlet} that
+ * has been trimmed down to only support the subset of features that we want to take advantage of
+ * (e.g. no gzipping, no chunked encoding, no buffering, etc.). A number of Jetty-specific
+ * optimizations and assumptions have also been removed (e.g. use of custom header manipulation
+ * API's, use of {@code ByteArrayBuffer} instead of Strings, etc.).
+ *
+ *
A few remaining Jetty-centric details remain, such as use of the {@link
+ * ContextHandler.APIContext} class, and Jetty-specific request attributes, but these are specific
+ * cases where there is no servlet-engine-neutral API available. This class also uses Jetty's {@link
+ * Resource} class as a convenience, but could be converted to use {@link
+ * javax.servlet.ServletContext#getResource(String)} instead.
+ */
+public class LocalResourceFileServlet extends HttpServlet {
+ private static final Logger logger = Logger.getLogger(LocalResourceFileServlet.class.getName());
+
+ private StaticFileUtils staticFileUtils;
+ private Resource resourceBase;
+ private String[] welcomeFiles;
+ private String resourceRoot;
+
+ /**
+ * Initialize the servlet by extracting some useful configuration data from the current {@link
+ * javax.servlet.ServletContext}.
+ */
+ @Override
+ public void init() throws ServletException {
+ ContextHandler.APIContext context = (ContextHandler.APIContext) getServletContext();
+ staticFileUtils = new StaticFileUtils(context);
+
+ // AFAICT, there is no real API to retrieve this information, so
+ // we access Jetty's internal state.
+ welcomeFiles = context.getContextHandler().getWelcomeFiles();
+
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ getServletContext()
+ .getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ resourceRoot = appEngineWebXml.getPublicRoot();
+ try {
+
+ String base;
+ if (resourceRoot.startsWith("/")) {
+ base = resourceRoot;
+ } else {
+ base = "/" + resourceRoot;
+ }
+ // In Jetty 9 "//public" is not seen as "/public" .
+ resourceBase = ResourceFactory.root().newResource(context.getResource(base));
+ } catch (MalformedURLException ex) {
+ logger.log(Level.WARNING, "Could not initialize:", ex);
+ throw new ServletException(ex);
+ }
+ }
+
+ public static final java.lang.String __INCLUDE_JETTY = RequestDispatcher.FORWARD_REQUEST_URI;
+ public static final java.lang.String __INCLUDE_SERVLET_PATH = RequestDispatcher.INCLUDE_SERVLET_PATH;
+ public static final java.lang.String __INCLUDE_PATH_INFO = RequestDispatcher.INCLUDE_PATH_INFO;
+ public static final java.lang.String __FORWARD_JETTY = RequestDispatcher.FORWARD_REQUEST_URI;
+
+ /** Retrieve the static resource file indicated. */
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String servletPath;
+ String pathInfo;
+
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ getServletContext()
+ .getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ WebXml webXml =
+ (WebXml) getServletContext().getAttribute("com.google.appengine.tools.development.webXml");
+
+ Boolean forwarded = request.getAttribute(__FORWARD_JETTY) != null;
+ if (forwarded == null) {
+ forwarded = Boolean.FALSE;
+ }
+
+ Boolean included = request.getAttribute(__INCLUDE_JETTY) != null;
+ if (included != null && included) {
+ servletPath = (String) request.getAttribute(__INCLUDE_SERVLET_PATH);
+ pathInfo = (String) request.getAttribute(__INCLUDE_PATH_INFO);
+ if (servletPath == null) {
+ servletPath = request.getServletPath();
+ pathInfo = request.getPathInfo();
+ }
+ } else {
+ included = Boolean.FALSE;
+ servletPath = request.getServletPath();
+ pathInfo = request.getPathInfo();
+ }
+
+ String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
+
+ if (maybeServeWelcomeFile(pathInContext, included, request, response)) {
+ // We served a welcome file (either via redirecting, forwarding, or including).
+ return;
+ }
+
+ // Find the resource
+ Resource resource = null;
+ try {
+ resource = getResource(pathInContext);
+
+ // Handle resource
+ if (resource != null && resource.isDirectory()) {
+ if (included || staticFileUtils.passConditionalHeaders(request, response, resource)) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
+ } else {
+ if (resource == null || !resource.exists()) {
+ logger.warning("No file found for: " + pathInContext);
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ } else {
+ boolean isStatic = appEngineWebXml.includesStatic(resourceRoot + pathInContext);
+ boolean isResource = appEngineWebXml.includesResource(resourceRoot + pathInContext);
+ boolean usesRuntime = webXml.matches(pathInContext);
+ Boolean isWelcomeFile =
+ (Boolean)
+ request.getAttribute("com.google.appengine.tools.development.isWelcomeFile");
+ if (isWelcomeFile == null) {
+ isWelcomeFile = false;
+ }
+
+ if (!isStatic && !usesRuntime && !(included || forwarded)) {
+ logger.warning(
+ "Can not serve "
+ + pathInContext
+ + " directly. "
+ + "You need to include it in in your "
+ + "appengine-web.xml.");
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ } else if (!isResource && !isWelcomeFile && (included || forwarded)) {
+ logger.warning(
+ "Could not serve "
+ + pathInContext
+ + " from a forward or "
+ + "include. You need to include it in in "
+ + "your appengine-web.xml.");
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ // passConditionalHeaders will set response headers, and
+ // return true if we also need to send the content.
+ if (included || staticFileUtils.passConditionalHeaders(request, response, resource)) {
+ staticFileUtils.sendData(request, response, included, resource);
+ }
+ }
+ }
+ } finally {
+ if (resource != null) {
+ // TODO: how to release
+ // resource.release();
+ }
+ }
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ doGet(request, response);
+ }
+
+ /**
+ * Get Resource to serve.
+ *
+ * @param pathInContext The path to find a resource for.
+ * @return The resource to serve. Can be null.
+ */
+ private Resource getResource(String pathInContext) {
+ try {
+ if (resourceBase != null) {
+ return resourceBase.resolve(pathInContext);
+ }
+ } catch (Throwable t) {
+ logger.log(Level.WARNING, "Could not find: " + pathInContext, t);
+ }
+ return null;
+ }
+
+ /**
+ * Finds a matching welcome file for the supplied path and, if found, serves it to the user. This
+ * will be the first entry in the list of configured {@link #welcomeFiles welcome files} that
+ * exists within the directory referenced by the path. If the resource is not a directory, or no
+ * matching file is found, then null is returned. The list of welcome files is read
+ * from the {@link ContextHandler} for this servlet, or "index.jsp" , "index.html" if
+ * that is null.
+ *
+ * @return true if a welcome file was served, false otherwise
+ * @throws IOException
+ * @throws MalformedURLException
+ */
+ private boolean maybeServeWelcomeFile(
+ String path, boolean included, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ if (welcomeFiles == null) {
+ return false;
+ }
+
+ // Add a slash for matching purposes. If we needed this slash, we
+ // are not doing an include, and we're not going to redirect
+ // somewhere else we'll redirect the user to add it later.
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ getServletContext()
+ .getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ ContextHandler.APIContext context = (ContextHandler.APIContext) getServletContext();
+ ServletHandler handler = ((WebAppContext) context.getContextHandler()).getServletHandler();
+ MappedResource defaultEntry = handler.getHolderEntry("/");
+ MappedResource jspEntry = handler.getHolderEntry("/foo.jsp");
+
+ // Search for dynamic welcome files.
+ for (String welcomeName : welcomeFiles) {
+ String welcomePath = path + welcomeName;
+ String relativePath = welcomePath.substring(1);
+
+ MappedResource entry = handler.getHolderEntry(welcomePath);
+ if (!Objects.equals(entry, defaultEntry) && !Objects.equals(entry, jspEntry)) {
+ // It's a path mapped to a servlet. Forward to it.
+ RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName);
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, included, request, response);
+ }
+
+ Resource welcomeFile = getResource(path + welcomeName);
+ if (welcomeFile != null && welcomeFile.exists()) {
+ if (!Objects.equals(entry, defaultEntry)) {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName);
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, included, request, response);
+ }
+ if (appEngineWebXml.includesResource(relativePath)) {
+ // It's a resource file. Forward to it.
+ RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName);
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, included, request, response);
+ }
+ }
+ RequestDispatcher namedDispatcher = context.getNamedDispatcher(welcomeName);
+ if (namedDispatcher != null) {
+ // It's a servlet name (allowed by Servlet 2.4 spec). We have
+ // to forward to it.
+ return staticFileUtils.serveWelcomeFileAsForward(
+ namedDispatcher, included,
+ request, response);
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/StaticFileFilter.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/StaticFileFilter.java
new file mode 100644
index 000000000..fca7557a3
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/StaticFileFilter.java
@@ -0,0 +1,233 @@
+/*
+ * 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.development.jetty;
+
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.InvalidPathException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee8.nested.ContextHandler;
+import org.eclipse.jetty.ee8.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * {@code StaticFileFilter} is a {@link Filter} that replicates the static file serving logic that
+ * is present in the PFE and AppServer. This logic was originally implemented in {@link
+ * LocalResourceFileServlet} but static file serving needs to take precedence over all other
+ * servlets and filters.
+ */
+public class StaticFileFilter implements Filter {
+ private static final Logger logger = Logger.getLogger(StaticFileFilter.class.getName());
+
+ private StaticFileUtils staticFileUtils;
+ private AppEngineWebXml appEngineWebXml;
+ private Resource resourceBase;
+ private String[] welcomeFiles;
+ private String resourceRoot;
+ private ContextHandler.APIContext servletContext;
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ servletContext =
+ ServletContextHandler.getServletContextHandler(servletContext).getServletContext();
+ staticFileUtils = new StaticFileUtils(servletContext);
+
+ // AFAICT, there is no real API to retrieve this information, so
+ // we access Jetty's internal state.
+ welcomeFiles = servletContext.getContextHandler().getWelcomeFiles();
+
+ appEngineWebXml =
+ (AppEngineWebXml)
+ servletContext.getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+ resourceRoot = appEngineWebXml.getPublicRoot();
+
+ try {
+ String base;
+ if (resourceRoot.startsWith("/")) {
+ base = resourceRoot;
+ } else {
+ base = "/" + resourceRoot;
+ }
+ // in Jetty 9 "//public" is not seen as "/public".
+ resourceBase = ResourceFactory.root().newResource(servletContext.getResource(base));
+ } catch (MalformedURLException ex) {
+ logger.log(Level.WARNING, "Could not initialize:", ex);
+ throw new ServletException(ex);
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws ServletException, IOException {
+ Boolean forwarded = (Boolean) request.getAttribute(LocalResourceFileServlet.__FORWARD_JETTY);
+ if (forwarded == null) {
+ forwarded = Boolean.FALSE;
+ }
+
+ Boolean included = (Boolean) request.getAttribute(LocalResourceFileServlet.__INCLUDE_JETTY);
+ if (included == null) {
+ included = Boolean.FALSE;
+ }
+
+ if (forwarded || included) {
+ // If we're forwarded or included, the request is already in the
+ // runtime and static file serving is not relevant.
+ chain.doFilter(request, response);
+ return;
+ }
+
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ String servletPath = httpRequest.getServletPath();
+ String pathInfo = httpRequest.getPathInfo();
+ String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
+
+ if (maybeServeWelcomeFile(pathInContext, httpRequest, httpResponse)) {
+ // We served a welcome file.
+ return;
+ }
+
+ // Find the resource
+ Resource resource = null;
+ try {
+ resource = getResource(pathInContext);
+
+ // Handle resource
+ if (resource != null && resource.exists() && !resource.isDirectory()) {
+ if (appEngineWebXml.includesStatic(resourceRoot + pathInContext)) {
+ // passConditionalHeaders will set response headers, and
+ // return true if we also need to send the content.
+ if (staticFileUtils.passConditionalHeaders(httpRequest, httpResponse, resource)) {
+ staticFileUtils.sendData(httpRequest, httpResponse, false, resource);
+ }
+ return;
+ }
+ }
+ } finally {
+ if (resource != null) {
+ // TODO: how to release
+ // resource.release();
+ }
+ }
+ chain.doFilter(request, response);
+ }
+
+ /**
+ * Get Resource to serve.
+ *
+ * @param pathInContext The path to find a resource for.
+ * @return The resource to serve.
+ */
+ private Resource getResource(String pathInContext) {
+ try {
+ if (resourceBase != null) {
+ return resourceBase.resolve(pathInContext);
+ }
+ } catch (InvalidPathException ex) {
+ // Do not warn for Windows machines for trying to access invalid paths like
+ // "hello/po:tato/index.html" that gives a InvalidPathException: Illegal char <:> error.
+ // This is definitely not a static resource.
+ if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
+ logger.log(Level.WARNING, "Could not find: " + pathInContext, ex);
+ }
+ } catch (Throwable t) {
+ logger.log(Level.WARNING, "Could not find: " + pathInContext, t);
+ }
+ return null;
+ }
+
+ /**
+ * Finds a matching welcome file for the supplied path and, if found, serves it to the user. This
+ * will be the first entry in the list of configured {@link #welcomeFiles welcome files} that
+ * exists within the directory referenced by the path.
+ *
+ * @param path
+ * @param request
+ * @param response
+ * @return true if a welcome file was served, false otherwise
+ * @throws IOException
+ * @throws MalformedURLException
+ */
+ private boolean maybeServeWelcomeFile(
+ String path, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ if (welcomeFiles == null) {
+ return false;
+ }
+
+ // Add a slash for matching purposes. If we needed this slash, we
+ // are not doing an include, and we're not going to redirect
+ // somewhere else we'll redirect the user to add it later.
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+
+ // First search for static welcome files.
+ for (String welcomeName : welcomeFiles) {
+ final String welcomePath = path + welcomeName;
+
+ Resource welcomeFile = getResource(path + welcomeName);
+ if (welcomeFile != null && welcomeFile.exists()) {
+ if (appEngineWebXml.includesStatic(resourceRoot + welcomePath)) {
+ // In production, we optimize this case by routing requests
+ // for static welcome files directly to the static file
+ // (without a redirect). This logic is here to emulate that
+ // case.
+ //
+ // Note that we want to forward to *our* default servlet,
+ // even if the default servlet for this webapp has been
+ // overridden.
+ RequestDispatcher dispatcher = servletContext.getNamedDispatcher("_ah_default");
+ // We need to pass in the new path so it doesn't try to do
+ // its own (dynamic) welcome path logic.
+ request =
+ new HttpServletRequestWrapper(request) {
+ @Override
+ public String getServletPath() {
+ return welcomePath;
+ }
+
+ @Override
+ public String getPathInfo() {
+ return "";
+ }
+ };
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, false, request, response);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void destroy() {}
+}
diff --git a/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/StaticFileUtils.java b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/StaticFileUtils.java
new file mode 100644
index 000000000..ef2b9a5be
--- /dev/null
+++ b/runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/StaticFileUtils.java
@@ -0,0 +1,424 @@
+/*
+ * 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.development.jetty;
+
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee8.nested.ContextHandler;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.io.WriterOutputStream;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * {@code StaticFileUtils} is a collection of utilities shared by {@link LocalResourceFileServlet}
+ * and {@link StaticFileFilter}.
+ */
+public class StaticFileUtils {
+ private static final String DEFAULT_CACHE_CONTROL_VALUE = "public, max-age=600";
+
+ private final ContextHandler.APIContext servletContext;
+
+ public StaticFileUtils(ContextHandler.APIContext servletContext) {
+ this.servletContext = servletContext;
+ }
+
+ public boolean serveWelcomeFileAsRedirect(
+ String path, boolean included, HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ if (included) {
+ // This is an error. We don't have the file so we can't
+ // include it in the request.
+ return false;
+ }
+
+ // Even if the trailing slash is missing, don't bother trying to
+ // add it. We're going to redirect to a full file anyway.
+ response.setContentLength(0);
+ String q = request.getQueryString();
+ if (q != null && q.length() != 0) {
+ response.sendRedirect(path + "?" + q);
+ } else {
+ response.sendRedirect(path);
+ }
+ return true;
+ }
+
+ public boolean serveWelcomeFileAsForward(
+ RequestDispatcher dispatcher,
+ boolean included,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+ // If the user didn't specify a slash but we know we want a
+ // welcome file, redirect them to add the slash now.
+ if (!included && !request.getRequestURI().endsWith("/")) {
+ redirectToAddSlash(request, response);
+ return true;
+ }
+
+ request.setAttribute("com.google.appengine.tools.development.isWelcomeFile", true);
+ if (dispatcher != null) {
+ if (included) {
+ dispatcher.include(request, response);
+ } else {
+ dispatcher.forward(request, response);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public void redirectToAddSlash(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ StringBuffer buf = request.getRequestURL();
+ int param = buf.lastIndexOf(";");
+ if (param < 0) {
+ buf.append('/');
+ } else {
+ buf.insert(param, '/');
+ }
+ String q = request.getQueryString();
+ if (q != null && q.length() != 0) {
+ buf.append('?');
+ buf.append(q);
+ }
+ response.setContentLength(0);
+ response.sendRedirect(response.encodeRedirectURL(buf.toString()));
+ }
+
+ /**
+ * Check the headers to see if content needs to be sent.
+ *
+ * @return true if the content should be sent, false otherwise.
+ */
+ public boolean passConditionalHeaders(
+ HttpServletRequest request, HttpServletResponse response, Resource resource)
+ throws IOException {
+ if (!request.getMethod().equals(HttpMethod.HEAD.asString())) {
+ String ifms = request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ if (ifms != null) {
+ long ifmsl = -1;
+ try {
+ ifmsl = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ } catch (IllegalArgumentException e) {
+ // Ignore bad date formats.
+ }
+ if (ifmsl != -1) {
+ if (resource.lastModified().toEpochMilli() <= ifmsl) {
+ response.reset();
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ response.flushBuffer();
+ return false;
+ }
+ }
+ }
+
+ // Parse the if[un]modified dates and compare to resource
+ long date = -1;
+ try {
+ date = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
+ } catch (IllegalArgumentException e) {
+ // Ignore bad date formats.
+ }
+ if (date != -1) {
+ if (resource.lastModified().toEpochMilli() > date) {
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /** Write or include the specified resource. */
+ public void sendData(
+ HttpServletRequest request, HttpServletResponse response, boolean include, Resource resource)
+ throws IOException {
+ long contentLength = resource.length();
+ if (!include) {
+ writeHeaders(response, request.getRequestURI(), resource, contentLength);
+ }
+
+ // Get the output stream (or writer)
+ OutputStream out = null;
+ try {
+ out = response.getOutputStream();
+ } catch (IllegalStateException e) {
+ out = new WriterOutputStream(response.getWriter());
+ }
+
+ IO.copy(resource.newInputStream(), out, contentLength);
+ }
+
+ /** Write the headers that should accompany the specified resource. */
+ public void writeHeaders(
+ HttpServletResponse response, String requestPath, Resource resource, long count) {
+ // Set Content-Length. Users are not allowed to override this. Therefore, we
+ // may do this before adding custom static headers.
+ if (count != -1) {
+ if (count < Integer.MAX_VALUE) {
+ response.setContentLength((int) count);
+ } else {
+ response.setHeader(HttpHeader.CONTENT_LENGTH.asString(), String.valueOf(count));
+ }
+ }
+
+ Set headersApplied = addUserStaticHeaders(requestPath, response);
+
+ // Set Content-Type.
+ if (!headersApplied.contains("content-type")) {
+ String contentType = servletContext.getMimeType(resource.getName());
+ if (contentType != null) {
+ response.setContentType(contentType);
+ }
+ }
+
+ // Set Last-Modified.
+ if (!headersApplied.contains("last-modified")) {
+ response.setDateHeader(
+ HttpHeader.LAST_MODIFIED.asString(), resource.lastModified().toEpochMilli());
+ }
+
+ // Set Cache-Control to the default value if it was not explicitly set.
+ if (!headersApplied.contains(HttpHeader.CACHE_CONTROL.asString().toLowerCase())) {
+ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), DEFAULT_CACHE_CONTROL_VALUE);
+ }
+ }
+
+ /**
+ * Adds HTTP Response headers that are specified in appengine-web.xml. The user may specify
+ * headers explicitly using the {@code http-header} element. Also the user may specify cache
+ * expiration headers implicitly using the {@code expiration} attribute. There is no check for
+ * consistency between different specified headers.
+ *
+ * @param localFilePath The path to the static file being served.
+ * @param response The HttpResponse object to which headers will be added
+ * @return The Set of the names of all headers that were added, canonicalized to lower case.
+ */
+ @VisibleForTesting
+ Set addUserStaticHeaders(String localFilePath, HttpServletResponse response) {
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ servletContext.getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ Set headersApplied = new HashSet<>();
+ for (AppEngineWebXml.StaticFileInclude include : appEngineWebXml.getStaticFileIncludes()) {
+ Pattern pattern = include.getRegularExpression();
+ if (pattern.matcher(localFilePath).matches()) {
+ for (Map.Entry entry : include.getHttpHeaders().entrySet()) {
+ response.addHeader(entry.getKey(), entry.getValue());
+ headersApplied.add(entry.getKey().toLowerCase());
+ }
+ String expirationString = include.getExpiration();
+ if (expirationString != null) {
+ addCacheControlHeaders(headersApplied, expirationString, response);
+ }
+ break;
+ }
+ }
+ return headersApplied;
+ }
+
+ /**
+ * Adds HTTP headers to the response to describe cache expiration behavior, based on the {@code
+ * expires} attribute of the {@code includes} element of the {@code static-files} element of
+ * appengine-web.xml.
+ *
+ *
We follow the same logic that is used in production App Engine. This includes:
+ *
+ *
+ *
There is no coordination between these headers (implied by the 'expires' attribute) and
+ * explicitly specified headers (expressed with the 'http-header' sub-element). If the user
+ * specifies contradictory headers then we will include contradictory headers.
+ *
If the expiration time is zero then we specify that the response should not be cached
+ * using three different headers: {@code Pragma: no-cache}, {@code Expires: 0} and {@code
+ * Cache-Control: no-cache, must-revalidate}.
+ *
If the expiration time is positive then we specify that the response should be cached for
+ * that many seconds using two different headers: {@code Expires: num-seconds} and {@code
+ * Cache-Control: public, max-age=num-seconds}.
+ *
If the expiration time is not specified then we use a default value of 10 minutes
+ *
+ *
+ * Note that there is one aspect of the production App Engine logic that is not replicated here.
+ * In production App Engine if the url to a static file is protected by a security constraint in
+ * web.xml then {@code Cache-Control: private} is used instead of {@code Cache-Control: public}.
+ * In the development App Server {@code Cache-Control: public} is always used.
+ *
+ *
Also if the expiration time is specified but cannot be parsed as a non-negative number of
+ * seconds then a RuntimeException is thrown.
+ *
+ * @param headersApplied Set of headers that have been applied, canonicalized to lower-case. Any
+ * new headers applied in this method will be added to the set.
+ * @param expiration The expiration String specified in appengine-web.xml
+ * @param response The HttpServletResponse into which we will write the HTTP headers.
+ */
+ private static void addCacheControlHeaders(
+ Set headersApplied, String expiration, HttpServletResponse response) {
+ // The logic in this method is replicating and should be kept in sync with
+ // the corresponding logic in production App Engine which is implemented
+ // in AppServerResponse::SetExpiration() in the file
+ // apphosting/appserver/appserver_response.cc. See also
+ // HTTPResponse::SetNotCacheable(), HTTPResponse::SetCacheablePrivate(),
+ // and HTTPResponse::SetCacheablePublic() in webutil/http/httpresponse.cc
+
+ int expirationSeconds = parseExpirationSpecifier(expiration);
+ if (expirationSeconds == 0) {
+ response.addHeader("Pragma", "no-cache");
+ response.addHeader(HttpHeader.CACHE_CONTROL.asString(), "no-cache, must-revalidate");
+ response.addDateHeader(HttpHeader.EXPIRES.asString(), 0);
+ headersApplied.add(HttpHeader.CACHE_CONTROL.asString().toLowerCase());
+ headersApplied.add(HttpHeader.EXPIRES.asString().toLowerCase());
+ headersApplied.add("pragma");
+ return;
+ }
+ if (expirationSeconds > 0) {
+ // TODO If we wish to support the corresponding logic
+ // in production App Engine, we would now determine if the current
+ // request URL is protected by a security constraint in web.xml and
+ // if so we would use Cache-Control: private here instead of public.
+ response.addHeader(
+ HttpHeader.CACHE_CONTROL.asString(), "public, max-age=" + expirationSeconds);
+ response.addDateHeader(
+ HttpHeader.EXPIRES.asString(), System.currentTimeMillis() + expirationSeconds * 1000L);
+ headersApplied.add(HttpHeader.CACHE_CONTROL.asString().toLowerCase());
+ headersApplied.add(HttpHeader.EXPIRES.asString().toLowerCase());
+ return;
+ }
+ throw new RuntimeException("expirationSeconds is negative: " + expirationSeconds);
+ }
+
+ /**
+ * Parses an expiration specifier String and returns the number of seconds it represents. A valid
+ * expiration specifier is a white-space-delimited list of components, each of which is a sequence
+ * of digits, optionally followed by a single letter from the set {D, d, H, h, M, m, S, s}. For
+ * example {@code 21D 4H 30m} represents the number of seconds in 21 days, 4.5 hours.
+ *
+ * @param expirationSpecifier The non-null, non-empty expiration specifier String to parse
+ * @return The non-negative number of seconds represented by this String.
+ */
+ @VisibleForTesting
+ static int parseExpirationSpecifier(String expirationSpecifier) {
+ // The logic in this and the following few methods is replicating and should be kept in
+ // sync with the corresponding logic in production App Engine which is implemented in
+ // apphosting/api/appinfo.py. See in particular in that file _DELTA_REGEX,
+ // _EXPIRATION_REGEX, _EXPIRATION_CONVERSION, and ParseExpiration().
+ expirationSpecifier = expirationSpecifier.trim();
+ if (expirationSpecifier.isEmpty()) {
+ throwExpirationParseException("", expirationSpecifier);
+ }
+ String[] components = expirationSpecifier.split("(\\s)+");
+ int expirationSeconds = 0;
+ for (String componentSpecifier : components) {
+ expirationSeconds +=
+ parseExpirationSpeciferComponent(componentSpecifier, expirationSpecifier);
+ }
+ return expirationSeconds;
+ }
+
+ // A Pattern for matching one component of an expiration specifier String
+ private static final Pattern EXPIRATION_COMPONENT_PATTERN = Pattern.compile("^(\\d+)([dhms]?)$");
+
+ /**
+ * Parses a single component of an expiration specifier, and returns the number of seconds that
+ * the component represents. A valid component specifier is a sequence of digits, optionally
+ * followed by a single letter from the set {D, d, H, h, M, m, S, s}, indicating days, hours,
+ * minutes and seconds. A lack of a trailing letter is interpreted as seconds.
+ *
+ * @param componentSpecifier The component specifier to parse
+ * @param fullSpecifier The full specifier of which {@code componentSpecifier} is a component.
+ * This will be included in an error message if necessary.
+ * @return The number of seconds represented by {@code componentSpecifier}
+ */
+ private static int parseExpirationSpeciferComponent(
+ String componentSpecifier, String fullSpecifier) {
+ Matcher matcher = EXPIRATION_COMPONENT_PATTERN.matcher(componentSpecifier.toLowerCase());
+ if (!matcher.matches()) {
+ throwExpirationParseException(componentSpecifier, fullSpecifier);
+ }
+ String numericString = matcher.group(1);
+ int numSeconds = parseExpirationInteger(numericString, componentSpecifier, fullSpecifier);
+ String unitString = matcher.group(2);
+ if (unitString.length() > 0) {
+ switch (unitString.charAt(0)) {
+ case 'd':
+ numSeconds *= 24 * 60 * 60;
+ break;
+ case 'h':
+ numSeconds *= 60 * 60;
+ break;
+ case 'm':
+ numSeconds *= 60;
+ break;
+ }
+ }
+ return numSeconds;
+ }
+
+ /**
+ * Parses a String from an expiration specifier as a non-negative integer. If successful returns
+ * the integer. Otherwise throws an {@link IllegalArgumentException} indicating that the specifier
+ * could not be parsed.
+ *
+ * @param intString String to parse
+ * @param componentSpecifier The component of the specifier being parsed
+ * @param fullSpecifier The full specifier
+ * @return The parsed integer
+ */
+ private static int parseExpirationInteger(
+ String intString, String componentSpecifier, String fullSpecifier) {
+ int seconds = 0;
+ try {
+ seconds = Integer.parseInt(intString);
+ } catch (NumberFormatException e) {
+ throwExpirationParseException(componentSpecifier, fullSpecifier);
+ }
+ if (seconds < 0) {
+ throwExpirationParseException(componentSpecifier, fullSpecifier);
+ }
+ return seconds;
+ }
+
+ /**
+ * Throws an {@link IllegalArgumentException} indicating that an expiration specifier String was
+ * not able to be parsed.
+ *
+ * @param componentSpecifier The component that could not be parsed
+ * @param fullSpecifier The full String
+ */
+ private static void throwExpirationParseException(
+ String componentSpecifier, String fullSpecifier) {
+ throw new IllegalArgumentException(
+ "Unable to parse cache expiration specifier '"
+ + fullSpecifier
+ + "' at component '"
+ + componentSpecifier
+ + "'");
+ }
+}
diff --git a/runtime/local_jetty121/src/main/resources/com/google/appengine/tools/development/jetty/webdefault.xml b/runtime/local_jetty121/src/main/resources/com/google/appengine/tools/development/jetty/webdefault.xml
new file mode 100644
index 000000000..617a84952
--- /dev/null
+++ b/runtime/local_jetty121/src/main/resources/com/google/appengine/tools/development/jetty/webdefault.xml
@@ -0,0 +1,961 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Default web.xml file.
+ This file is applied to a Web application before its own WEB_INF/web.xml file
+
+
+
+
+
+
+ _ah_DevAppServerRequestLogFilter
+
+ com.google.appengine.tools.development.DevAppServerRequestLogFilter
+
+
+
+
+
+
+ _ah_DevAppServerModulesFilter
+
+ com.google.appengine.tools.development.DevAppServerModulesFilter
+
+
+
+
+ _ah_StaticFileFilter
+
+ com.google.appengine.tools.development.jetty.StaticFileFilter
+
+
+
+
+
+
+
+
+
+ _ah_AbandonedTransactionDetector
+
+ com.google.apphosting.utils.servlet.TransactionCleanupFilter
+
+
+
+
+
+
+ _ah_ServeBlobFilter
+
+ com.google.appengine.api.blobstore.dev.ServeBlobFilter
+
+
+
+
+ _ah_HeaderVerificationFilter
+
+ com.google.appengine.tools.development.HeaderVerificationFilter
+
+
+
+
+ _ah_ResponseRewriterFilter
+
+ com.google.appengine.tools.development.jetty.JettyResponseRewriterFilter
+
+
+
+
+ _ah_DevAppServerRequestLogFilter
+ /*
+
+ FORWARD
+ REQUEST
+
+
+
+ _ah_DevAppServerModulesFilter
+ /*
+
+ FORWARD
+ REQUEST
+
+
+
+ _ah_StaticFileFilter
+ /*
+
+
+
+ _ah_AbandonedTransactionDetector
+ /*
+
+
+
+ _ah_ServeBlobFilter
+ /*
+ FORWARD
+ REQUEST
+
+
+
+ _ah_HeaderVerificationFilter
+ /*
+
+
+
+ _ah_ResponseRewriterFilter
+ /*
+
+
+
+
+
+ _ah_DevAppServerRequestLogFilter
+ _ah_DevAppServerModulesFilter
+ _ah_StaticFileFilter
+ _ah_AbandonedTransactionDetector
+ _ah_ServeBlobFilter
+ _ah_HeaderVerificationFilter
+ _ah_ResponseRewriterFilter
+
+
+
+ _ah_default
+ com.google.appengine.tools.development.jetty.LocalResourceFileServlet
+
+
+
+ _ah_blobUpload
+ com.google.appengine.api.blobstore.dev.UploadBlobServlet
+
+
+
+ _ah_blobImage
+ com.google.appengine.api.images.dev.LocalBlobImageServlet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ jsp
+ com.google.appengine.tools.development.jetty.FixupJspServlet
+
+ logVerbosityLevel
+ DEBUG
+
+
+ xpoweredBy
+ false
+
+ 0
+
+
+
+ jsp
+ *.jsp
+ *.jspf
+ *.jspx
+ *.xsp
+ *.JSP
+ *.JSPF
+ *.JSPX
+ *.XSP
+
+
+
+
+ _ah_login
+ com.google.appengine.api.users.dev.LocalLoginServlet
+
+
+ _ah_logout
+ com.google.appengine.api.users.dev.LocalLogoutServlet
+
+
+
+ _ah_oauthGetRequestToken
+ com.google.appengine.api.users.dev.LocalOAuthRequestTokenServlet
+
+
+ _ah_oauthAuthorizeToken
+ com.google.appengine.api.users.dev.LocalOAuthAuthorizeTokenServlet
+
+
+ _ah_oauthGetAccessToken
+ com.google.appengine.api.users.dev.LocalOAuthAccessTokenServlet
+
+
+
+ _ah_queue_deferred
+ com.google.apphosting.utils.servlet.DeferredTaskServlet
+
+
+
+ _ah_sessioncleanup
+ com.google.apphosting.utils.servlet.SessionCleanupServlet
+
+
+
+
+ _ah_capabilitiesViewer
+ com.google.apphosting.utils.servlet.CapabilitiesStatusServlet
+
+
+
+ _ah_datastoreViewer
+ com.google.apphosting.utils.servlet.DatastoreViewerServlet
+
+
+
+ _ah_modules
+ com.google.apphosting.utils.servlet.ModulesServlet
+
+
+
+ _ah_taskqueueViewer
+ com.google.apphosting.utils.servlet.TaskQueueViewerServlet
+
+
+
+ _ah_inboundMail
+ com.google.apphosting.utils.servlet.InboundMailServlet
+
+
+
+ _ah_search
+ com.google.apphosting.utils.servlet.SearchServlet
+
+
+
+ _ah_resources
+ com.google.apphosting.utils.servlet.AdminConsoleResourceServlet
+
+
+
+ _ah_adminConsole
+ org.apache.jsp.ah.jetty.adminConsole_jsp
+
+
+
+ _ah_datastoreViewerHead
+ org.apache.jsp.ah.jetty.datastoreViewerHead_jsp
+
+
+
+ _ah_datastoreViewerBody
+ org.apache.jsp.ah.jetty.datastoreViewerBody_jsp
+
+
+
+ _ah_datastoreViewerFinal
+ org.apache.jsp.ah.jetty.datastoreViewerFinal_jsp
+
+
+
+ _ah_searchIndexesListHead
+ org.apache.jsp.ah.jetty.searchIndexesListHead_jsp
+
+
+
+ _ah_searchIndexesListBody
+ org.apache.jsp.ah.jetty.searchIndexesListBody_jsp
+
+
+
+ _ah_searchIndexesListFinal
+ org.apache.jsp.ah.jetty.searchIndexesListFinal_jsp
+
+
+
+ _ah_searchIndexHead
+ org.apache.jsp.ah.jetty.searchIndexHead_jsp
+
+
+
+ _ah_searchIndexBody
+ org.apache.jsp.ah.jetty.searchIndexBody_jsp
+
+
+
+ _ah_searchIndexFinal
+ org.apache.jsp.ah.jetty.searchIndexFinal_jsp
+
+
+
+ _ah_searchDocumentHead
+ org.apache.jsp.ah.jetty.searchDocumentHead_jsp
+
+
+
+ _ah_searchDocumentBody
+ org.apache.jsp.ah.jetty.searchDocumentBody_jsp
+
+
+
+ _ah_searchDocumentFinal
+ org.apache.jsp.ah.jetty.searchDocumentFinal_jsp
+
+
+
+ _ah_capabilitiesStatusHead
+ org.apache.jsp.ah.jetty.capabilitiesStatusHead_jsp
+
+
+
+ _ah_capabilitiesStatusBody
+ org.apache.jsp.ah.jetty.capabilitiesStatusBody_jsp
+
+
+
+ _ah_capabilitiesStatusFinal
+ org.apache.jsp.ah.jetty.capabilitiesStatusFinal_jsp
+
+
+
+ _ah_entityDetailsHead
+ org.apache.jsp.ah.jetty.entityDetailsHead_jsp
+
+
+
+ _ah_entityDetailsBody
+ org.apache.jsp.ah.jetty.entityDetailsBody_jsp
+
+
+
+ _ah_entityDetailsFinal
+ org.apache.jsp.ah.jetty.entityDetailsFinal_jsp
+
+
+
+ _ah_indexDetailsHead
+ org.apache.jsp.ah.jetty.indexDetailsHead_jsp
+
+
+
+ _ah_indexDetailsBody
+ org.apache.jsp.ah.jetty.indexDetailsBody_jsp
+
+
+
+ _ah_indexDetailsFinal
+ org.apache.jsp.ah.jetty.indexDetailsFinal_jsp
+
+
+
+ _ah_modulesHead
+ org.apache.jsp.ah.jetty.modulesHead_jsp
+
+
+
+ _ah_modulesBody
+ org.apache.jsp.ah.jetty.modulesBody_jsp
+
+
+
+ _ah_modulesFinal
+ org.apache.jsp.ah.jetty.modulesFinal_jsp
+
+
+
+ _ah_taskqueueViewerHead
+ org.apache.jsp.ah.jetty.taskqueueViewerHead_jsp
+
+
+
+ _ah_taskqueueViewerBody
+ org.apache.jsp.ah.jetty.taskqueueViewerBody_jsp
+
+
+
+ _ah_taskqueueViewerFinal
+ org.apache.jsp.ah.jetty.taskqueueViewerFinal_jsp
+
+
+
+ _ah_inboundMailHead
+ org.apache.jsp.ah.jetty.inboundMailHead_jsp
+
+
+
+ _ah_inboundMailBody
+ org.apache.jsp.ah.jetty.inboundMailBody_jsp
+
+
+
+ _ah_inboundMailFinal
+ org.apache.jsp.ah.jetty.inboundMailFinal_jsp
+
+
+
+
+ _ah_sessioncleanup
+ /_ah/sessioncleanup
+
+
+
+ _ah_default
+ /
+
+
+
+
+ _ah_login
+ /_ah/login
+
+
+ _ah_logout
+ /_ah/logout
+
+
+
+ _ah_oauthGetRequestToken
+ /_ah/OAuthGetRequestToken
+
+
+ _ah_oauthAuthorizeToken
+ /_ah/OAuthAuthorizeToken
+
+
+ _ah_oauthGetAccessToken
+ /_ah/OAuthGetAccessToken
+
+
+
+
+
+
+
+ _ah_datastoreViewer
+ /_ah/admin
+
+
+
+
+ _ah_datastoreViewer
+ /_ah/admin/
+
+
+
+ _ah_datastoreViewer
+ /_ah/admin/datastore
+
+
+
+ _ah_capabilitiesViewer
+ /_ah/admin/capabilitiesstatus
+
+
+
+ _ah_modules
+ /_ah/admin/modules
+
+
+
+ _ah_taskqueueViewer
+ /_ah/admin/taskqueue
+
+
+
+ _ah_inboundMail
+ /_ah/admin/inboundmail
+
+
+
+ _ah_search
+ /_ah/admin/search
+
+
+
+
+
+
+ _ah_adminConsole
+ /_ah/adminConsole
+
+
+
+ _ah_resources
+ /_ah/resources
+
+
+
+ _ah_datastoreViewerHead
+ /_ah/datastoreViewerHead
+
+
+
+ _ah_datastoreViewerBody
+ /_ah/datastoreViewerBody
+
+
+
+ _ah_datastoreViewerFinal
+ /_ah/datastoreViewerFinal
+
+
+
+ _ah_searchIndexesListHead
+ /_ah/searchIndexesListHead
+
+
+
+ _ah_searchIndexesListBody
+ /_ah/searchIndexesListBody
+
+
+
+ _ah_searchIndexesListFinal
+ /_ah/searchIndexesListFinal
+
+
+
+ _ah_searchIndexHead
+ /_ah/searchIndexHead
+
+
+
+ _ah_searchIndexBody
+ /_ah/searchIndexBody
+
+
+
+ _ah_searchIndexFinal
+ /_ah/searchIndexFinal
+
+
+
+ _ah_searchDocumentHead
+ /_ah/searchDocumentHead
+
+
+
+ _ah_searchDocumentBody
+ /_ah/searchDocumentBody
+
+
+
+ _ah_searchDocumentFinal
+ /_ah/searchDocumentFinal
+
+
+
+ _ah_entityDetailsHead
+ /_ah/entityDetailsHead
+
+
+
+ _ah_entityDetailsBody
+ /_ah/entityDetailsBody
+
+
+
+ _ah_entityDetailsFinal
+ /_ah/entityDetailsFinal
+
+
+
+ _ah_indexDetailsHead
+ /_ah/indexDetailsHead
+
+
+
+ _ah_indexDetailsBody
+ /_ah/indexDetailsBody
+
+
+
+ _ah_indexDetailsFinal
+ /_ah/indexDetailsFinal
+
+
+
+ _ah_modulesHead
+ /_ah/modulesHead
+
+
+
+ _ah_modulesBody
+ /_ah/modulesBody
+
+
+
+ _ah_modulesFinal
+ /_ah/modulesFinal
+
+
+
+ _ah_taskqueueViewerHead
+ /_ah/taskqueueViewerHead
+
+
+
+ _ah_taskqueueViewerBody
+ /_ah/taskqueueViewerBody
+
+
+
+ _ah_taskqueueViewerFinal
+ /_ah/taskqueueViewerFinal
+
+
+
+ _ah_inboundMailHead
+ /_ah/inboundmailHead
+
+
+
+ _ah_inboundMailBody
+ /_ah/inboundmailBody
+
+
+
+ _ah_inboundMailFinal
+ /_ah/inboundmailFinal
+
+
+
+ _ah_blobUpload
+ /_ah/upload/*
+
+
+
+ _ah_blobImage
+ /_ah/img/*
+
+
+
+ _ah_queue_deferred
+ /_ah/queue/__deferred__
+
+
+
+ _ah_capabilitiesStatusHead
+ /_ah/capabilitiesstatusHead
+
+
+
+ _ah_capabilitiesStatusBody
+ /_ah/capabilitiesstatusBody
+
+
+
+ _ah_capabilitiesStatusFinal
+ /_ah/capabilitiesstatusFinal
+
+
+
+
+
+
+
+ Disable TRACE
+ /
+ TRACE
+
+
+
+
+
+ Enable everything but TRACE
+ /
+ TRACE
+
+
+
+
+ index.html
+ index.jsp
+
+
+
+
+
+
+
+ ar
+ ISO-8859-6
+
+
+ be
+ ISO-8859-5
+
+
+ bg
+ ISO-8859-5
+
+
+ ca
+ ISO-8859-1
+
+
+ cs
+ ISO-8859-2
+
+
+ da
+ ISO-8859-1
+
+
+ de
+ ISO-8859-1
+
+
+ el
+ ISO-8859-7
+
+
+ en
+ ISO-8859-1
+
+
+ es
+ ISO-8859-1
+
+
+ et
+ ISO-8859-1
+
+
+ fi
+ ISO-8859-1
+
+
+ fr
+ ISO-8859-1
+
+
+ hr
+ ISO-8859-2
+
+
+ hu
+ ISO-8859-2
+
+
+ is
+ ISO-8859-1
+
+
+ it
+ ISO-8859-1
+
+
+ iw
+ ISO-8859-8
+
+
+ ja
+ Shift_JIS
+
+
+ ko
+ EUC-KR
+
+
+ lt
+ ISO-8859-2
+
+
+ lv
+ ISO-8859-2
+
+
+ mk
+ ISO-8859-5
+
+
+ nl
+ ISO-8859-1
+
+
+ no
+ ISO-8859-1
+
+
+ pl
+ ISO-8859-2
+
+
+ pt
+ ISO-8859-1
+
+
+ ro
+ ISO-8859-2
+
+
+ ru
+ ISO-8859-5
+
+
+ sh
+ ISO-8859-5
+
+
+ sk
+ ISO-8859-2
+
+
+ sl
+ ISO-8859-2
+
+
+ sq
+ ISO-8859-2
+
+
+ sr
+ ISO-8859-5
+
+
+ sv
+ ISO-8859-1
+
+
+ tr
+ ISO-8859-9
+
+
+ uk
+ ISO-8859-5
+
+
+ zh
+ GB2312
+
+
+ zh_TW
+ Big5
+
+
+
diff --git a/runtime/local_jetty121_ee11/pom.xml b/runtime/local_jetty121_ee11/pom.xml
new file mode 100644
index 000000000..17b732a0a
--- /dev/null
+++ b/runtime/local_jetty121_ee11/pom.xml
@@ -0,0 +1,300 @@
+
+
+
+
+ 4.0.0
+
+ appengine-local-runtime-jetty121-ee11
+
+
+ com.google.appengine
+ runtime-parent
+ 3.0.0-SNAPSHOT
+
+
+ jar
+ AppEngine :: appengine-local-runtime Jetty 12.1 EE11
+ https://github.com/GoogleCloudPlatform/appengine-java-standard/
+ App Engine Local devappserver Jetty 12.1 EE11.
+
+
+
+ com.google.appengine
+ appengine-api-stubs
+
+
+ com.google.appengine
+ appengine-remote-api
+
+
+ com.google.appengine
+ appengine-tools-sdk
+
+
+ com.google.appengine
+ sessiondata
+
+
+
+ com.google.auto.value
+ auto-value
+
+
+ com.google.appengine
+ shared-sdk
+
+
+ com.google.appengine
+ appengine-utils
+
+
+ com.google.flogger
+ flogger-system-backend
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+ com.google.appengine
+ proto1
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-webapp
+ ${jetty121.version}
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty121.version}
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-annotations
+ ${jetty121.version}
+
+
+ org.mortbay.jasper
+ apache-jsp
+ 10.1.7
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-apache-jsp
+ ${jetty121.version}
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+
+
+ org.eclipse.jetty
+ jetty-http
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-io
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-jndi
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-security
+ ${jetty121.version}
+
+
+ org.eclipse.jetty.toolchain
+ jetty-servlet-api
+ 4.0.6
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty121.version}
+
+
+ org.eclipse.jetty
+ jetty-xml
+ ${jetty121.version}
+
+
+
+ com.google.appengine
+ shared-sdk-jetty121
+ ${project.version}
+
+
+ org.eclipse.jetty.ee8
+ jetty-ee8-security
+
+
+ org.eclipse.jetty.ee8
+ jetty-ee8-servlet
+
+
+
+
+
+
+
+
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ com.google.common
+ com.google.appengine.repackaged.com.google.common
+
+
+ com.google.io
+ com.google.appengine.repackaged.com.google.io
+
+
+ com.google.protobuf
+ com.google.appengine.repackaged.com.google.protobuf
+
+
+ com.google.gaia.mint.proto2api
+ com.google.appengine.repackaged.com.google.gaia.mint.proto2api
+
+
+ com.esotericsoftware.yamlbeans
+ com.google.appengine.repackaged.com.esotericsoftware.yamlbeans
+
+
+ com.google.borg.borgcron
+ com.google.appengine.repackaged.com.google.cron
+
+
+
+
+ com.google.appengine:appengine-apis-dev:*
+
+ com/google/appengine/tools/development/**
+
+
+ com/google/appengine/tools/development/testing/**
+
+
+
+ com.google.appengine:appengine-apis:*
+
+ com/google/apphosting/utils/security/urlfetch/**
+
+
+
+ com.google.appengine:appengine-utils
+
+ com/google/apphosting/utils/config/**
+ com/google/apphosting/utils/io/**
+ com/google/apphosting/utils/security/urlfetch/**
+ com/google/borg/borgcron/**
+
+
+
+ com.google.appengine:proto1:*
+
+ com/google/common/flags/*
+ com/google/common/flags/ext/*
+ com/google/io/protocol/**
+ com/google/protobuf/**
+
+
+ com/google/io/protocol/proto2/*
+
+
+
+ com.google.appengine:shared-sdk-jetty12:*
+
+ com/google/apphosting/runtime/**
+ com/google/appengine/tools/development/**
+
+
+
+ com.google.guava:guava
+
+ com/google/common/base/**
+ com/google/common/cache/**
+ com/google/common/collect/**
+ com/google/common/escape/**
+ com/google/common/flags/**
+ com/google/common/flogger/**
+ com/google/common/graph/**
+ com/google/common/hash/**
+ com/google/common/html/**
+ com/google/common/io/**
+ com/google/common/math/**
+ com/google/common/net/HostAndPort.class
+ com/google/common/net/InetAddresses.class
+ com/google/common/primitives/**
+ com/google/common/time/**
+ com/google/common/util/concurrent/**
+ com/google/common/xml/**
+
+
+
+ com.contrastsecurity:yamlbeans
+
+
+ com/esotericsoftware/yamlbeans/**
+
+
+
+ com.google.appengine:sessiondata
+
+ com/**
+
+
+
+
+
+ com.google.appengine:appengine-tools-sdk
+ com.google.appengine:appengine-utils
+ com.google.appengine:sessiondata
+ com.google.appengine:shared-sdk
+ com.google.appengine:shared-sdk-jetty121
+ com.google.appengine:appengine-local-runtime-jetty121-ee11
+ com.google.flogger:google-extensions
+ com.google.flogger:flogger-system-backend
+ com.google.flogger:flogger
+
+
+
+
+
+
+
+
+
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/AppEngineAnnotationConfiguration.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/AppEngineAnnotationConfiguration.java
new file mode 100644
index 000000000..e921fce5c
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/AppEngineAnnotationConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.development.jetty.ee11;
+
+import jakarta.servlet.ServletContainerInitializer;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jetty.ee11.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.ee11.apache.jsp.JettyJasperInitializer;
+
+/**
+ * Customization of AnnotationConfiguration which correctly configures the JSP Jasper initializer.
+ * For more context, see b/37513903
+ */
+public class AppEngineAnnotationConfiguration extends AnnotationConfiguration {
+ @Override
+ protected List getNonExcludedInitializers(State state) {
+
+ List initializers = super.getNonExcludedInitializers(state);
+ for (ServletContainerInitializer sci : initializers) {
+ if (sci instanceof JettyJasperInitializer) {
+ // Jasper is already there, no need to add it.
+ return initializers;
+ }
+ }
+
+ initializers = new ArrayList<>(initializers);
+ initializers.add(new JettyJasperInitializer());
+ return initializers;
+ }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/AppEngineWebAppContext.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/AppEngineWebAppContext.java
new file mode 100644
index 000000000..8e84f0709
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/AppEngineWebAppContext.java
@@ -0,0 +1,170 @@
+/*
+ * 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.development.jetty.ee11;
+
+import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.runtime.jetty.EE11AppEngineAuthentication;
+import java.io.File;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.security.Constraint;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * {@code AppEngineWebAppContext} is a customization of Jetty's {@link WebAppContext} that is aware
+ * of the {@link ApiProxy} and can provide custom logging and authentication.
+ */
+public class AppEngineWebAppContext extends WebAppContext {
+
+ // TODO: This should be some sort of Prometheus-wide
+ // constant. If it's much larger than this we may need to
+ // restructure the code a bit.
+ private static final int MAX_RESPONSE_SIZE = 32 * 1024 * 1024;
+
+ private final String serverInfo;
+
+ public AppEngineWebAppContext(File appDir, String serverInfo) {
+ // We set the contextPath to / for all applications.
+ super(appDir.getPath(), "/");
+ Resource webApp = null;
+ try {
+ webApp = ResourceFactory.root().newResource(appDir.getAbsolutePath());
+
+ if (appDir.isDirectory()) {
+ setWar(appDir.getPath());
+ setBaseResource(webApp);
+ } else {
+ // Real war file, not exploded , so we explode it in tmp area.
+ File extractedWebAppDir = createTempDir();
+ extractedWebAppDir.mkdir();
+ extractedWebAppDir.deleteOnExit();
+ Resource jarWebWpp = ResourceFactory.root().newJarFileResource(webApp.getURI());
+ jarWebWpp.copyTo(extractedWebAppDir.toPath());
+ setBaseResource(ResourceFactory.root().newResource(extractedWebAppDir.getAbsolutePath()));
+ setWar(extractedWebAppDir.getPath());
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("cannot create AppEngineWebAppContext:", e);
+ }
+
+ this.serverInfo = serverInfo;
+
+ // Configure the Jetty SecurityHandler to understand our method of
+ // authentication (via the UserService).
+ setSecurityHandler(EE11AppEngineAuthentication.newSecurityHandler());
+
+ setMaxFormContentSize(MAX_RESPONSE_SIZE);
+ }
+
+ @Override
+ public ServletScopedContext getContext() {
+ // TODO: Override the default HttpServletContext implementation (for logging)?.
+ AppEngineServletContext appEngineServletContext = new AppEngineServletContext();
+ return super.getContext();
+ }
+
+ private static File createTempDir() {
+ File baseDir = new File(System.getProperty("java.io.tmpdir"));
+ String baseName = System.currentTimeMillis() + "-";
+
+ for (int counter = 0; counter < 10; counter++) {
+ File tempDir = new File(baseDir, baseName + counter);
+ if (tempDir.mkdir()) {
+ return tempDir;
+ }
+ }
+ throw new IllegalStateException("Failed to create directory ");
+ }
+
+ @Override
+ public Class extends SecurityHandler> getDefaultSecurityHandlerClass() {
+ return AppEngineConstraintSecurityHandler.class;
+ }
+
+ /**
+ * Override to make sure all RoleInfos do not have security constraints to avoid a Jetty failure
+ * when not running with https.
+ */
+ public static class AppEngineConstraintSecurityHandler extends ConstraintSecurityHandler {
+ @Override
+ protected Constraint getConstraint(String pathInContext, Request request) {
+ Constraint constraint = super.getConstraint(pathInContext, request);
+
+ // Remove constraints so that we can emulate HTTPS locally.
+ constraint =
+ Constraint.from(
+ constraint.getName(),
+ Constraint.Transport.ANY,
+ constraint.getAuthorization(),
+ constraint.getRoles());
+ return constraint;
+ }
+ }
+
+ // N.B.: Yuck. Jetty hardcodes all of this logic into an
+ // inner class of ContextHandler. We need to subclass WebAppContext
+ // (which extends ContextHandler) and then subclass the SContext
+ // inner class to modify its behavior.
+
+ /** Context extension that allows logs to be written to the App Engine log APIs. */
+ public class AppEngineServletContext extends ServletScopedContext {
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return AppEngineWebAppContext.this.getClassLoader();
+ }
+
+ /*
+ TODO fix logging.
+ @Override
+ public void log(String message) {
+ log(message, null);
+ }
+ */
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param throwable an exception associated with this log message, or {@code null}.
+ */
+ /*
+ @Override
+ public void log(String message, Throwable throwable) {
+ StringWriter writer = new StringWriter();
+ writer.append("javax.servlet.ServletContext log: ");
+ writer.append(message);
+
+ if (throwable != null) {
+ writer.append("\n");
+ throwable.printStackTrace(new PrintWriter(writer));
+ }
+
+ LogRecord.Level logLevel = throwable == null ? LogRecord.Level.info : LogRecord.Level.error;
+ ApiProxy.log(
+ new ApiProxy.LogRecord(logLevel, System.currentTimeMillis() * 1000L, writer.toString()));
+ }
+
+ @Override
+ public void log(Exception exception, String msg) {
+ log(msg, exception);
+ }
+ */
+ }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/DevAppEngineWebAppContext.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/DevAppEngineWebAppContext.java
new file mode 100644
index 000000000..8511e7948
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/DevAppEngineWebAppContext.java
@@ -0,0 +1,205 @@
+/*
+ * 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.development.jetty.ee11;
+
+import com.google.appengine.tools.development.DevAppServer;
+import com.google.appengine.tools.info.AppengineSdk;
+import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.utils.io.IoUtil;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintMapping;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.security.Constraint;
+import org.eclipse.jetty.server.Context;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.resource.Resource;
+
+/** An AppEngineWebAppContext for the DevAppServer. */
+public class DevAppEngineWebAppContext extends AppEngineWebAppContext {
+
+ private static final Logger logger = Logger.getLogger(DevAppEngineWebAppContext.class.getName());
+
+ // Copied from org.apache.jasper.Constants.SERVLET_CLASSPATH
+ // to remove compile-time dependency on Jasper
+ private static final String JASPER_SERVLET_CLASSPATH = "org.apache.catalina.jsp_classpath";
+
+ // Header that allows arbitrary requests to bypass jetty's security
+ // mechanisms. Useful for things like the dev task queue, which needs
+ // to hit secure urls without an authenticated user.
+ private static final String X_GOOGLE_DEV_APPSERVER_SKIPADMINCHECK =
+ "X-Google-DevAppserver-SkipAdminCheck";
+
+ // Keep in sync with com.google.apphosting.utils.jetty.AppEngineAuthentication.
+ private static final String SKIP_ADMIN_CHECK_ATTR =
+ "com.google.apphosting.internal.SkipAdminCheck";
+
+ private final Object transportGuaranteeLock = new Object();
+ private boolean transportGuaranteesDisabled = false;
+
+ public DevAppEngineWebAppContext(
+ File appDir,
+ File externalResourceDir,
+ String serverInfo,
+ ApiProxy.Delegate> apiProxyDelegate,
+ DevAppServer devAppServer) {
+ super(appDir, serverInfo);
+
+ // Set up the classpath required to compile JSPs. This is specific to Jasper.
+ setAttribute(JASPER_SERVLET_CLASSPATH, buildClasspath());
+ setAttribute(
+ "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
+ ".*/jakarta.servlet-api-[^/]*\\.jar$|.*jakarta.servlet.jsp.jstl-.*\\.jar$");
+
+ // Make ApiProxyLocal available via the servlet context. This allows
+ // servlets that are part of the dev appserver (like those that render the
+ // dev console for example) to get access to this resource even in the
+ // presence of libraries that install their own custom Delegates (like
+ // Remote api and Appstats for example).
+ getServletContext()
+ .setAttribute("com.google.appengine.devappserver.ApiProxyLocal", apiProxyDelegate);
+
+ // Make the dev appserver available via the servlet context as well.
+ getServletContext().setAttribute("com.google.appengine.devappserver.Server", devAppServer);
+ }
+
+ /**
+ * By default, the context is created with alias checkers for symlinks: {@link
+ * org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker}.
+ *
+ *
Note: this is a dangerous configuration and should not be used in production.
+ */
+ @Override
+ public boolean checkAlias(String path, Resource resource) {
+ return true;
+ }
+
+ @Override
+ protected ClassLoader configureClassLoader(ClassLoader loader) {
+ // Avoid wrapping the provided classloader with WebAppClassLoader.
+ return loader;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+ disableTransportGuarantee();
+ }
+
+ @Override
+ protected ClassLoader enterScope(Request contextRequest) {
+ if ((contextRequest != null) && (hasSkipAdminCheck(contextRequest))) {
+ contextRequest.setAttribute(SKIP_ADMIN_CHECK_ATTR, Boolean.TRUE);
+ }
+
+ // TODO An extremely heinous way of helping the DevAppServer's
+ // SecurityManager determine if a DevAppServer request thread is executing.
+ // Find something better.
+ // See DevAppServerFactory.CustomSecurityManager.
+
+ // ludo remove entirely
+ System.setProperty("devappserver-thread-" + Thread.currentThread().getName(), "true");
+ return super.enterScope(contextRequest);
+ }
+
+ @Override
+ protected void exitScope(Request request, Context lastContext, ClassLoader lastLoader) {
+ super.exitScope(request, lastContext, lastLoader);
+ System.clearProperty("devappserver-thread-" + Thread.currentThread().getName());
+ }
+
+ /**
+ * Returns true if the X-Google-Internal-SkipAdminCheck header is present. There is nothing
+ * preventing usercode from setting this header and circumventing dev appserver security, but the
+ * dev appserver was not designed to be secure.
+ */
+ private boolean hasSkipAdminCheck(Request request) {
+ for (HttpField field : request.getHeaders()) {
+ // We don't care about the header value, its presence is sufficient.
+ if (field.getName().equalsIgnoreCase(X_GOOGLE_DEV_APPSERVER_SKIPADMINCHECK)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Builds a classpath up for the webapp for JSP compilation. */
+ private String buildClasspath() {
+ StringBuilder classpath = new StringBuilder();
+
+ // Shared servlet container classes
+ for (File f : AppengineSdk.getSdk().getSharedLibFiles()) {
+ classpath.append(f.getAbsolutePath());
+ classpath.append(File.pathSeparatorChar);
+ }
+
+ String webAppPath = getWar();
+
+ // webapp classes
+ classpath.append(webAppPath + File.separator + "classes" + File.pathSeparatorChar);
+
+ List files = IoUtil.getFilesAndDirectories(new File(webAppPath, "lib"));
+ for (File f : files) {
+ if (f.isFile() && f.getName().endsWith(".jar")) {
+ classpath.append(f.getAbsolutePath());
+ classpath.append(File.pathSeparatorChar);
+ }
+ }
+
+ return classpath.toString();
+ }
+
+ /**
+ * The first time this method is called it will walk through the constraint mappings on the
+ * current SecurityHandler and disable any transport guarantees that have been set. This is
+ * required to disable SSL requirements in the DevAppServer because it does not support SSL.
+ */
+ private void disableTransportGuarantee() {
+ synchronized (transportGuaranteeLock) {
+ ConstraintSecurityHandler securityHandler = (ConstraintSecurityHandler) getSecurityHandler();
+ if (!transportGuaranteesDisabled && securityHandler != null) {
+ List mappings = new ArrayList<>();
+ for (ConstraintMapping mapping : securityHandler.getConstraintMappings()) {
+ Constraint constraint = mapping.getConstraint();
+ if (constraint.getTransport() == Constraint.Transport.SECURE) {
+ logger.info(
+ "Ignoring for "
+ + mapping.getPathSpec()
+ + " as the SDK does not support HTTPS. It will still be used"
+ + " when you upload your application.");
+ }
+
+ mapping.setConstraint(
+ Constraint.from(
+ constraint.getName(),
+ Constraint.Transport.ANY,
+ constraint.getAuthorization(),
+ constraint.getRoles()));
+ mappings.add(mapping);
+ }
+
+ Set knownRoles = Set.copyOf(securityHandler.getKnownRoles());
+ securityHandler.setConstraintMappings(mappings, knownRoles);
+ }
+ transportGuaranteesDisabled = true;
+ }
+ }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/FixupJspServlet.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/FixupJspServlet.java
new file mode 100644
index 000000000..4a6cfebd1
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/FixupJspServlet.java
@@ -0,0 +1,128 @@
+/*
+ * 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.development.jetty.ee11;
+
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import java.lang.reflect.InvocationTargetException;
+import org.apache.tomcat.InstanceManager;
+import org.eclipse.jetty.ee11.jsp.JettyJspServlet;
+
+/** {@code FixupJspServlet} adds some logic to work around bugs in the Jasper {@link JspServlet}. */
+public class FixupJspServlet extends JettyJspServlet {
+
+ /**
+ * The request attribute that contains the name of the JSP file, when the request path doesn't
+ * refer directly to the JSP file (for example, it's instead a servlet mapping).
+ */
+ // private static final String JASPER_JSP_FILE = "org.apache.catalina.jsp_file";
+ // private static final String WEB31XML =
+ // ""
+ // + ""
+ // + "";
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ config
+ .getServletContext()
+ .setAttribute(InstanceManager.class.getName(), new InstanceManagerImpl());
+ // config
+ // .getServletContext()
+ // .setAttribute("org.apache.tomcat.util.scan.MergedWebXml", WEB31XML);
+ super.init(config);
+ }
+
+ // @Override
+ // public void service(HttpServletRequest request, HttpServletResponse response)
+ // throws ServletException, IOException {
+ // fixupJspFileAttribute(request);
+ // super.service(request, response);
+ // }
+
+ private static class InstanceManagerImpl implements InstanceManager {
+ @Override
+ public Object newInstance(String className)
+ throws IllegalAccessException,
+ InvocationTargetException,
+ InstantiationException,
+ ClassNotFoundException {
+ return newInstance(className, this.getClass().getClassLoader());
+ }
+
+ @Override
+ public Object newInstance(String fqcn, ClassLoader classLoader)
+ throws IllegalAccessException,
+ InvocationTargetException,
+ InstantiationException,
+ ClassNotFoundException {
+ Class> cl = classLoader.loadClass(fqcn);
+ return newInstance(cl);
+ }
+
+ @Override
+ @SuppressWarnings("ClassNewInstance")
+ // We would prefer clazz.getConstructor().newInstance() here, but that throws
+ // NoSuchMethodException. It would also lead to a change in behaviour, since an exception
+ // thrown by the constructor would be wrapped in InvocationTargetException rather than being
+ // propagated from newInstance(). Although that's funky, and the reason for preferring
+ // getConstructor().newInstance(), we don't know if something is relying on the current
+ // behaviour.
+ public Object newInstance(Class> clazz)
+ throws IllegalAccessException, InvocationTargetException, InstantiationException {
+ return clazz.newInstance();
+ }
+
+ @Override
+ public void newInstance(Object o) {}
+
+ @Override
+ public void destroyInstance(Object o)
+ throws IllegalAccessException, InvocationTargetException {}
+ }
+
+ // NB This method is here, because there appears to be
+ // a bug in either Jetty or Jasper where entries in web.xml
+ // don't get handled correctly. This interaction between Jetty and Jasper
+ // appears to have always been broken, irrespective of App Engine
+ // integration.
+ //
+ // Jetty hands the name of the JSP file to Jasper (via a request attribute)
+ // without a leading slash. This seems to cause all sorts of problems.
+ // - Jasper turns around and asks Jetty to lookup that same file
+ // (using ServletContext.getResourceAsStream). Jetty rejects, out-of-hand,
+ // any resource requests that don't start with a leading slash.
+ // - Jasper seems to plain blow up on jsp paths that don't have a leading
+ // slash.
+ //
+ // If we enforce a leading slash, Jetty and Jasper seem to co-operate
+ // correctly.
+ // private void fixupJspFileAttribute(HttpServletRequest request) {
+ // String jspFile = (String) request.getAttribute(JASPER_JSP_FILE);
+ //
+ // if (jspFile != null) {
+ // if (jspFile.length() == 0) {
+ // jspFile = "/";
+ // } else if (jspFile.charAt(0) != '/') {
+ // jspFile = "/" + jspFile;
+ // }
+ // request.setAttribute(JASPER_JSP_FILE, jspFile);
+ // }
+ // }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java
new file mode 100644
index 000000000..9a3d4e083
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java
@@ -0,0 +1,745 @@
+/*
+ * 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.development.jetty.ee11;
+
+import static com.google.appengine.tools.development.LocalEnvironment.DEFAULT_VERSION_HOSTNAME;
+
+import com.google.appengine.api.log.dev.DevLogHandler;
+import com.google.appengine.api.log.dev.LocalLogService;
+import com.google.appengine.tools.development.AbstractContainerService;
+import com.google.appengine.tools.development.ApiProxyLocal;
+import com.google.appengine.tools.development.AppContext;
+import com.google.appengine.tools.development.ContainerService;
+import com.google.appengine.tools.development.DevAppServer;
+import com.google.appengine.tools.development.DevAppServerModulesFilter;
+import com.google.appengine.tools.development.IsolatedAppClassLoader;
+import com.google.appengine.tools.development.LocalEnvironment;
+import com.google.appengine.tools.development.jakarta.LocalHttpRequestEnvironment;
+import com.google.appengine.tools.info.AppengineSdk;
+import com.google.apphosting.api.ApiProxy;
+import com.google.apphosting.runtime.jetty.EE11SessionManagerHandler;
+import com.google.apphosting.utils.config.AppEngineConfigException;
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.apphosting.utils.config.WebModule;
+import com.google.common.base.Predicates;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Files;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.security.Permissions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import org.eclipse.jetty.ee11.servlet.ServletApiRequest;
+import org.eclipse.jetty.ee11.servlet.ServletContextRequest;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.webapp.Configuration;
+import org.eclipse.jetty.ee11.webapp.JettyWebXmlConfiguration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.server.Context;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.HttpStream;
+import org.eclipse.jetty.server.NetworkTrafficServerConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Fields;
+import org.eclipse.jetty.util.Scanner;
+import org.eclipse.jetty.util.VirtualThreads;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+
+/** Implements a Jetty backed {@link ContainerService}. */
+public class JettyContainerService extends AbstractContainerService
+ implements com.google.appengine.tools.development.jakarta.ContainerService {
+
+ private static final Logger log = Logger.getLogger(JettyContainerService.class.getName());
+
+ private static final String JETTY_TAG_LIB_JAR_PREFIX = "org.apache.taglibs.taglibs-";
+ private static final Pattern JSP_REGEX = Pattern.compile(".*\\.jspx?");
+
+ public static final String WEB_DEFAULTS_XML =
+ "com/google/appengine/tools/development/jetty/ee11/webdefault.xml";
+
+ // This should match the value of the --clone_max_outstanding_api_rpcs flag.
+ private static final int MAX_SIMULTANEOUS_API_CALLS = 100;
+
+ // The soft deadline for requests. It is defined here, as the normal way to
+ // get this deadline is through JavaRuntimeFactory, which is part of the
+ // runtime and not really part of the devappserver.
+ private static final Long SOFT_DEADLINE_DELAY_MS = 60000L;
+
+ /**
+ * Specify which {@link Configuration} objects should be invoked when configuring a web
+ * application.
+ *
+ *
This is a subset of: org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
+ *
+ *
Specifically, we've removed {@link JettyWebXmlConfiguration} which allows users to use
+ * {@code jetty-web.xml} files.
+ */
+ private static final String[] CONFIG_CLASSES =
+ new String[] {
+ org.eclipse.jetty.ee11.webapp.WebInfConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.ee11.webapp.WebXmlConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.ee11.webapp.MetaInfConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.ee11.webapp.FragmentConfiguration.class.getCanonicalName(),
+ // Special annotationConfiguration to deal with Jasper ServletContainerInitializer.
+ AppEngineAnnotationConfiguration.class.getCanonicalName()
+ };
+
+ private static final String WEB_XML_ATTR = "com.google.appengine.tools.development.webXml";
+ private static final String APPENGINE_WEB_XML_ATTR =
+ "com.google.appengine.tools.development.appEngineWebXml";
+
+ private static final int SCAN_INTERVAL_SECONDS = 5;
+
+ /** Jetty webapp context. */
+ private WebAppContext context;
+
+ /** Our webapp context. */
+ private AppContext appContext;
+
+ /** The Jetty server. */
+ private Server server;
+
+ /** Hot deployment support. */
+ private Scanner scanner;
+
+ /** Collection of current LocalEnvironments */
+ private final Set environments = ConcurrentHashMap.newKeySet();
+
+ private class JettyAppContext implements AppContext {
+ @Override
+ public ClassLoader getClassLoader() {
+ return context.getClassLoader();
+ }
+
+ @Override
+ public Permissions getUserPermissions() {
+ return JettyContainerService.this.getUserPermissions();
+ }
+
+ @Override
+ public Permissions getApplicationPermissions() {
+ // Should not be called in Java8/Jetty9.
+ throw new RuntimeException("No permissions needed for this runtime.");
+ }
+
+ @Override
+ public Object getContainerContext() {
+ return context;
+ }
+ }
+
+ public JettyContainerService() {}
+
+ @Override
+ protected File initContext() throws IOException {
+ // Register our own slight modification of Jetty's WebAppContext,
+ // which maintains ApiProxy's environment ThreadLocal.
+ this.context =
+ new DevAppEngineWebAppContext(
+ appDir, externalResourceDir, devAppServerVersion, apiProxyDelegate, devAppServer);
+
+ context.addEventListener(
+ new ContextHandler.ContextScopeListener() {
+ @Override
+ public void enterScope(Context context, Request request) {
+ JettyContainerService.this.enterScope(request);
+ }
+
+ @Override
+ public void exitScope(Context context, Request request) {
+ JettyContainerService.this.exitScope(null);
+ }
+ });
+
+ this.appContext = new JettyAppContext();
+
+ // Set the location of deployment descriptor. This value might be null,
+ // which is fine, it just means Jetty will look for it in the default
+ // location (WEB-INF/web.xml).
+ context.setDescriptor(webXmlLocation == null ? null : webXmlLocation.getAbsolutePath());
+
+ // Override the web.xml that Jetty automatically prepends to other
+ // web.xml files. This is where the DefaultServlet is registered,
+ // which serves static files. We override it to disable some
+ // other magic (e.g. JSP compilation), and to turn off some static
+ // file functionality that Prometheus won't support
+ // (e.g. directory listings) and turn on others (e.g. symlinks).
+ String webDefaultXml =
+ devAppServer
+ .getServiceProperties()
+ .getOrDefault("appengine.webdefault.xml", WEB_DEFAULTS_XML);
+ context.setDefaultsDescriptor(webDefaultXml);
+
+ // Disable support for jetty-web.xml.
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(WebAppContext.class.getClassLoader());
+ context.setConfigurationClasses(CONFIG_CLASSES);
+ } finally {
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+ // Create the webapp ClassLoader.
+ // We need to load appengine-web.xml to initialize the class loader.
+ File appRoot = determineAppRoot();
+ installLocalInitializationEnvironment();
+
+ // Create the webapp ClassLoader.
+ // ADD TLDs that must be under WEB-INF for Jetty9.
+ // We make it non fatal, and emit a warning when it fails, as the user can add this dependency
+ // in the application itself.
+ if (applicationContainsJSP(appDir, JSP_REGEX)) {
+ for (File file : AppengineSdk.getSdk().getUserJspLibFiles()) {
+ if (file.getName().startsWith(JETTY_TAG_LIB_JAR_PREFIX)) {
+ // Jetty provided tag lib jars are currently
+ // org.apache.taglibs.taglibs-standard-spec-1.2.5.jar and
+ // org.apache.taglibs.taglibs-standard-impl-1.2.5.jar.
+ // For jars provided by a Maven or Gradle builder, the prefix org.apache.taglibs.taglibs-
+ // is not present, so the jar names are:
+ // standard-spec-1.2.5.jar and
+ // standard-impl-1.2.5.jar.
+ // We check if these jars are provided by the web app, or we copy them from Jetty distro.
+ File jettyProvidedDestination = new File(appDir + "/WEB-INF/lib/" + file.getName());
+ if (!jettyProvidedDestination.exists()) {
+ File mavenProvidedDestination =
+ new File(
+ appDir
+ + "/WEB-INF/lib/"
+ + file.getName().substring(JETTY_TAG_LIB_JAR_PREFIX.length()));
+ if (!mavenProvidedDestination.exists()) {
+ log.log(
+ Level.WARNING,
+ "Adding jar "
+ + file.getName()
+ + " to WEB-INF/lib."
+ + " You might want to add a dependency in your project build system to avoid"
+ + " this warning.");
+ try {
+ Files.copy(file, jettyProvidedDestination);
+ } catch (IOException e) {
+ log.log(
+ Level.WARNING,
+ "Cannot copy org.apache.taglibs.taglibs jar file to WEB-INF/lib.",
+ e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ URL[] classPath = getClassPathForApp(appRoot);
+
+ IsolatedAppClassLoader isolatedClassLoader =
+ new IsolatedAppClassLoader(
+ appRoot, externalResourceDir, classPath, JettyContainerService.class.getClassLoader());
+ context.setClassLoader(isolatedClassLoader);
+ if (Boolean.parseBoolean(System.getProperty("appengine.allowRemoteShutdown"))) {
+ context.addServlet(new ServletHolder(new ServerShutdownServlet()), "/_ah/admin/quit");
+ }
+
+ return appRoot;
+ }
+
+ private ApiProxy.Environment enterScope(Request request) {
+ ApiProxy.Environment oldEnv = ApiProxy.getCurrentEnvironment();
+
+ // We should have a request that use its associated environment, if there is no request
+ // we cannot select a local environment as picking the wrong one could result in
+ // waiting on the LocalEnvironment API call semaphore forever.
+ LocalEnvironment env =
+ request == null
+ ? null
+ : (LocalEnvironment) request.getAttribute(LocalEnvironment.class.getName());
+ if (env != null) {
+ ApiProxy.setEnvironmentForCurrentThread(env);
+ DevAppServerModulesFilter.injectBackendServiceCurrentApiInfo(
+ backendName, backendInstance, portMappingProvider.getPortMapping());
+ }
+
+ return oldEnv;
+ }
+
+ private void exitScope(ApiProxy.Environment environment) {
+ ApiProxy.setEnvironmentForCurrentThread(environment);
+ }
+
+ /** Check if the application contains a JSP file. */
+ private static boolean applicationContainsJSP(File dir, Pattern jspPattern) {
+ for (File file :
+ FluentIterable.from(Files.fileTraverser().depthFirstPreOrder(dir))
+ .filter(Predicates.not(Files.isDirectory()))) {
+ if (jspPattern.matcher(file.getName()).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static class ServerShutdownServlet extends HttpServlet {
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.getWriter().println("Shutting down local server.");
+ resp.flushBuffer();
+ DevAppServer server =
+ (DevAppServer)
+ getServletContext().getAttribute("com.google.appengine.devappserver.Server");
+ // don't shut down until outstanding requests (like this one) have finished
+ server.gracefulShutdown();
+ }
+ }
+
+ @Override
+ protected void connectContainer() throws Exception {
+ moduleConfigurationHandle.checkEnvironmentVariables();
+
+ // Jetty uses the thread context ClassLoader to find things
+ // This needs to be null for the DevAppClassLoader to
+ // work correctly. There have been clients that set this to
+ // something else.
+ Thread currentThread = Thread.currentThread();
+ ClassLoader previousCcl = currentThread.getContextClassLoader();
+
+ HttpConfiguration configuration = new HttpConfiguration();
+ configuration.setSendDateHeader(false);
+ configuration.setSendServerVersion(false);
+ configuration.setSendXPoweredBy(false);
+ // Try to enable virtual threads if requested on java21:
+ if (Boolean.getBoolean("appengine.use.virtualthreads")) {
+ QueuedThreadPool threadPool = new QueuedThreadPool();
+ threadPool.setVirtualThreadsExecutor(VirtualThreads.getDefaultVirtualThreadsExecutor());
+ server = new Server(threadPool);
+ } else {
+ server = new Server();
+ }
+ try {
+ NetworkTrafficServerConnector connector =
+ new NetworkTrafficServerConnector(
+ server,
+ null,
+ null,
+ null,
+ 0,
+ Runtime.getRuntime().availableProcessors(),
+ new HttpConnectionFactory(configuration));
+ connector.setHost(address);
+ connector.setPort(port);
+ // Linux keeps the port blocked after shutdown if we don't disable this.
+ // TODO: WHAT IS THIS connector.setSoLingerTime(0);
+ connector.open();
+
+ server.addConnector(connector);
+
+ port = connector.getLocalPort();
+ } finally {
+ currentThread.setContextClassLoader(previousCcl);
+ }
+ }
+
+ @Override
+ protected void startContainer() throws Exception {
+ context.setAttribute(WEB_XML_ATTR, webXml);
+ context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
+
+ // Jetty uses the thread context ClassLoader to find things
+ // This needs to be null for the DevAppClassLoader to
+ // work correctly. There have been clients that set this to
+ // something else.
+ Thread currentThread = Thread.currentThread();
+ ClassLoader previousCcl = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(null);
+
+ try {
+ // Wrap context in a handler that manages the ApiProxy ThreadLocal.
+ ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
+ context.insertHandler(apiHandler);
+ server.setHandler(context);
+ EE11SessionManagerHandler unused =
+ EE11SessionManagerHandler.create(
+ EE11SessionManagerHandler.Config.builder()
+ .setEnableSession(isSessionsEnabled())
+ .setServletContextHandler(context)
+ .build());
+
+ server.start();
+ } finally {
+ currentThread.setContextClassLoader(previousCcl);
+ }
+ }
+
+ @Override
+ protected void stopContainer() throws Exception {
+ server.stop();
+ }
+
+ /**
+ * If the property "appengine.fullscan.seconds" is set to a positive integer, the web app content
+ * (deployment descriptors, classes/ and lib/) is scanned for changes that will trigger the
+ * reloading of the application. If the property is not set (default), we monitor the webapp war
+ * file or the appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp
+ * whenever an update is detected, i.e. a newer timestamp for the monitored file. As a
+ * single-context deployment, add/delete is not applicable here.
+ *
+ *
appengine-web.xml will be reloaded too. However, changes that require a module instance
+ * restart, e.g. address/port, will not be part of the reload.
+ */
+ @Override
+ protected void startHotDeployScanner() throws Exception {
+ String fullScanInterval = System.getProperty("appengine.fullscan.seconds");
+ if (fullScanInterval != null) {
+ try {
+ int interval = Integer.parseInt(fullScanInterval);
+ if (interval < 1) {
+ log.info("Full scan of the web app for changes is disabled.");
+ return;
+ }
+ log.info("Full scan of the web app in place every " + interval + "s.");
+ fullWebAppScanner(interval);
+ return;
+ } catch (NumberFormatException ex) {
+ log.log(Level.WARNING, "appengine.fullscan.seconds property is not an integer:", ex);
+ log.log(Level.WARNING, "Using the default scanning method.");
+ }
+ }
+ scanner = new Scanner();
+ scanner.setReportExistingFilesOnStartup(false);
+ scanner.setScanInterval(SCAN_INTERVAL_SECONDS);
+ scanner.setScanDirs(ImmutableList.of(getScanTarget().toPath()));
+ scanner.setFilenameFilter(
+ new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ try {
+ if (name.equals(getScanTarget().getName())) {
+ return true;
+ }
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ });
+ scanner.addListener(new ScannerListener());
+ scanner.start();
+ }
+
+ @Override
+ protected void stopHotDeployScanner() throws Exception {
+ if (scanner != null) {
+ scanner.stop();
+ }
+ scanner = null;
+ }
+
+ private class ScannerListener implements Scanner.DiscreteListener {
+ @Override
+ public void fileAdded(String filename) throws Exception {
+ // trigger a reload
+ fileChanged(filename);
+ }
+
+ @Override
+ public void fileChanged(String filename) throws Exception {
+ log.info(filename + " updated, reloading the webapp!");
+ reloadWebApp();
+ }
+
+ @Override
+ public void fileRemoved(String filename) throws Exception {
+ // ignored
+ }
+ }
+
+ /** To minimize the overhead, we point the scanner right to the single file in question. */
+ private File getScanTarget() throws Exception {
+ if (appDir.isFile() || context.getWebInf() == null) {
+ // war or running without a WEB-INF
+ return appDir;
+ } else {
+ // by this point, we know the WEB-INF must exist
+ // TODO: consider scanning the whole web-inf
+ return new File(context.getWebInf().getPath() + File.separator + "appengine-web.xml");
+ }
+ }
+
+ private void fullWebAppScanner(int interval) throws IOException {
+ String webInf = context.getWebInf().getPath().toString();
+ List scanList = new ArrayList<>();
+ Collections.addAll(
+ scanList,
+ new File(webInf, "classes").toPath(),
+ new File(webInf, "lib").toPath(),
+ new File(webInf, "web.xml").toPath(),
+ new File(webInf, "appengine-web.xml").toPath());
+
+ scanner = new Scanner();
+ scanner.setScanInterval(interval);
+ scanner.setScanDirs(scanList);
+ scanner.setReportExistingFilesOnStartup(false);
+ scanner.setScanDepth(3);
+
+ scanner.addListener(
+ new Scanner.BulkListener() {
+ @Override
+ public void pathsChanged(Map changeSet) throws Exception {
+ log.info("A file has changed, reloading the web application.");
+ reloadWebApp();
+ }
+ });
+
+ LifeCycle.start(scanner);
+ }
+
+ /**
+ * Assuming Jetty handles race conditions nicely, as this is how Jetty handles a hot deploy too.
+ */
+ @Override
+ protected void reloadWebApp() throws Exception {
+ // Tell Jetty to stop caching jar files, because the changed app may invalidate that
+ // caching.
+ // TODO: Resource.setDefaultUseCaches(false);
+
+ // stop the context
+ server.getHandler().stop();
+ server.stop();
+ moduleConfigurationHandle.restoreSystemProperties();
+ moduleConfigurationHandle.readConfiguration();
+ moduleConfigurationHandle.checkEnvironmentVariables();
+ extractFieldsFromWebModule(moduleConfigurationHandle.getModule());
+
+ /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
+ Thread currentThread = Thread.currentThread();
+ ClassLoader previousCcl = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(null);
+ try {
+ // reinit the context
+ initContext();
+ installLocalInitializationEnvironment();
+ context.setAttribute(WEB_XML_ATTR, webXml);
+ context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
+
+ // reset the handler
+ ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
+ context.insertHandler(apiHandler);
+ server.setHandler(context);
+ EE11SessionManagerHandler unused =
+ EE11SessionManagerHandler.create(
+ EE11SessionManagerHandler.Config.builder()
+ .setEnableSession(isSessionsEnabled())
+ .setServletContextHandler(context)
+ .build());
+ // restart the context (on the same module instance)
+ server.start();
+ } finally {
+ currentThread.setContextClassLoader(previousCcl);
+ }
+ }
+
+ @Override
+ public AppContext getAppContext() {
+ return appContext;
+ }
+
+ @Override
+ public void forwardToServer(HttpServletRequest hrequest, HttpServletResponse hresponse)
+ throws IOException, ServletException {
+ log.finest("forwarding request to module: " + appEngineWebXml.getModule() + "." + instance);
+ RequestDispatcher requestDispatcher =
+ context.getServletContext().getRequestDispatcher(hrequest.getRequestURI());
+ requestDispatcher.forward(hrequest, hresponse);
+ }
+
+ private File determineAppRoot() throws IOException {
+ // Use the context's WEB-INF location instead of appDir since the latter
+ // might refer to a WAR whereas the former gets updated by Jetty when it
+ // extracts a WAR to a temporary directory.
+ Resource webInf = context.getWebInf();
+ if (webInf == null) {
+ if (userCodeClasspathManager.requiresWebInf()) {
+ throw new AppEngineConfigException(
+ "Supplied application has to contain WEB-INF directory.");
+ }
+ return appDir;
+ }
+ return webInf.getPath().toFile().getParentFile();
+ }
+
+ /**
+ * {@code ApiProxyHandler} wraps around an existing {@link Handler} and creates a {@link
+ * com.google.apphosting.api.ApiProxy.Environment} which is stored as a request Attribute and then
+ * set/cleared on a ThreadLocal by the ContextScopeListener {@link ThreadLocal}.
+ */
+ private class ApiProxyHandler extends Handler.Wrapper {
+ @SuppressWarnings("hiding") // Hides AbstractContainerService.appEngineWebXml
+ private final AppEngineWebXml appEngineWebXml;
+
+ public ApiProxyHandler(AppEngineWebXml appEngineWebXml) {
+ this.appEngineWebXml = appEngineWebXml;
+ }
+
+ @Override
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_API_CALLS);
+
+ ServletContextRequest contextRequest = Request.as(request, ServletContextRequest.class);
+ LocalEnvironment env =
+ new LocalHttpRequestEnvironment(
+ appEngineWebXml.getAppId(),
+ WebModule.getModuleName(appEngineWebXml),
+ appEngineWebXml.getMajorVersionId(),
+ instance,
+ getPort(),
+ contextRequest.getServletApiRequest(),
+ SOFT_DEADLINE_DELAY_MS,
+ modulesFilterHelper);
+ env.getAttributes().put(LocalEnvironment.API_CALL_SEMAPHORE, semaphore);
+ env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + devAppServer.getPort());
+
+ request.setAttribute(LocalEnvironment.class.getName(), env);
+ environments.add(env);
+
+ // We need this here because the ContextScopeListener is invoked before
+ // this and so the Environment has not yet been created.
+ ApiProxy.Environment oldEnv = enterScope(request);
+ try {
+ request.addHttpStreamWrapper(
+ s ->
+ new HttpStream.Wrapper(s) {
+ @Override
+ public void succeeded() {
+ onComplete(contextRequest);
+ super.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable x) {
+ onComplete(contextRequest);
+ super.failed(x);
+ }
+ });
+ return super.handle(request, response, callback);
+ } finally {
+ exitScope(oldEnv);
+ }
+ }
+ }
+
+ private void onComplete(ServletContextRequest request) {
+ try {
+ // a special hook with direct access to the container instance
+ // we invoke this only after the normal request processing,
+ // in order to generate a valid response
+ if (request.getHttpURI().getPath().startsWith(AH_URL_RELOAD)) {
+ try {
+ reloadWebApp();
+ Fields parameters = Request.getParameters(request);
+ log.info("Reloaded the webapp context: " + parameters.get("info"));
+ } catch (Exception ex) {
+ log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
+ }
+ }
+ } finally {
+
+ LocalEnvironment env =
+ (LocalEnvironment) request.getAttribute(LocalEnvironment.class.getName());
+ if (env != null) {
+ environments.remove(env);
+
+ // Acquire all of the semaphores back, which will block if any are outstanding.
+ Semaphore semaphore =
+ (Semaphore) env.getAttributes().get(LocalEnvironment.API_CALL_SEMAPHORE);
+ try {
+ semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ log.log(Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
+ }
+
+ try {
+ ApiProxy.setEnvironmentForCurrentThread(env);
+
+ // Invoke all of the registered RequestEndListeners.
+ env.callRequestEndListeners();
+
+ if (apiProxyDelegate instanceof ApiProxyLocal) {
+ // If apiProxyDelegate is not instanceof ApiProxyLocal, we are presumably running in
+ // the devappserver2 environment, where the master web server in Python will take care
+ // of logging requests.
+ ApiProxyLocal apiProxyLocal = (ApiProxyLocal) apiProxyDelegate;
+ String appId = env.getAppId();
+ String versionId = env.getVersionId();
+ String requestId = DevLogHandler.getRequestId();
+
+ LocalLogService logService =
+ (LocalLogService) apiProxyLocal.getService(LocalLogService.PACKAGE);
+
+ ServletApiRequest httpServletRequest = request.getServletApiRequest();
+ @SuppressWarnings("NowMillis")
+ long nowMillis = System.currentTimeMillis();
+ try {
+ logService.addRequestInfo(
+ appId,
+ versionId,
+ requestId,
+ httpServletRequest.getRemoteAddr(),
+ httpServletRequest.getRemoteUser(),
+ Request.getTimeStamp(request) * 1000,
+ nowMillis * 1000,
+ request.getMethod(),
+ httpServletRequest.getRequestURI(),
+ httpServletRequest.getProtocol(),
+ httpServletRequest.getHeader("User-Agent"),
+ true,
+ request.getHttpServletResponse().getStatus(),
+ request.getHeaders().get("Referrer"));
+ logService.clearResponseSize();
+ } catch (NullPointerException ignored) {
+ // TODO remove when
+ // https://github.com/GoogleCloudPlatform/appengine-java-standard/issues/70 is fixed
+ }
+ }
+ } finally {
+ ApiProxy.clearEnvironmentForCurrentThread();
+ }
+ }
+ }
+ }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyResponseRewriterFilter.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyResponseRewriterFilter.java
new file mode 100644
index 000000000..0aad83779
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyResponseRewriterFilter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.development.jetty.ee11;
+
+import com.google.appengine.tools.development.jakarta.ResponseRewriterFilter;
+import com.google.common.base.Preconditions;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.OutputStream;
+
+/**
+ * A filter that rewrites the response headers and body from the user's application.
+ *
+ *
This sanitises the headers to ensure that they are sensible and the user is not setting
+ * sensitive headers, such as Content-Length, incorrectly. It also deletes the body if the response
+ * status code indicates a non-body status.
+ *
+ *
This also strips out some request headers before passing the request to the application.
+ */
+public class JettyResponseRewriterFilter extends ResponseRewriterFilter {
+
+ public JettyResponseRewriterFilter() {
+ super();
+ }
+
+ /**
+ * Creates a JettyResponseRewriterFilter for testing purposes, which mocks the current time.
+ *
+ * @param mockTimestamp Indicates that the current time will be emulated with this timestamp.
+ */
+ public JettyResponseRewriterFilter(long mockTimestamp) {
+ super(mockTimestamp);
+ }
+
+ @Override
+ protected ResponseWrapper getResponseWrapper(HttpServletResponse response) {
+ return new ResponseWrapper(response);
+ }
+
+ private static class ResponseWrapper extends ResponseRewriterFilter.ResponseWrapper {
+
+ public ResponseWrapper(HttpServletResponse response) {
+ super(response);
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ // The user can write directly into our private buffer.
+ // The response will not be committed until all rewriting is complete.
+ if (bodyServletStream != null) {
+ return bodyServletStream;
+ } else {
+ Preconditions.checkState(bodyPrintWriter == null, "getWriter has already been called");
+ bodyServletStream = new ServletOutputStreamWrapper(body);
+ return bodyServletStream;
+ }
+ }
+
+ /** A ServletOutputStream that wraps some other OutputStream. */
+ private static class ServletOutputStreamWrapper
+ extends ResponseRewriterFilter.ResponseWrapper.ServletOutputStreamWrapper {
+
+ ServletOutputStreamWrapper(OutputStream stream) {
+ super(stream);
+ }
+
+ // New method and new new class WriteListener only in Servlet 3.1.
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ // Not used for us.
+ }
+ }
+ }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/LocalJspC.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/LocalJspC.java
new file mode 100644
index 000000000..e1f97361b
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/LocalJspC.java
@@ -0,0 +1,96 @@
+/*
+ * 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.development.jetty.ee11;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+import org.apache.jasper.JasperException;
+import org.apache.jasper.JspC;
+import org.apache.jasper.compiler.AntCompiler;
+import org.apache.jasper.compiler.Localizer;
+import org.apache.jasper.compiler.SmapStratum;
+
+/**
+ * Simple wrapper around the Apache JSP compiler. It defines a Java compiler only to compile the
+ * user defined tag files, as it seems that this cannot be avoided. For the regular JSPs, the
+ * compilation phase is not done here but in single compiler invocation during deployment, to speed
+ * up compilation (See cr/37599187.)
+ */
+public class LocalJspC {
+
+ // Cannot use System.getProperty("java.class.path") anymore
+ // as this process can run embedded in the GAE tools JVM. so we cache
+ // the classpath parameter passed to the JSP compiler to be used to compile
+ // the generated java files for user tag libs.
+ static String classpath;
+
+ public static void main(String[] args) throws JasperException {
+ if (args.length == 0) {
+ System.out.println(Localizer.getMessage("jspc.usage"));
+ } else {
+ JspC jspc =
+ new JspC() {
+ @Override
+ public String getCompilerClassName() {
+ return LocalCompiler.class.getName();
+ }
+ };
+ jspc.setArgs(args);
+ jspc.setCompiler("extJavac");
+ jspc.setAddWebXmlMappings(true);
+ classpath = jspc.getClassPath();
+ jspc.execute();
+ }
+ }
+
+ /**
+ * Very simple compiler for JSPc that is behaving like the ANT compiler, but uses the Tools System
+ * Java compiler to speed compilation process. Only the generated code for *.tag files is compiled
+ * by JSPc even with the "-compile" flag not set.
+ */
+ public static class LocalCompiler extends AntCompiler {
+
+ // Cache the compiler and the file manager:
+ static JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+
+ @Override
+ protected void generateClass(Map smaps) {
+ // Lazily check for the existence of the compiler:
+ if (compiler == null) {
+ throw new RuntimeException(
+ "Cannot get the System Java Compiler. Please use a JDK, not a JRE.");
+ }
+ StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
+ ArrayList files = new ArrayList<>();
+ files.add(new File(ctxt.getServletJavaFileName()));
+ List optionList = new ArrayList<>();
+ // Set compiler's classpath to be same as the jspc main class's
+ optionList.addAll(Arrays.asList("-classpath", LocalJspC.classpath));
+ optionList.addAll(Arrays.asList("-encoding", ctxt.getOptions().getJavaEncoding()));
+ Iterable extends JavaFileObject> compilationUnits =
+ fileManager.getJavaFileObjectsFromFiles(files);
+ compiler.getTask(null, fileManager, null, optionList, null, compilationUnits).call();
+ }
+ }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/LocalResourceFileServlet.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/LocalResourceFileServlet.java
new file mode 100644
index 000000000..a88cd468a
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/LocalResourceFileServlet.java
@@ -0,0 +1,304 @@
+/*
+ * 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.development.jetty.ee11;
+
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.apphosting.utils.config.WebXml;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHandler;
+import org.eclipse.jetty.ee11.servlet.ServletMapping;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * {@code ResourceFileServlet} is a copy of {@code org.mortbay.jetty.servlet.DefaultServlet} that
+ * has been trimmed down to only support the subset of features that we want to take advantage of
+ * (e.g. no gzipping, no chunked encoding, no buffering, etc.). A number of Jetty-specific
+ * optimizations and assumptions have also been removed (e.g. use of custom header manipulation
+ * API's, use of {@code ByteArrayBuffer} instead of Strings, etc.).
+ *
+ *
A few remaining Jetty-centric details remain, such as use of the {@link ServletContextHandler}
+ * class, and Jetty-specific request attributes, but these are specific cases where there is no
+ * servlet-engine-neutral API available. This class also uses Jetty's {@link Resource} class as a
+ * convenience, but could be converted to use {@link
+ * jakarta.servlet.ServletContext#getResource(String)} instead.
+ */
+public class LocalResourceFileServlet extends HttpServlet {
+ private static final Logger logger = Logger.getLogger(LocalResourceFileServlet.class.getName());
+
+ private StaticFileUtils staticFileUtils;
+ private Resource resourceBase;
+ private String[] welcomeFiles;
+ private String resourceRoot;
+ private String defaultServletName;
+
+ /**
+ * Initialize the servlet by extracting some useful configuration data from the current {@link
+ * jakarta.servlet.ServletContext}.
+ */
+ @Override
+ public void init() throws ServletException {
+ ServletContext servletContext = getServletContext();
+ staticFileUtils = new StaticFileUtils(servletContext);
+
+ // AFAICT, there is no real API to retrieve this information, so
+ // we access Jetty's internal state.
+ ServletContextHandler contextHandler =
+ ServletContextHandler.getServletContextHandler(servletContext);
+ welcomeFiles = contextHandler.getWelcomeFiles();
+
+ ServletMapping servletMapping = contextHandler.getServletHandler().getServletMapping("/");
+ if (servletMapping == null) {
+ throw new ServletException("No servlet mapping found");
+ }
+ defaultServletName = servletMapping.getServletName();
+
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ servletContext.getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ resourceRoot = appEngineWebXml.getPublicRoot();
+ try {
+
+ String base;
+ if (resourceRoot.startsWith("/")) {
+ base = resourceRoot;
+ } else {
+ base = "/" + resourceRoot;
+ }
+ // In Jetty 9 "//public" is not seen as "/public" .
+ resourceBase = ResourceFactory.root().newResource(servletContext.getResource(base));
+ } catch (MalformedURLException ex) {
+ logger.log(Level.WARNING, "Could not initialize:", ex);
+ throw new ServletException(ex);
+ }
+ }
+
+ public static final java.lang.String __INCLUDE_JETTY = RequestDispatcher.FORWARD_REQUEST_URI;
+ public static final java.lang.String __INCLUDE_SERVLET_PATH = RequestDispatcher.INCLUDE_SERVLET_PATH;
+ public static final java.lang.String __INCLUDE_PATH_INFO = RequestDispatcher.INCLUDE_PATH_INFO;
+ public static final java.lang.String __FORWARD_JETTY = RequestDispatcher.FORWARD_REQUEST_URI;
+
+ /** Retrieve the static resource file indicated. */
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String servletPath;
+ String pathInfo;
+
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ getServletContext()
+ .getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ WebXml webXml =
+ (WebXml) getServletContext().getAttribute("com.google.appengine.tools.development.webXml");
+
+ Boolean forwarded = request.getAttribute(__FORWARD_JETTY) != null;
+ if (forwarded == null) {
+ forwarded = Boolean.FALSE;
+ }
+
+ Boolean included = request.getAttribute(__INCLUDE_JETTY) != null;
+ if (included != null && included) {
+ servletPath = (String) request.getAttribute(__INCLUDE_SERVLET_PATH);
+ pathInfo = (String) request.getAttribute(__INCLUDE_PATH_INFO);
+ if (servletPath == null) {
+ servletPath = request.getServletPath();
+ pathInfo = request.getPathInfo();
+ }
+ } else {
+ included = Boolean.FALSE;
+ servletPath = request.getServletPath();
+ pathInfo = request.getPathInfo();
+ }
+
+ String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
+
+ if (maybeServeWelcomeFile(pathInContext, included, request, response)) {
+ // We served a welcome file (either via redirecting, forwarding, or including).
+ return;
+ }
+
+ // Find the resource
+ Resource resource = null;
+ try {
+ resource = getResource(pathInContext);
+
+ // Handle resource
+ if (resource != null && resource.isDirectory()) {
+ if (included || staticFileUtils.passConditionalHeaders(request, response, resource)) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
+ } else {
+ if (resource == null || !resource.exists()) {
+ logger.warning("No file found for: " + pathInContext);
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ } else {
+ boolean isStatic = appEngineWebXml.includesStatic(resourceRoot + pathInContext);
+ boolean isResource = appEngineWebXml.includesResource(resourceRoot + pathInContext);
+ boolean usesRuntime = webXml.matches(pathInContext);
+ Boolean isWelcomeFile =
+ (Boolean)
+ request.getAttribute("com.google.appengine.tools.development.isWelcomeFile");
+ if (isWelcomeFile == null) {
+ isWelcomeFile = false;
+ }
+
+ if (!isStatic && !usesRuntime && !(included || forwarded)) {
+ logger.warning(
+ "Can not serve "
+ + pathInContext
+ + " directly. "
+ + "You need to include it in in your "
+ + "appengine-web.xml.");
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ } else if (!isResource && !isWelcomeFile && (included || forwarded)) {
+ logger.warning(
+ "Could not serve "
+ + pathInContext
+ + " from a forward or "
+ + "include. You need to include it in in "
+ + "your appengine-web.xml.");
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ // passConditionalHeaders will set response headers, and
+ // return true if we also need to send the content.
+ if (included || staticFileUtils.passConditionalHeaders(request, response, resource)) {
+ staticFileUtils.sendData(request, response, included, resource);
+ }
+ }
+ }
+ } finally {
+ if (resource != null) {
+ // TODO: how to release
+ // resource.release();
+ }
+ }
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ doGet(request, response);
+ }
+
+ /**
+ * Get Resource to serve.
+ *
+ * @param pathInContext The path to find a resource for.
+ * @return The resource to serve. Can be null.
+ */
+ private Resource getResource(String pathInContext) {
+ try {
+ if (resourceBase != null) {
+ return resourceBase.resolve(pathInContext);
+ }
+ } catch (Throwable t) {
+ logger.log(Level.WARNING, "Could not find: " + pathInContext, t);
+ }
+ return null;
+ }
+
+ /**
+ * Finds a matching welcome file for the supplied path and, if found, serves it to the user. This
+ * will be the first entry in the list of configured {@link #welcomeFiles welcome files} that
+ * exists within the directory referenced by the path. If the resource is not a directory, or no
+ * matching file is found, then null is returned. The list of welcome files is read
+ * from the {@link ServletContextHandler} for this servlet, or "index.jsp" , "index.html"
+ * if that is null.
+ *
+ * @return true if a welcome file was served, false otherwise
+ * @throws IOException
+ * @throws MalformedURLException
+ */
+ private boolean maybeServeWelcomeFile(
+ String path, boolean included, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ if (welcomeFiles == null) {
+ return false;
+ }
+
+ // Add a slash for matching purposes. If we needed this slash, we
+ // are not doing an include, and we're not going to redirect
+ // somewhere else we'll redirect the user to add it later.
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ getServletContext()
+ .getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ ServletContext context = getServletContext();
+ ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context);
+ ServletHandler handler = contextHandler.getServletHandler();
+ ServletHandler.MappedServlet jspEntry = handler.getMappedServlet("/foo.jsp");
+
+ // Search for dynamic welcome files.
+ for (String welcomeName : welcomeFiles) {
+ String welcomePath = path + welcomeName;
+ String relativePath = welcomePath.substring(1);
+
+ ServletHandler.MappedServlet mappedServlet = handler.getMappedServlet(welcomePath);
+ if (!Objects.equals(mappedServlet.getServletHolder().getName(), defaultServletName)
+ && !Objects.equals(mappedServlet, jspEntry)) {
+ // It's a path mapped to a servlet. Forward to it.
+ RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName);
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, included, request, response);
+ }
+
+ Resource welcomeFile = getResource(path + welcomeName);
+ if (welcomeFile != null && welcomeFile.exists()) {
+ if (!Objects.equals(mappedServlet.getServletHolder().getName(), defaultServletName)) {
+ RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName);
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, included, request, response);
+ }
+ if (appEngineWebXml.includesResource(relativePath)) {
+ // It's a resource file. Forward to it.
+ RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName);
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, included, request, response);
+ }
+ }
+ RequestDispatcher namedDispatcher = context.getNamedDispatcher(welcomeName);
+ if (namedDispatcher != null) {
+ // It's a servlet name (allowed by Servlet 2.4 spec). We have
+ // to forward to it.
+ return staticFileUtils.serveWelcomeFileAsForward(
+ namedDispatcher, included,
+ request, response);
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/StaticFileFilter.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/StaticFileFilter.java
new file mode 100644
index 000000000..662461035
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/StaticFileFilter.java
@@ -0,0 +1,234 @@
+/*
+ * 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.development.jetty.ee11;
+
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.InvalidPathException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * {@code StaticFileFilter} is a {@link Filter} that replicates the static file serving logic that
+ * is present in the PFE and AppServer. This logic was originally implemented in {@link
+ * LocalResourceFileServlet} but static file serving needs to take precedence over all other
+ * servlets and filters.
+ */
+public class StaticFileFilter implements Filter {
+ private static final Logger logger = Logger.getLogger(StaticFileFilter.class.getName());
+
+ private StaticFileUtils staticFileUtils;
+ private AppEngineWebXml appEngineWebXml;
+ private Resource resourceBase;
+ private String[] welcomeFiles;
+ private String resourceRoot;
+ private ServletContext servletContext;
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ ServletContextHandler contextHandler =
+ ServletContextHandler.getServletContextHandler(servletContext);
+ servletContext = contextHandler.getServletContext();
+ staticFileUtils = new StaticFileUtils(servletContext);
+
+ // AFAICT, there is no real API to retrieve this information, so
+ // we access Jetty's internal state.
+ welcomeFiles = contextHandler.getWelcomeFiles();
+
+ appEngineWebXml =
+ (AppEngineWebXml)
+ servletContext.getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+ resourceRoot = appEngineWebXml.getPublicRoot();
+
+ try {
+ String base;
+ if (resourceRoot.startsWith("/")) {
+ base = resourceRoot;
+ } else {
+ base = "/" + resourceRoot;
+ }
+ // in Jetty 9 "//public" is not seen as "/public".
+ resourceBase = ResourceFactory.root().newResource(servletContext.getResource(base));
+ } catch (MalformedURLException ex) {
+ logger.log(Level.WARNING, "Could not initialize:", ex);
+ throw new ServletException(ex);
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws ServletException, IOException {
+ Boolean forwarded = (Boolean) request.getAttribute(LocalResourceFileServlet.__FORWARD_JETTY);
+ if (forwarded == null) {
+ forwarded = Boolean.FALSE;
+ }
+
+ Boolean included = (Boolean) request.getAttribute(LocalResourceFileServlet.__INCLUDE_JETTY);
+ if (included == null) {
+ included = Boolean.FALSE;
+ }
+
+ if (forwarded || included) {
+ // If we're forwarded or included, the request is already in the
+ // runtime and static file serving is not relevant.
+ chain.doFilter(request, response);
+ return;
+ }
+
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ String servletPath = httpRequest.getServletPath();
+ String pathInfo = httpRequest.getPathInfo();
+ String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
+
+ if (maybeServeWelcomeFile(pathInContext, httpRequest, httpResponse)) {
+ // We served a welcome file.
+ return;
+ }
+
+ // Find the resource
+ Resource resource = null;
+ try {
+ resource = getResource(pathInContext);
+
+ // Handle resource
+ if (resource != null && resource.exists() && !resource.isDirectory()) {
+ if (appEngineWebXml.includesStatic(resourceRoot + pathInContext)) {
+ // passConditionalHeaders will set response headers, and
+ // return true if we also need to send the content.
+ if (staticFileUtils.passConditionalHeaders(httpRequest, httpResponse, resource)) {
+ staticFileUtils.sendData(httpRequest, httpResponse, false, resource);
+ }
+ return;
+ }
+ }
+ } finally {
+ if (resource != null) {
+ // TODO: how to release
+ // resource.release();
+ }
+ }
+ chain.doFilter(request, response);
+ }
+
+ /**
+ * Get Resource to serve.
+ *
+ * @param pathInContext The path to find a resource for.
+ * @return The resource to serve.
+ */
+ private Resource getResource(String pathInContext) {
+ try {
+ if (resourceBase != null) {
+ return resourceBase.resolve(pathInContext);
+ }
+ } catch (InvalidPathException ex) {
+ // Do not warn for Windows machines for trying to access invalid paths like
+ // "hello/po:tato/index.html" that gives a InvalidPathException: Illegal char <:> error.
+ // This is definitely not a static resource.
+ if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
+ logger.log(Level.WARNING, "Could not find: " + pathInContext, ex);
+ }
+ } catch (Throwable t) {
+ logger.log(Level.WARNING, "Could not find: " + pathInContext, t);
+ }
+ return null;
+ }
+
+ /**
+ * Finds a matching welcome file for the supplied path and, if found, serves it to the user. This
+ * will be the first entry in the list of configured {@link #welcomeFiles welcome files} that
+ * exists within the directory referenced by the path.
+ *
+ * @param path
+ * @param request
+ * @param response
+ * @return true if a welcome file was served, false otherwise
+ * @throws IOException
+ * @throws MalformedURLException
+ */
+ private boolean maybeServeWelcomeFile(
+ String path, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ if (welcomeFiles == null) {
+ return false;
+ }
+
+ // Add a slash for matching purposes. If we needed this slash, we
+ // are not doing an include, and we're not going to redirect
+ // somewhere else we'll redirect the user to add it later.
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+
+ // First search for static welcome files.
+ for (String welcomeName : welcomeFiles) {
+ final String welcomePath = path + welcomeName;
+
+ Resource welcomeFile = getResource(path + welcomeName);
+ if (welcomeFile != null && welcomeFile.exists()) {
+ if (appEngineWebXml.includesStatic(resourceRoot + welcomePath)) {
+ // In production, we optimize this case by routing requests
+ // for static welcome files directly to the static file
+ // (without a redirect). This logic is here to emulate that
+ // case.
+ //
+ // Note that we want to forward to *our* default servlet,
+ // even if the default servlet for this webapp has been
+ // overridden.
+ RequestDispatcher dispatcher = servletContext.getNamedDispatcher("_ah_default");
+ // We need to pass in the new path so it doesn't try to do
+ // its own (dynamic) welcome path logic.
+ request =
+ new HttpServletRequestWrapper(request) {
+ @Override
+ public String getServletPath() {
+ return welcomePath;
+ }
+
+ @Override
+ public String getPathInfo() {
+ return "";
+ }
+ };
+ return staticFileUtils.serveWelcomeFileAsForward(dispatcher, false, request, response);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void destroy() {}
+}
diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/StaticFileUtils.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/StaticFileUtils.java
new file mode 100644
index 000000000..59df76526
--- /dev/null
+++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/StaticFileUtils.java
@@ -0,0 +1,424 @@
+/*
+ * 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.development.jetty.ee11;
+
+import com.google.apphosting.utils.config.AppEngineWebXml;
+import com.google.common.annotations.VisibleForTesting;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.io.WriterOutputStream;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * {@code StaticFileUtils} is a collection of utilities shared by {@link LocalResourceFileServlet}
+ * and {@link StaticFileFilter}.
+ */
+public class StaticFileUtils {
+ private static final String DEFAULT_CACHE_CONTROL_VALUE = "public, max-age=600";
+
+ private final ServletContext servletContext;
+
+ public StaticFileUtils(ServletContext servletContext) {
+ this.servletContext = servletContext;
+ }
+
+ public boolean serveWelcomeFileAsRedirect(
+ String path, boolean included, HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ if (included) {
+ // This is an error. We don't have the file so we can't
+ // include it in the request.
+ return false;
+ }
+
+ // Even if the trailing slash is missing, don't bother trying to
+ // add it. We're going to redirect to a full file anyway.
+ response.setContentLength(0);
+ String q = request.getQueryString();
+ if (q != null && q.length() != 0) {
+ response.sendRedirect(path + "?" + q);
+ } else {
+ response.sendRedirect(path);
+ }
+ return true;
+ }
+
+ public boolean serveWelcomeFileAsForward(
+ RequestDispatcher dispatcher,
+ boolean included,
+ HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+ // If the user didn't specify a slash but we know we want a
+ // welcome file, redirect them to add the slash now.
+ if (!included && !request.getRequestURI().endsWith("/")) {
+ redirectToAddSlash(request, response);
+ return true;
+ }
+
+ request.setAttribute("com.google.appengine.tools.development.isWelcomeFile", true);
+ if (dispatcher != null) {
+ if (included) {
+ dispatcher.include(request, response);
+ } else {
+ dispatcher.forward(request, response);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public void redirectToAddSlash(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ StringBuffer buf = request.getRequestURL();
+ int param = buf.lastIndexOf(";");
+ if (param < 0) {
+ buf.append('/');
+ } else {
+ buf.insert(param, '/');
+ }
+ String q = request.getQueryString();
+ if (q != null && q.length() != 0) {
+ buf.append('?');
+ buf.append(q);
+ }
+ response.setContentLength(0);
+ response.sendRedirect(response.encodeRedirectURL(buf.toString()));
+ }
+
+ /**
+ * Check the headers to see if content needs to be sent.
+ *
+ * @return true if the content should be sent, false otherwise.
+ */
+ public boolean passConditionalHeaders(
+ HttpServletRequest request, HttpServletResponse response, Resource resource)
+ throws IOException {
+ if (!request.getMethod().equals(HttpMethod.HEAD.asString())) {
+ String ifms = request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ if (ifms != null) {
+ long ifmsl = -1;
+ try {
+ ifmsl = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ } catch (IllegalArgumentException e) {
+ // Ignore bad date formats.
+ }
+ if (ifmsl != -1) {
+ if (resource.lastModified().toEpochMilli() <= ifmsl) {
+ response.reset();
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ response.flushBuffer();
+ return false;
+ }
+ }
+ }
+
+ // Parse the if[un]modified dates and compare to resource
+ long date = -1;
+ try {
+ date = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
+ } catch (IllegalArgumentException e) {
+ // Ignore bad date formats.
+ }
+ if (date != -1) {
+ if (resource.lastModified().toEpochMilli() > date) {
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /** Write or include the specified resource. */
+ public void sendData(
+ HttpServletRequest request, HttpServletResponse response, boolean include, Resource resource)
+ throws IOException {
+ long contentLength = resource.length();
+ if (!include) {
+ writeHeaders(response, request.getRequestURI(), resource, contentLength);
+ }
+
+ // Get the output stream (or writer)
+ OutputStream out = null;
+ try {
+ out = response.getOutputStream();
+ } catch (IllegalStateException e) {
+ out = new WriterOutputStream(response.getWriter());
+ }
+
+ IO.copy(resource.newInputStream(), out, contentLength);
+ }
+
+ /** Write the headers that should accompany the specified resource. */
+ public void writeHeaders(
+ HttpServletResponse response, String requestPath, Resource resource, long count) {
+ // Set Content-Length. Users are not allowed to override this. Therefore, we
+ // may do this before adding custom static headers.
+ if (count != -1) {
+ if (count < Integer.MAX_VALUE) {
+ response.setContentLength((int) count);
+ } else {
+ response.setHeader(HttpHeader.CONTENT_LENGTH.asString(), String.valueOf(count));
+ }
+ }
+
+ Set headersApplied = addUserStaticHeaders(requestPath, response);
+
+ // Set Content-Type.
+ if (!headersApplied.contains("content-type")) {
+ String contentType = servletContext.getMimeType(resource.getName());
+ if (contentType != null) {
+ response.setContentType(contentType);
+ }
+ }
+
+ // Set Last-Modified.
+ if (!headersApplied.contains("last-modified")) {
+ response.setDateHeader(
+ HttpHeader.LAST_MODIFIED.asString(), resource.lastModified().toEpochMilli());
+ }
+
+ // Set Cache-Control to the default value if it was not explicitly set.
+ if (!headersApplied.contains(HttpHeader.CACHE_CONTROL.asString().toLowerCase())) {
+ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), DEFAULT_CACHE_CONTROL_VALUE);
+ }
+ }
+
+ /**
+ * Adds HTTP Response headers that are specified in appengine-web.xml. The user may specify
+ * headers explicitly using the {@code http-header} element. Also the user may specify cache
+ * expiration headers implicitly using the {@code expiration} attribute. There is no check for
+ * consistency between different specified headers.
+ *
+ * @param localFilePath The path to the static file being served.
+ * @param response The HttpResponse object to which headers will be added
+ * @return The Set of the names of all headers that were added, canonicalized to lower case.
+ */
+ @VisibleForTesting
+ Set addUserStaticHeaders(String localFilePath, HttpServletResponse response) {
+ AppEngineWebXml appEngineWebXml =
+ (AppEngineWebXml)
+ servletContext.getAttribute("com.google.appengine.tools.development.appEngineWebXml");
+
+ Set headersApplied = new HashSet<>();
+ for (AppEngineWebXml.StaticFileInclude include : appEngineWebXml.getStaticFileIncludes()) {
+ Pattern pattern = include.getRegularExpression();
+ if (pattern.matcher(localFilePath).matches()) {
+ for (Map.Entry entry : include.getHttpHeaders().entrySet()) {
+ response.addHeader(entry.getKey(), entry.getValue());
+ headersApplied.add(entry.getKey().toLowerCase());
+ }
+ String expirationString = include.getExpiration();
+ if (expirationString != null) {
+ addCacheControlHeaders(headersApplied, expirationString, response);
+ }
+ break;
+ }
+ }
+ return headersApplied;
+ }
+
+ /**
+ * Adds HTTP headers to the response to describe cache expiration behavior, based on the {@code
+ * expires} attribute of the {@code includes} element of the {@code static-files} element of
+ * appengine-web.xml.
+ *
+ *