diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 6c5421841..68c5d945a 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -31,8 +31,15 @@ jobs: strategy: matrix: os: [ubuntu-latest] - java: [17, 21] + java: [17, 21, 25-ea] jdk: [temurin] + include: + - java: 17 + maven_profile: "" + - java: 21 + maven_profile: "-Pjdk21" + - java: 25-ea + maven_profile: "-Pjdk25" fail-fast: false runs-on: ${{ matrix.os }} @@ -48,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 5dedcf607..44f3cf2c1 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,18 +1,2 @@ -# -# 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. -# - -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar \ No newline at end of file +distributionType=only-script +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 7885ddc5e..6e7d075a1 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. --> -[![Java8/11/17](https://github.com/GoogleCloudPlatform/appengine-java-standard/actions/workflows/maven.yml/badge.svg)](https://github.com/GoogleCloudPlatform/appengine-java-standard/actions/workflows/maven.yml) +[![Java8/11/17/21](https://github.com/GoogleCloudPlatform/appengine-java-standard/actions/workflows/maven.yml/badge.svg)](https://github.com/GoogleCloudPlatform/appengine-java-standard/actions/workflows/maven.yml) [![Maven][maven-version-image]][maven-version-link] [![Code of conduct](https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-blue.svg)](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 and Java 17. +# 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 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 and Java 17 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 8/11/17 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 @@ -48,7 +48,7 @@ Soon we will stop entirely pushing internal 1.9.xx artifacts and encourage all A Orange items are public modules artifacts and yellow are internal ones. Modules ending with * are only used on the production server side. -pom_dependencies +pom_dependencies ### App Engine Java APIs @@ -64,12 +64,12 @@ Source code for all public APIs for com.google.appengine.api.* packages. ``` war - ... + ... com.google.appengine appengine-api-1.0-sdk - 2.0.21 + 2.0.39 javax.servlet @@ -77,9 +77,71 @@ Source code for all public APIs for com.google.appengine.api.* packages. 3.1 provided - ... + ... ``` +* Maven Java 21 with Jarkata EE 10 support pom.xml + + ``` + war + ... + + + com.google.appengine + appengine-api-1.0-sdk + 2.0.39 + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided + + ... + ``` + +* 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 <-- or java25 alpha--> + true + + + + ``` + + +- [Public Java 21/25 Documentation](https://cloud.google.com/appengine/docs/standard/java-gen2/runtime) +- [How to upgrade to Java21/25](https://cloud.google.com/appengine/docs/standard/java-gen2/upgrade-java-runtime) + * Java 17 appengine-web.xml ``` @@ -90,12 +152,23 @@ Source code for all public APIs for com.google.appengine.api.* packages. ``` -* Java 11 appengine-web.xml +* Java 21 appengine-web.xml (will default to EE10, but EE8 possible) + + ``` + + + java21 + true + + ``` + + +* Java 25 appengine-web.xml (will default to EE11, but EE8 possible) Alpha ``` - java11 + java25 true ``` @@ -125,13 +198,29 @@ Source code for remote APIs for App Engine. ``` -* Maven pom.xml + +* Servlet Jarkata EE10 and EE11 web.xml + +``` + + Remote API Servlet + RemoteApiServlet + com.google.apphosting.utils.remoteapi.JakartaRemoteApiServlet + 1 + + + RemoteApiServlet + /remote_api + +``` + +* Maven javax and jakarta API pom.xml ``` com.google.appengine appengine-remote-api - 2.0.21 + 2.0.39 ``` @@ -154,11 +243,11 @@ We moved `com.google.appengine.api.memcache.stdimpl` and its old dependency com.google.appengine appengine-api-legacy.jar/artifactId> - 2.0.21 + 2.0.39 ``` -### Local Unit Testing for Java 8, 11, 17 +### Local Unit Testing for Java 8, 11, 17, 21 - [Code Sample](https://github.com/GoogleCloudPlatform/java-docs-samples/tree/main/unittests) - [Latest javadoc.io Javadocs from this repository](https://javadoc.io/doc/com.google.appengine/appengine-testing) @@ -169,19 +258,19 @@ We moved `com.google.appengine.api.memcache.stdimpl` and its old dependency com.google.appengine appengine-testing - 2.0.21 + 2.0.39 test com.google.appengine appengine-api-stubs - 2.0.21 + 2.0.39 test com.google.appengine appengine-tools-sdk - 2.0.21 + 2.0.39 test ``` @@ -219,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 and Java17 +## Default entrypoint used by Java17, Java21 and Java25 -The Java 11, Java 17 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: @@ -235,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 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 dc9919911..cac5768a8 100644 --- a/TRYLATESTBITSINPROD.md +++ b/TRYLATESTBITSINPROD.md @@ -23,33 +23,55 @@ somewhere in your App Engine Application and use these jars instead of the one i You could either use a custom build of the runtime of pin to a version you like of the runtime, without being impacted with a scheduled new runtime push. -Well, it is possible, but changing just a little bit your application configuration and your +Well, it is possible, by changing just a little bit your application configuration and your pom.xml file. -First, you need to decide which App Engine Java runtime jars version you want to use. There are 3 runtime jars that +First, you need to decide which App Engine Java runtime jars version you want to use. There are 6 runtime jars that are bundled as a Maven assembly under `runtime-deployment`: - * runtime-impl.jar - * runtime-shared.jar + * runtime-impl-jetty9.jar + * runtime-impl-jetty12.jar + * runtime-impl-jetty121.jar + * runtime-shared-jetty9.jar + * runtime-shared-jetty12.jar + * runtime-shared-jetty121-ee8.jar + * runtime-shared-jetty12-ee10.jar + * runtime-shared-jetty121-ee11.jar * runtime-main.jar -Let's say you want the latest from head in this github repository. You could built the 3 jars, add them at the +Let's say you want the latest from head in this github repository. You could built the 9 jars, add them at the top of your web application and change the entrypoint to boot with these jars instead of the one maintained in production. ``` git clone https://github.com/GoogleCloudPlatform/appengine-java-standard.git cd appengine-java-standard - mvn clean install + ./mvnw clean install ``` -Let's assume the current built version is `2.0.22-SNAPSHOT`. +Let's assume the current build version is `3.0.0-SNAPSHOT`. + +See the output of the runtime deployment module which contains all the jars needed by the runtime: + + +``` +ls runtime/deployment/target/runtime-deployment-*/ +runtime-impl-jetty12.jar runtime-impl-jetty121.jar runtime-main.jar runtime-shared-jetty12.jar +runtime-shared-jetty121-ee8.jar runtime-shared-jetty121-ee11.jar +runtime-impl-jetty9.jar runtime-shared-jetty12-ee10.jar runtime-shared-jetty9.jar +``` + +These jars are pushed in Maven Central as well under artifact com.google.appengine:runtime-deployment. +For example, look at all the pushed versions in https://repo1.maven.org/maven2/com/google/appengine/runtime-deployment + +The idea is to add these runtime jars inside your web application during deployment and change the entry point to start using these runtime jars instead of the ones provided by default by the App Engine runtime. + Add the dependency for the GAE runtime jars in your application pom.xml file: ``` - 2.0.22-SNAPSHOT - ${appengine.runtime.location} + 3.0.0-SNAPSHOT + target/${project.artifactId}-${project.version} ... @@ -78,12 +100,36 @@ deployed web application. - ${appengine.runtime.location}/WEB-INF/lib/runtime-impl-${appengine.runtime.version}.jar - ${appengine.runtime.location}/runtime-impl.jar + ${appengine.runtime.location}/WEB-INF/lib/runtime-impl-jetty9-${appengine.runtime.version}.jar + ${appengine.runtime.location}/runtime-impl-jetty9.jar - ${appengine.runtime.location}/WEB-INF/lib/runtime-shared-${appengine.runtime.version}.jar - ${appengine.runtime.location}/runtime-shared.jar + ${appengine.runtime.location}/WEB-INF/lib/runtime-shared-jetty9-${appengine.runtime.version}.jar + ${appengine.runtime.location}/runtime-shared-jetty9.jar + + + ${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 + ${appengine.runtime.location}/runtime-shared-jetty12.jar + + + ${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 @@ -102,7 +148,7 @@ In the appengine-web.xml, modify the entrypoint to use the bundled runtime jars ``` - java17 + java21 true diff --git a/api/pom.xml b/api/pom.xml index 649c67599..5968a75ba 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,13 +21,14 @@ com.google.appengine parent - 2.0.22-SNAPSHOT + 3.0.0-SNAPSHOT true jar AppEngine :: appengine-apis + https://github.com/GoogleCloudPlatform/appengine-java-standard/ API for Google App Engine standard environment @@ -145,6 +146,10 @@ guava-testlib test + + jakarta.servlet + jakarta.servlet-api + @@ -235,7 +240,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.3.1 + 3.11.3 com.microsoft.doclet.DocFxDoclet false @@ -261,6 +266,23 @@ + + maven-compiler-plugin + + + + com.google.auto.service + auto-service + 1.1.1 + + + com.google.auto.value + auto-value + 1.11.0 + + + + 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 1f3b6e098..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 @@ -51,11 +51,15 @@ public UploadOptions maxUploadSizeBytesPerBlob(long maxUploadSizeBytesPerBlob) { return this; } - boolean hasMaxUploadSizeBytesPerBlob() { + /** Determines if the maximum upload size per blob is set. */ + public boolean hasMaxUploadSizeBytesPerBlob() { return maxUploadSizeBytesPerBlob != null; } - long getMaxUploadSizeBytesPerBlob() { + /** + * @returns the maximum upload size per blob. + */ + public long getMaxUploadSizeBytesPerBlob() { if (maxUploadSizeBytesPerBlob == null) { throw new IllegalStateException("maxUploadSizeBytesPerBlob has not been set."); } @@ -76,11 +80,15 @@ public UploadOptions maxUploadSizeBytes(long maxUploadSizeBytes) { return this; } - boolean hasMaxUploadSizeBytes() { + /** Determines if the maximum size is set. */ + public boolean hasMaxUploadSizeBytes() { return maxUploadSizeBytes != null; } - long getMaxUploadSizeBytes() { + /** + * @returns the maximum upload size. + */ + public long getMaxUploadSizeBytes() { if (maxUploadSizeBytes == null) { throw new IllegalStateException("maxUploadSizeBytes has not been set."); } @@ -92,11 +100,15 @@ public UploadOptions googleStorageBucketName(String bucketName) { return this; } - boolean hasGoogleStorageBucketName() { + /** Determines if the storage bucket is set. */ + public boolean hasGoogleStorageBucketName() { return this.gsBucketName != null; } - String getGoogleStorageBucketName() { + /** + * @returns the storage bucket name. + */ + public String getGoogleStorageBucketName() { if (gsBucketName == null) { throw new IllegalStateException("gsBucketName has not been set."); } 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 new file mode 100644 index 000000000..37ebfe4cd --- /dev/null +++ b/api/src/main/java/com/google/appengine/api/blobstore/ee10/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.ee10; + +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; + +/** + * @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; + + /** + * 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/ee10/BlobstoreServiceFactory.java b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceFactory.java new file mode 100644 index 000000000..5ebde48c2 --- /dev/null +++ b/api/src/main/java/com/google/appengine/api/blobstore/ee10/BlobstoreServiceFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.appengine.api.blobstore.ee10; + + +/** + * @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. */ + public static BlobstoreService getBlobstoreService() { + return new BlobstoreServiceImpl(); + } + +} 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 new file mode 100644 index 000000000..488d7fae4 --- /dev/null +++ b/api/src/main/java/com/google/appengine/api/blobstore/ee10/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.ee10; + +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; + +/** + * @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"; + 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/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 145277be4..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 @@ -97,7 +97,7 @@ public String getText() { } } - static class DetailsBuilder { + public static class DetailsBuilder { private @Nullable String from; private @Nullable String to; private @Nullable String cc; @@ -140,7 +140,7 @@ public DetailsBuilder withText(@Nullable String text) { } } - static class BounceNotificationBuilder { + public static class BounceNotificationBuilder { public BounceNotification build() { return new BounceNotification(rawMessage, original, notification); } 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 new file mode 100644 index 000000000..1580bd8e5 --- /dev/null +++ b/api/src/main/java/com/google/appengine/api/mail/ee10/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.ee10; + +import com.google.appengine.api.mail.BounceNotification; +import com.google.appengine.api.utils.ee10.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; + +/** + * @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. + * + * @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/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/mail/stdimpl/GMTransport.java b/api/src/main/java/com/google/appengine/api/mail/stdimpl/GMTransport.java index eedc01c4b..4f0923588 100644 --- a/api/src/main/java/com/google/appengine/api/mail/stdimpl/GMTransport.java +++ b/api/src/main/java/com/google/appengine/api/mail/stdimpl/GMTransport.java @@ -72,6 +72,7 @@ public class GMTransport extends Transport { "In-Reply-To", "List-Id", "List-Unsubscribe", + "List-Unsubscribe-Post", "On-Behalf-Of", "References", "Resent-Date", 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 new file mode 100644 index 000000000..a791d91c2 --- /dev/null +++ b/api/src/main/java/com/google/appengine/api/taskqueue/ee10/DeferredTaskContext.java @@ -0,0 +1,104 @@ +/* + * 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.ee10; + +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; + +/** + * @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 = + "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/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/ee10/HttpRequestParser.java b/api/src/main/java/com/google/appengine/api/utils/ee10/HttpRequestParser.java new file mode 100644 index 000000000..8c7fa3cb3 --- /dev/null +++ b/api/src/main/java/com/google/appengine/api/utils/ee10/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.ee10; + +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/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/appengine_setup/api/src/main/java/com/google/appengine/setup/ApiProxyDelegate.java b/api/src/main/java/com/google/appengine/setup/ApiProxyDelegate.java similarity index 93% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/ApiProxyDelegate.java rename to api/src/main/java/com/google/appengine/setup/ApiProxyDelegate.java index 224af88ff..588c137da 100644 --- a/appengine_setup/api/src/main/java/com/google/appengine/setup/ApiProxyDelegate.java +++ b/api/src/main/java/com/google/appengine/setup/ApiProxyDelegate.java @@ -16,13 +16,15 @@ package com.google.appengine.setup; -import com.google.appengine.repackaged.com.google.common.collect.Lists; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.ApiConfig; import com.google.apphosting.api.ApiProxy.ApiProxyException; import com.google.apphosting.api.ApiProxy.LogRecord; import com.google.apphosting.api.ApiProxy.RPCFailedException; -import com.google.apphosting.utils.remoteapi.RemoteApiPb; +import com.google.apphosting.base.protos.api.RemoteApiPb; +import com.google.common.collect.Lists; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; import java.io.BufferedInputStream; import java.io.IOException; import java.util.List; @@ -147,20 +149,22 @@ protected byte[] runSyncCall(LazyApiProxyEnvironment environment, String package } } try (BufferedInputStream bis = new BufferedInputStream(response.getEntity().getContent())) { - RemoteApiPb.Response remoteResponse = new RemoteApiPb.Response(); - if (!remoteResponse.parseFrom(bis)) { - logger.info( - "HTTP ApiProxy unable to parse response for " + packageName + "." + methodName); + RemoteApiPb.Response remoteResponse = RemoteApiPb.Response.getDefaultInstance(); + try { + remoteResponse.getParserForType().parseFrom(bis); + } catch (InvalidProtocolBufferException e) { + logger.info( + "HTTP ApiProxy unable to parse response for " + packageName + "." + methodName); throw new RPCFailedException(packageName, methodName); } if (remoteResponse.hasRpcError() || remoteResponse.hasApplicationError()) { throw convertRemoteError(remoteResponse, packageName, methodName, logger); } - return remoteResponse.getResponseAsBytes(); + return remoteResponse.getResponse().toByteArray(); } } catch (IOException e) { - logger.info( - "HTTP ApiProxy I/O error for " + packageName + "." + methodName + ": " + e.getMessage()); + logger.info( + "HTTP ApiProxy I/O error for " + packageName + "." + methodName + ": " + e.getMessage()); throw new RPCFailedException(packageName, methodName); } finally { request.releaseConnection(); @@ -179,12 +183,13 @@ protected byte[] runSyncCall(LazyApiProxyEnvironment environment, String package */ static HttpPost createRequest(LazyApiProxyEnvironment environment, String packageName, String methodName, byte[] requestData, int timeoutMs) { - RemoteApiPb.Request remoteRequest = new RemoteApiPb.Request(); + RemoteApiPb.Request.Builder remoteRequest = RemoteApiPb.Request.newBuilder(); remoteRequest.setServiceName(packageName); remoteRequest.setMethod(methodName); - // Commenting below line to validate the use-cases where security ticket may be needed. So far we did not need. - //remoteRequest.setRequestId(environment.getTicket()); - remoteRequest.setRequestAsBytes(requestData); + // Commenting below line to validate the use-cases where security ticket may be needed. So far + // we did not need. + // remoteRequest.setRequestId(environment.getTicket()); + remoteRequest.setRequest(ByteString.copyFrom(requestData)); HttpPost request = new HttpPost("http://" + environment.getServer() + REQUEST_ENDPOINT); request.setHeader(RPC_STUB_ID_HEADER, REQUEST_STUB_ID); @@ -217,8 +222,9 @@ static HttpPost createRequest(LazyApiProxyEnvironment environment, String packag ApiProxyEnvironment.AttributeMapping.DAPPER_ID.headerKey, (String) dapperHeader); } - ByteArrayEntity postPayload = new ByteArrayEntity(remoteRequest.toByteArray(), - ContentType.APPLICATION_OCTET_STREAM); + ByteArrayEntity postPayload = + new ByteArrayEntity( + remoteRequest.getRequest().toByteArray(), ContentType.APPLICATION_OCTET_STREAM); postPayload.setChunked(false); request.setEntity(postPayload); @@ -374,7 +380,7 @@ public List getRequestThreads(LazyApiProxyEnvironment environment) { if (threadFactory != null && threadFactory instanceof RequestThreadFactory) { return ((RequestThreadFactory) threadFactory).getRequestThreads(); } - logger.warning("Got a call to getRequestThreads() but no VmRequestThreadFactory is available"); + logger.warning("Got a call to getRequestThreads() but no VmRequestThreadFactory is available"); return Lists.newLinkedList(); } diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/ApiProxyEnvironment.java b/api/src/main/java/com/google/appengine/setup/ApiProxyEnvironment.java similarity index 100% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/ApiProxyEnvironment.java rename to api/src/main/java/com/google/appengine/setup/ApiProxyEnvironment.java diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/ApiProxySetupUtil.java b/api/src/main/java/com/google/appengine/setup/ApiProxySetupUtil.java similarity index 100% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/ApiProxySetupUtil.java rename to api/src/main/java/com/google/appengine/setup/ApiProxySetupUtil.java diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/AppLogsWriter.java b/api/src/main/java/com/google/appengine/setup/AppLogsWriter.java similarity index 98% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/AppLogsWriter.java rename to api/src/main/java/com/google/appengine/setup/AppLogsWriter.java index fa24342d3..85fa6decd 100644 --- a/appengine_setup/api/src/main/java/com/google/appengine/setup/AppLogsWriter.java +++ b/api/src/main/java/com/google/appengine/setup/AppLogsWriter.java @@ -16,8 +16,8 @@ package com.google.appengine.setup; -import com.google.appengine.repackaged.com.google.common.base.Stopwatch; -import com.google.appengine.repackaged.com.google.protobuf.ByteString; +import com.google.common.base.Stopwatch; +import com.google.protobuf.ByteString; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.ApiConfig; import com.google.apphosting.api.ApiProxy.LogRecord; diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/LazyApiProxyEnvironment.java b/api/src/main/java/com/google/appengine/setup/LazyApiProxyEnvironment.java similarity index 100% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/LazyApiProxyEnvironment.java rename to api/src/main/java/com/google/appengine/setup/LazyApiProxyEnvironment.java diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/RequestThreadFactory.java b/api/src/main/java/com/google/appengine/setup/RequestThreadFactory.java similarity index 92% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/RequestThreadFactory.java rename to api/src/main/java/com/google/appengine/setup/RequestThreadFactory.java index 18d48615d..205b5c40b 100644 --- a/appengine_setup/api/src/main/java/com/google/appengine/setup/RequestThreadFactory.java +++ b/api/src/main/java/com/google/appengine/setup/RequestThreadFactory.java @@ -16,10 +16,10 @@ package com.google.appengine.setup; -import static com.google.appengine.repackaged.com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Preconditions.checkState; -import com.google.appengine.repackaged.com.google.common.collect.ImmutableList; -import com.google.appengine.repackaged.com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.Environment; import java.util.List; diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/RuntimeUtils.java b/api/src/main/java/com/google/appengine/setup/RuntimeUtils.java similarity index 82% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/RuntimeUtils.java rename to api/src/main/java/com/google/appengine/setup/RuntimeUtils.java index 8674e2a25..1f8cb2888 100644 --- a/appengine_setup/api/src/main/java/com/google/appengine/setup/RuntimeUtils.java +++ b/api/src/main/java/com/google/appengine/setup/RuntimeUtils.java @@ -16,11 +16,11 @@ package com.google.appengine.setup; -import static com.google.appengine.repackaged.com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.MoreObjects.firstNonNull; public class RuntimeUtils { - private static final String VM_API_PROXY_HOST = "appengine.googleapis.com"; - private static final int VM_API_PROXY_PORT = 10001; + private static final String API_PROXY_HOST = "appengine.googleapis.com"; + private static final int API_PROXY_PORT = 10001; public static final long ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000; public static final long MAX_USER_API_CALL_WAIT_MS = 60 * 1000; @@ -31,8 +31,8 @@ public class RuntimeUtils { * calculated from them. Otherwise the default host:port is used. */ public static String getApiServerAddress() { - String server = firstNonNull(System.getenv("API_HOST"), VM_API_PROXY_HOST); - String port = firstNonNull(System.getenv("API_PORT"), "" + VM_API_PROXY_PORT); + String server = firstNonNull(System.getenv("API_HOST"), API_PROXY_HOST); + String port = firstNonNull(System.getenv("API_PORT"), "" + API_PROXY_PORT); return server + ":" + port; } } diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/TimerImpl.java b/api/src/main/java/com/google/appengine/setup/TimerImpl.java similarity index 100% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/TimerImpl.java rename to api/src/main/java/com/google/appengine/setup/TimerImpl.java diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/timer/AbstractIntervalTimer.java b/api/src/main/java/com/google/appengine/setup/timer/AbstractIntervalTimer.java similarity index 100% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/timer/AbstractIntervalTimer.java rename to api/src/main/java/com/google/appengine/setup/timer/AbstractIntervalTimer.java diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/timer/Timer.java b/api/src/main/java/com/google/appengine/setup/timer/Timer.java similarity index 100% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/timer/Timer.java rename to api/src/main/java/com/google/appengine/setup/timer/Timer.java diff --git a/appengine_setup/api/src/main/java/com/google/appengine/setup/utils/http/HttpRequest.java b/api/src/main/java/com/google/appengine/setup/utils/http/HttpRequest.java similarity index 100% rename from appengine_setup/api/src/main/java/com/google/appengine/setup/utils/http/HttpRequest.java rename to api/src/main/java/com/google/appengine/setup/utils/http/HttpRequest.java diff --git a/api/src/main/java/com/google/appengine/spi/ServiceFactoryFactory.java b/api/src/main/java/com/google/appengine/spi/ServiceFactoryFactory.java index cc842bdba..e087c8f2e 100644 --- a/api/src/main/java/com/google/appengine/spi/ServiceFactoryFactory.java +++ b/api/src/main/java/com/google/appengine/spi/ServiceFactoryFactory.java @@ -17,8 +17,6 @@ package com.google.appengine.spi; import com.google.common.base.Preconditions; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -137,44 +135,36 @@ private static final class RuntimeRegistry { } } - /** - * Retrieves the list of factory providers from the classpath - */ + /** Retrieves the list of factory providers from the classpath */ private static List> getProvidersUsingServiceLoader() { - return AccessController.doPrivileged( - new PrivilegedAction>>() { - @Override - public List> run() { - List> result = new ArrayList>(); - - // Sandboxed applications use the classloader of this class (ServiceFactoryFactory). - // VM runtime applications use the thread context classloader (if not null) and fall - // back to - // the ServiceFactoryFactory classloader otherwise. - // - ClassLoader classLoader = null; - if (Boolean.getBoolean(USE_THREAD_CONTEXT_CLASSLOADER_PROPERTY)) { - classLoader = Thread.currentThread().getContextClassLoader(); - } - if (classLoader == null) { - // If the classloader isn't set (or set to null). Use the classloader of this class. - classLoader = ServiceFactoryFactory.class.getClassLoader(); - } - - // Can't use parameterized types in ServiceLoader.load - // - @SuppressWarnings("rawtypes") - ServiceLoader providers = - ServiceLoader.load(FactoryProvider.class, classLoader); - - if (providers != null) { - for (FactoryProvider provider : providers) { - result.add(provider); - } - } - - return result; - } - }); + List> result = new ArrayList>(); + + // Sandboxed applications use the classloader of this class (ServiceFactoryFactory). + // VM runtime applications use the thread context classloader (if not null) and fall + // back to + // the ServiceFactoryFactory classloader otherwise. + // + ClassLoader classLoader = null; + if (Boolean.getBoolean(USE_THREAD_CONTEXT_CLASSLOADER_PROPERTY)) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + if (classLoader == null) { + // If the classloader isn't set (or set to null). Use the classloader of this class. + classLoader = ServiceFactoryFactory.class.getClassLoader(); + } + + // Can't use parameterized types in ServiceLoader.load + // + @SuppressWarnings("rawtypes") + ServiceLoader providers = + ServiceLoader.load(FactoryProvider.class, classLoader); + + if (providers != null) { + for (FactoryProvider provider : providers) { + result.add(provider); + } + } + + return result; } } 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 new file mode 100644 index 000000000..04f2cdb1c --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/remoteapi/EE10RemoteApiServlet.java @@ -0,0 +1,22 @@ +/* + * 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; + +/** + * @deprecated as of version 3.0.0, use {@link JakartaRemoteApiServlet} instead. + */ +@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/remoteapi/RemoteApiServlet.java b/api/src/main/java/com/google/apphosting/utils/remoteapi/RemoteApiServlet.java index 745837d2b..f21cded48 100644 --- a/api/src/main/java/com/google/apphosting/utils/remoteapi/RemoteApiServlet.java +++ b/api/src/main/java/com/google/apphosting/utils/remoteapi/RemoteApiServlet.java @@ -1,3 +1,4 @@ + /* * Copyright 2021 Google LLC * @@ -13,11 +14,11 @@ * 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.DatastoreV3Pb.Error.ErrorCode.BAD_REQUEST; -import static com.google.apphosting.datastore.DatastoreV3Pb.Error.ErrorCode.CONCURRENT_TRANSACTION; +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; @@ -25,26 +26,28 @@ import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; import com.google.apphosting.api.ApiProxy; -import com.google.apphosting.datastore.DatastoreV3Pb.BeginTransactionRequest; -import com.google.apphosting.datastore.DatastoreV3Pb.DeleteRequest; -import com.google.apphosting.datastore.DatastoreV3Pb.GetRequest; -import com.google.apphosting.datastore.DatastoreV3Pb.GetResponse; -import com.google.apphosting.datastore.DatastoreV3Pb.NextRequest; -import com.google.apphosting.datastore.DatastoreV3Pb.PutRequest; -import com.google.apphosting.datastore.DatastoreV3Pb.Query; -import com.google.apphosting.datastore.DatastoreV3Pb.QueryResult; -import com.google.apphosting.utils.remoteapi.RemoteApiPb.ApplicationError; -import com.google.apphosting.utils.remoteapi.RemoteApiPb.Request; -import com.google.apphosting.utils.remoteapi.RemoteApiPb.Response; -import com.google.apphosting.utils.remoteapi.RemoteApiPb.TransactionQueryResult; -import com.google.apphosting.utils.remoteapi.RemoteApiPb.TransactionRequest; -import com.google.apphosting.utils.remoteapi.RemoteApiPb.TransactionRequest.Precondition; -import com.google.io.protocol.ProtocolMessage; +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.OnestoreEntity; -import com.google.storage.onestore.v3.OnestoreEntity.EntityProto; -import com.google.storage.onestore.v3.OnestoreEntity.Path.Element; +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 java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; @@ -97,10 +100,8 @@ public UnknownPythonServerException(String message) { * @param req the {@link HttpServletRequest} * @param res the {@link HttpServletResponse} * @return true if the application is known. - * @throws java.io.IOException */ - boolean checkIsValidRequest(HttpServletRequest req, HttpServletResponse res) - throws java.io.IOException { + boolean checkIsValidRequest(HttpServletRequest req, HttpServletResponse res) throws IOException { if (!checkIsKnownInbound(req) && !checkIsAdmin(req, res)) { return false; } @@ -112,10 +113,8 @@ boolean checkIsValidRequest(HttpServletRequest req, HttpServletResponse res) * * @param req the {@link HttpServletRequest} * @return true if the application is known. - * @throws java.io.IOException */ - private synchronized boolean checkIsKnownInbound(HttpServletRequest req) - throws java.io.IOException { + private synchronized boolean checkIsKnownInbound(HttpServletRequest req) { if (allowedApps == null) { allowedApps = new HashSet(); String allowedAppsStr = System.getProperty(INBOUND_APP_SYSTEM_PROPERTY); @@ -136,10 +135,9 @@ private synchronized boolean checkIsKnownInbound(HttpServletRequest req) * @param req the {@link HttpServletRequest} * @param res the {@link HttpServletResponse} * @return true if the header exists. - * @throws java.io.IOException */ private boolean checkIsValidHeader(HttpServletRequest req, HttpServletResponse res) - throws java.io.IOException { + throws IOException { if (req.getHeader("X-appcfg-api-version") == null) { res.setStatus(403); res.setContentType("text/plain"); @@ -152,11 +150,9 @@ private boolean checkIsValidHeader(HttpServletRequest req, HttpServletResponse r /** * 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. + * @return true if the current user is logged in with admin access, false otherwise. */ - private boolean checkIsAdmin(HttpServletRequest req, HttpServletResponse res) - throws java.io.IOException { + private boolean checkIsAdmin(HttpServletRequest req, HttpServletResponse res) throws IOException { UserService userService = UserServiceFactory.getUserService(); // Check for regular (cookie-based) authentication. @@ -185,19 +181,16 @@ private boolean checkIsAdmin(HttpServletRequest req, HttpServletResponse res) return false; } - private void respondNotAdmin(HttpServletResponse res) throws java.io.IOException { + 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. - */ + /** Serve GET requests with a YAML encoding of the app-id and a validation token. */ @Override - public void doGet(HttpServletRequest req, HttpServletResponse res) throws java.io.IOException { + public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { if (!checkIsValidRequest(req, res)) { return; } @@ -209,21 +202,17 @@ public void doGet(HttpServletRequest req, HttpServletResponse res) throws java.i res.getWriter().println(outYaml); } - /** - * Serve POST requests by forwarding calls to ApiProxy. - */ + /** Serve POST requests by forwarding calls to ApiProxy. */ @Override - public void doPost(HttpServletRequest req, HttpServletResponse res) throws java.io.IOException { + public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException { if (!checkIsValidRequest(req, res)) { return; } res.setContentType("application/octet-stream"); - - Response response = new Response(); - + Response.Builder response = Response.newBuilder(); try { byte[] responseData = executeRequest(req); - response.setResponseAsBytes(responseData); + response.setResponse(ByteString.copyFrom(responseData)); res.setStatus(200); } catch (Exception e) { log.warning("Caught exception while executing remote_api command:\n" + e); @@ -233,74 +222,71 @@ public void doPost(HttpServletRequest req, HttpServletResponse res) throws java. out.writeObject(e); out.close(); byte[] serializedException = byteStream.toByteArray(); - response.setJavaExceptionAsBytes(serializedException); + response.setJavaException(ByteString.copyFrom(serializedException)); if (e instanceof ApiProxy.ApplicationException) { ApiProxy.ApplicationException ae = (ApiProxy.ApplicationException) e; - ApplicationError appError = response.getMutableApplicationError(); - appError.setCode(ae.getApplicationError()); - appError.setDetail(ae.getErrorDetail()); + response + .getApplicationErrorBuilder() + .setCode(ae.getApplicationError()) + .setDetail(ae.getErrorDetail()); } } - res.getOutputStream().write(response.toByteArray()); + response.build().writeTo(res.getOutputStream()); } - private byte[] executeRunQuery(Request request) { - Query queryRequest = new Query(); - parseFromBytes(queryRequest, request.getRequestAsBytes()); + 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 runQueryResponse = new QueryResult(); - byte[] res = ApiProxy.makeSyncCall("datastore_v3", "RunQuery", request.getRequestAsBytes()); + 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.isMoreResults()) { - NextRequest nextRequest = new NextRequest(); - nextRequest.getMutableCursor().mergeFrom(runQueryResponse.getCursor()); + while (runQueryResponse.getMoreResults()) { + NextRequest.Builder nextRequest = NextRequest.newBuilder(); + nextRequest.getCursorBuilder().mergeFrom(runQueryResponse.getCursor()); nextRequest.setCount(batchSize); - byte[] nextRes = ApiProxy.makeSyncCall("datastore_v3", "Next", nextRequest.toByteArray()); + byte[] nextRes = + ApiProxy.makeSyncCall("datastore_v3", "Next", nextRequest.build().toByteArray()); parseFromBytes(runQueryResponse, nextRes); } } - return runQueryResponse.toByteArray(); + return runQueryResponse.build().toByteArray(); } - private byte[] executeTxQuery(Request request) { - TransactionQueryResult result = new TransactionQueryResult(); - - Query query = new Query(); - parseFromBytes(query, request.getRequestAsBytes()); - + 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.getValue(), - "No ancestor in transactional query."); + throw new ApiProxy.ApplicationException( + BAD_REQUEST.getNumber(), "No ancestor in transactional query."); } // Make __entity_group__ key - OnestoreEntity.Reference egKey = - result.getMutableEntityGroupKey().mergeFrom(query.getAncestor()); + OnestoreEntity.Reference.Builder egKey = + result.getEntityGroupKeyBuilder().mergeFrom(query.getAncestor()); OnestoreEntity.Path.Element root = egKey.getPath().getElement(0); - egKey.getMutablePath().clearElement().addElement(root); - OnestoreEntity.Path.Element egElement = new OnestoreEntity.Path.Element(); - egElement.setType("__entity_group__").setId(1); - egKey.getMutablePath().addElement(egElement); - + 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.getMutableTransaction(), tx); - byte[] queryBytes = ApiProxy.makeSyncCall("datastore_v3", "RunQuery", query.toByteArray()); - parseFromBytes(result.getMutableResult(), queryBytes); - - GetRequest egRequest = new GetRequest(); + 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 egResponse = txGet(tx, egRequest); + GetResponse.Builder egResponse = txGet(tx, egRequest); if (egResponse.getEntity(0).hasEntity()) { result.setEntityGroup(egResponse.getEntity(0).getEntity()); } rollback(tx); - - return result.toByteArray(); + return result.build().toByteArray(); } /** @@ -310,48 +296,40 @@ 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.getValue(), - "Transaction precondition failed"); + 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.getHashAsBytes(), computeSha1(entity))) { + 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.getHashAsBytes(), backwardsCompatibleHash)) { + if (!Arrays.equals(precondition.getHashBytes().toByteArray(), backwardsCompatibleHash)) { throw new ApiProxy.ApplicationException( - CONCURRENT_TRANSACTION.getValue(), "Transaction precondition failed"); + CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition failed"); } } // Else, the Entity was missing from both. } - private byte[] executeTx(Request request) { - TransactionRequest txRequest = new TransactionRequest(); - parseFromBytes(txRequest, request.getRequestAsBytes()); - - byte[] tx = beginTransaction(txRequest.isAllowMultipleEg()); - - List preconditions = txRequest.preconditions(); - + 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 getRequest = new GetRequest(); + GetRequest.Builder getRequest = GetRequest.newBuilder(); for (Precondition precondition : preconditions) { OnestoreEntity.Reference key = precondition.getKey(); - OnestoreEntity.Reference requestKey = getRequest.addKey(); - requestKey.mergeFrom(key); + getRequest.addKeyBuilder().mergeFrom(key); } - - GetResponse getResponse = txGet(tx, getRequest); - List entities = getResponse.entitys(); - + 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()); @@ -364,51 +342,47 @@ private byte[] executeTx(Request request) { // Perform puts. byte[] res = new byte[0]; // a serialized VoidProto if (txRequest.hasPuts()) { - PutRequest putRequest = txRequest.getPuts(); - parseFromBytes(putRequest.getMutableTransaction(), tx); - res = ApiProxy.makeSyncCall("datastore_v3", "Put", putRequest.toByteArray()); + PutRequest.Builder putRequest = txRequest.getPutsBuilder(); + parseFromBytes(putRequest.getTransactionBuilder(), tx); + res = ApiProxy.makeSyncCall("datastore_v3", "Put", putRequest.build().toByteArray()); } // Perform deletes. if (txRequest.hasDeletes()) { - DeleteRequest deleteRequest = txRequest.getDeletes(); - parseFromBytes(deleteRequest.getMutableTransaction(), tx); - ApiProxy.makeSyncCall("datastore_v3", "Delete", deleteRequest.toByteArray()); + 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 request, boolean isXG) { - PutRequest putRequest = new PutRequest(); - parseFromBytes(putRequest, request.getRequestAsBytes()); - for (EntityProto entity : putRequest.entitys()) { - assert (entity.propertySize() == 0); - assert (entity.rawPropertySize() == 0); - assert (entity.getEntityGroup().elementSize() == 0); - List elementList = entity.getKey().getPath().elements(); + 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); - assert (lastPart.getId() == 0); - assert (!lastPart.hasName()); + verify(lastPart.getId() == 0); + verify(!lastPart.hasName()); } - // Start a Transaction. - // TODO: Shouldn't this use allocateIds instead? - byte[] tx = beginTransaction(isXG); - parseFromBytes(putRequest.getMutableTransaction(), tx); - + 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.toByteArray()); - + 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 java.io.IOException { - Request request = new Request(); + private byte[] executeRequest(HttpServletRequest req) throws IOException { + Request.Builder request = Request.newBuilder(); parseFromInputStream(request, req.getInputStream()); String service = request.getServiceName(); String method = request.getMethod(); @@ -430,7 +404,7 @@ private byte[] executeRequest(HttpServletRequest req) throws java.io.IOException throw new ApiProxy.CallNotFoundException(service, method); } } else { - return ApiProxy.makeSyncCall(service, method, request.getRequestAsBytes()); + return ApiProxy.makeSyncCall(service, method, request.getRequest().toByteArray()); } } @@ -438,8 +412,12 @@ private byte[] executeRequest(HttpServletRequest req) throws java.io.IOException private static byte[] beginTransaction(boolean allowMultipleEg) { String appId = ApiProxy.getCurrentEnvironment().getAppId(); - byte[] req = new BeginTransactionRequest().setApp(appId) - .setAllowMultipleEg(allowMultipleEg).toByteArray(); + byte[] req = + BeginTransactionRequest.newBuilder() + .setApp(appId) + .setAllowMultipleEg(allowMultipleEg) + .build() + .toByteArray(); return ApiProxy.makeSyncCall("datastore_v3", "BeginTransaction", req); } @@ -447,10 +425,10 @@ private static void rollback(byte[] tx) { ApiProxy.makeSyncCall("datastore_v3", "Rollback", tx); } - private static GetResponse txGet(byte[] tx, GetRequest request) { - parseFromBytes(request.getMutableTransaction(), tx); - GetResponse response = new GetResponse(); - byte[] resultBytes = ApiProxy.makeSyncCall("datastore_v3", "Get", request.toByteArray()); + 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; } @@ -481,30 +459,41 @@ private static byte[] computeSha1(byte[] bytes, int length) { md = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException e) { throw new ApiProxy.ApplicationException( - CONCURRENT_TRANSACTION.getValue(), "Transaction precondition could not be computed"); + CONCURRENT_TRANSACTION.getNumber(), "Transaction precondition could not be computed"); } md.update(bytes, 0, length); return md.digest(); } - private static void parseFromBytes(ProtocolMessage message, byte[] bytes) { - boolean parsed = message.parseFrom(bytes); - checkParse(message, parsed); + 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(ProtocolMessage message, InputStream inputStream) { - boolean parsed = message.parseFrom(inputStream); - checkParse(message, 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(ProtocolMessage message, boolean parsed) { + + private static void checkParse(Message message, boolean parsed) { if (!parsed) { throw new ApiProxy.ApiProxyException("Could not parse protobuf"); } - String error = message.findInitializationError(); - if (error != null) { - throw new ApiProxy.ApiProxyException("Could not parse protobuf: " + error); + 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 new file mode 100644 index 000000000..101d64d58 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/DeferredTaskServlet.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.apphosting.utils.servlet.ee10; + +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.DeferredTaskServlet} instead. + */ +@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 new file mode 100644 index 000000000..8430df85f --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/JdbcMySqlConnectionCleanupFilter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.apphosting.utils.servlet.ee10; + +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.JdbcMySqlConnectionCleanupFilter} instead. + */ +@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 new file mode 100644 index 000000000..f812c258d --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/MultipartMimeUtils.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.apphosting.utils.servlet.ee10; + +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.MultipartMimeUtils} instead. + */ +@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 new file mode 100644 index 000000000..e78e257f8 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/ParseBlobUploadFilter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.apphosting.utils.servlet.ee10; + +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.ParseBlobUploadFilter} instead. + */ +@Deprecated(since = "3.0.0") +public class ParseBlobUploadFilter + extends com.google.apphosting.utils.servlet.jakarta.ParseBlobUploadFilter {} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/SessionCleanupServlet.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/SessionCleanupServlet.java new file mode 100644 index 000000000..f5766daf7 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/SessionCleanupServlet.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.apphosting.utils.servlet.ee10; + +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.SessionCleanupServlet} instead. + */ +@Deprecated(since = "3.0.0") +public class SessionCleanupServlet + extends com.google.apphosting.utils.servlet.jakarta.SessionCleanupServlet {} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/SnapshotServlet.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/SnapshotServlet.java new file mode 100644 index 000000000..e257eb317 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/SnapshotServlet.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.apphosting.utils.servlet.ee10; + +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.SnapshotServlet} instead. + */ +@Deprecated(since = "3.0.0") +public class SnapshotServlet + extends com.google.apphosting.utils.servlet.jakarta.SnapshotServlet {} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/ee10/TransactionCleanupFilter.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/TransactionCleanupFilter.java new file mode 100644 index 000000000..ebd6d24b6 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/TransactionCleanupFilter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.apphosting.utils.servlet.ee10; + +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.TransactionCleanupFilter} instead. + */ +@Deprecated(since = "3.0.0") +public class TransactionCleanupFilter + extends com.google.apphosting.utils.servlet.jakarta.TransactionCleanupFilter {} diff --git a/runtime/runtime_impl_jetty12/src/main/java/com/google/apphosting/runtime/jetty/ee8/WebAppContextFactory.java b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/WarmupServlet.java similarity index 66% rename from runtime/runtime_impl_jetty12/src/main/java/com/google/apphosting/runtime/jetty/ee8/WebAppContextFactory.java rename to api/src/main/java/com/google/apphosting/utils/servlet/ee10/WarmupServlet.java index 9b1696c05..54eb09e34 100644 --- a/runtime/runtime_impl_jetty12/src/main/java/com/google/apphosting/runtime/jetty/ee8/WebAppContextFactory.java +++ b/api/src/main/java/com/google/apphosting/utils/servlet/ee10/WarmupServlet.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.google.apphosting.runtime.jetty.ee8; +package com.google.apphosting.utils.servlet.ee10; -import com.google.apphosting.runtime.AppVersion; - -/** A base interface for factories that create {@link AppEngineWebAppContext}. */ -public interface WebAppContextFactory { - AppEngineWebAppContext createContext(AppVersion appVersion, String serverInfo); -} +/** + * @deprecated as of version 3.0, use {@link + * com.google.apphosting.utils.servlet.jakarta.WarmupServlet} instead. + */ +@Deprecated(since = "3.0.0") +public class WarmupServlet extends com.google.apphosting.utils.servlet.jakarta.WarmupServlet {} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/DeferredTaskServlet.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/DeferredTaskServlet.java new file mode 100644 index 000000000..a4e63b1fc --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/DeferredTaskServlet.java @@ -0,0 +1,237 @@ +/* + * 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.servlet.jakarta; + +import com.google.appengine.api.taskqueue.DeferredTask; +import com.google.appengine.api.taskqueue.jakarta.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.
+ * + *

+ *    <servlet>
+ *      <servlet-name>/_ah/queue/__deferred__</servlet-name>
+ *      <servlet-class
+ *        >com.google.apphosting.utils.servlet.DeferredTaskServlet</servlet-class>
+ *    </servlet>
+ *
+ *    <servlet-mapping>
+ *      <servlet-name>_ah_queue_deferred</servlet-name>
+ *      <url-pattern>/_ah/queue/__deferred__</url-pattern>
+ *    </servlet-mapping>
+ * 
+ * + */ +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 ClassNotFoundException}: Deserialization failure. + *
  • {@link IOException}: Deserialization failure. + *
  • {@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); + } + } +} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/JdbcMySqlConnectionCleanupFilter.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/JdbcMySqlConnectionCleanupFilter.java new file mode 100644 index 000000000..81c400a81 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/JdbcMySqlConnectionCleanupFilter.java @@ -0,0 +1,199 @@ +/* + * 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.servlet.jakarta; + +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. + */ +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); + } + } + } +} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/MultipartMimeUtils.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/MultipartMimeUtils.java new file mode 100644 index 000000000..7d576a1d0 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/MultipartMimeUtils.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.apphosting.utils.servlet.jakarta; + +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. + * + */ +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"; + } + } +} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/ParseBlobUploadFilter.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/ParseBlobUploadFilter.java new file mode 100644 index 000000000..f42333607 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/ParseBlobUploadFilter.java @@ -0,0 +1,200 @@ +/* + * 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.servlet.jakarta; + +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}. + * + */ +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> infos = + blobInfos.computeIfAbsent(fieldName, k -> new ArrayList<>()); + infos.add(getInfoFromBody(MultipartMimeUtils.getTextContent(part), blobKeyString)); + } + } else { + List values = otherParams.computeIfAbsent(fieldName, k -> new ArrayList<>()); + values.add(MultipartMimeUtils.getTextContent(part)); + } + } + req.setAttribute(UPLOADED_BLOBKEY_ATTR, blobKeys); + req.setAttribute(UPLOADED_BLOBINFO_ATTR, blobInfos); + } catch (MessagingException ex) { + logger.log(Level.WARNING, "Could not parse multipart message:", ex); + } + + chain.doFilter(new ParameterServletWrapper(request, otherParams), response); + } else { + chain.doFilter(request, response); + } + } + + private Map getInfoFromBody(String bodyContent, String key) + throws MessagingException { + MimeBodyPart part = new MimeBodyPart(new ByteArrayInputStream(bodyContent.getBytes(UTF_8))); + Map info = new HashMap<>(); + info.put("key", key); + info.put("content-type", part.getContentType()); + info.put("creation-date", part.getHeader(UPLOAD_CREATION_HEADER)[0]); + info.put("filename", part.getFileName()); + info.put("size", part.getHeader(CONTENT_LENGTH_HEADER)[0]); // part.getSize() returns 0 + info.put("md5-hash", part.getContentMD5()); + + String[] headers = part.getHeader(CLOUD_STORAGE_OBJECT_HEADER); + if (headers != null && headers.length == 1) { + info.put("gs-name", headers[0]); + } + + return info; + } + + private static class ParameterServletWrapper extends HttpServletRequestWrapper { + private final Map> otherParams; + + ParameterServletWrapper(ServletRequest request, Map> otherParams) { + super((HttpServletRequest) request); + this.otherParams = otherParams; + } + + @SuppressWarnings("unchecked") + @Override + public Map getParameterMap() { + Map parameters = super.getParameterMap(); + if (otherParams.isEmpty()) { + return parameters; + } else { + // HttpServlet.getParameterMap() result is immutable so we need to take a copy. + Map map = new HashMap<>(parameters); + otherParams.forEach((k, v) -> map.put(k, v.toArray(new String[0]))); + // Maintain the semantic of ServletRequestWrapper by returning an immutable map. + return Collections.unmodifiableMap(map); + } + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration getParameterNames() { + List allNames = new ArrayList<>(Collections.list(super.getParameterNames())); + allNames.addAll(otherParams.keySet()); + return Collections.enumeration(allNames); + } + + @Override + public String[] getParameterValues(String name) { + if (otherParams.containsKey(name)) { + return otherParams.get(name).toArray(new String[0]); + } else { + return super.getParameterValues(name); + } + } + + @Override + public String getParameter(String name) { + if (otherParams.containsKey(name)) { + return otherParams.get(name).get(0); + } else { + return super.getParameter(name); + } + } + } +} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/SessionCleanupServlet.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/SessionCleanupServlet.java new file mode 100644 index 000000000..b9ad7fc26 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/SessionCleanupServlet.java @@ -0,0 +1,111 @@ +/* + * 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.servlet.jakarta; + +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.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.Query; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * This servlet is run to cleanup expired sessions. Since our + * sessions are clustered, no individual runtime knows when they expire (nor + * do we guarantee that runtimes survive to do cleanup), so we have to push + * this determination out to an external sweeper like cron. + * + */ +public class SessionCleanupServlet extends HttpServlet { + + static final String SESSION_ENTITY_TYPE = "_ah_SESSION"; + static final String EXPIRES_PROP = "_expires"; + + // N.B.: This must be less than 500, which is the maximum + // number of entities that may occur in a single bulk delete call. + static final int MAX_SESSION_COUNT = 100; + + private DatastoreService datastore; + + @Override + public void init() { + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) { + if ("clear".equals(request.getQueryString())) { + clearAll(response); + } else { + sendForm(request.getRequestURI() + "?clear", response); + } + } + + private void clearAll(HttpServletResponse response) { + Query query = new Query(SESSION_ENTITY_TYPE); + query.setKeysOnly(); + query.addFilter(EXPIRES_PROP, Query.FilterOperator.LESS_THAN, + System.currentTimeMillis()); + ArrayList killList = new ArrayList(); + Iterable entities = datastore.prepare(query).asIterable( + FetchOptions.Builder.withLimit(MAX_SESSION_COUNT)); + for (Entity expiredSession : entities) { + Key key = expiredSession.getKey(); + killList.add(key); + } + datastore.delete(killList); + response.setStatus(HttpServletResponse.SC_OK); + try { + response.getWriter().println("Cleared " + killList.size() + " expired sessions."); + } catch (IOException ex) { + // We still did the work, and successfully... just send an empty body. + } + } + + private void sendForm(String actionUrl, HttpServletResponse response) { + Query query = new Query(SESSION_ENTITY_TYPE); + query.setKeysOnly(); + query.addFilter(EXPIRES_PROP, Query.FilterOperator.LESS_THAN, + System.currentTimeMillis()); + int count = datastore.prepare(query).countEntities(); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + try { + PrintWriter writer = response.getWriter(); + writer.println("Codestin Search App"); + writer.println("There are currently " + count + " expired sessions."); + writer.println("

    "); + writer.println(""); + writer.println("
    "); + } catch (IOException ex) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + try { + response.getWriter().println(ex); + } catch (IOException innerEx) { + // we lose notifying them what went wrong. + } + } + response.setStatus(HttpServletResponse.SC_OK); + } +} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/SnapshotServlet.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/SnapshotServlet.java new file mode 100644 index 000000000..a7165544c --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/SnapshotServlet.java @@ -0,0 +1,36 @@ +/* + * 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.servlet.jakarta; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Servlet invoked for {@code /_ah/snapshot} requests. Users can override this by providing their + * own mapping for the {@code _ah_snapshot} servlet name. + * + */ +public class SnapshotServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Currently this does nothing. The logic of interest is in the surrounding framework. + } +} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/TransactionCleanupFilter.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/TransactionCleanupFilter.java new file mode 100644 index 000000000..b3ac2dd5c --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/TransactionCleanupFilter.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.apphosting.utils.servlet.jakarta; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Transaction; +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.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A servlet {@link Filter} that looks for datastore transactions that are + * still active when request processing is finished. The filter attempts + * to roll back any transactions that are found, and swallows any exceptions + * that are thrown while trying to perform roll backs. This ensures that + * any problems we encounter while trying to perform roll backs do not have any + * impact on the result returned the user. + * + */ +public class TransactionCleanupFilter implements Filter { + + private static final Logger logger = Logger.getLogger(TransactionCleanupFilter.class.getName()); + + private DatastoreService datastoreService; + + @Override + public void init(FilterConfig filterConfig) { + datastoreService = getDatastoreService(); + } + + @Override + public void destroy() { + datastoreService = null; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + chain.doFilter(request, response); + } finally { + handleAbandonedTxns(datastoreService.getActiveTransactions()); + } + } + + private void handleAbandonedTxns(Collection txns) { + // TODO: In the dev appserver, capture a stack trace whenever a + // transaction is started so we can print it here. + for (Transaction txn : txns) { + String txnId; + try { + // getId() can throw if the beginTransaction() call failed. The rollback() call cleans up + // thread local state (even if it also throws), so it's imperative we actually make the + // call. See http://b/26878109 for details. + txnId = txn.getId(); + } catch (Exception e) { + txnId = "[unknown]"; + } + logger.warning("Request completed without committing or rolling back transaction with id " + + txnId + ". Transaction will be rolled back."); + + try { + txn.rollback(); + } catch (Exception e) { + // We swallow exceptions so that there is no risk of our cleanup + // impacting the actual result of the request. + logger.log(Level.SEVERE, "Swallowing an exception we received while trying to rollback " + + "abandoned transaction.", e); + } + } + } + + // @VisibleForTesting + DatastoreService getDatastoreService() { + // Active transactions are ultimately stored in a thread local, so any instance of the + // DatastoreService is sufficient to access them. Transactions that are active in other threads + // are not cleaned up by this filter. + return DatastoreServiceFactory.getDatastoreService(); + } +} diff --git a/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/WarmupServlet.java b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/WarmupServlet.java new file mode 100644 index 000000000..676da4bd1 --- /dev/null +++ b/api/src/main/java/com/google/apphosting/utils/servlet/jakarta/WarmupServlet.java @@ -0,0 +1,49 @@ +/* + * 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.servlet.jakarta; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.logging.Logger; + +/** + * {@code WarmupServlet} does very little. It primarily serves as a + * placeholder that is mapped to the warmup path (/_ah/warmup) and is + * marked <load-on-startup%gt;. This causes all other + * <load-on-startup%gt; servlets to be initialized during warmup + * requests. + * + */ +public class WarmupServlet extends HttpServlet { + + private static final Logger logger = Logger.getLogger(WarmupServlet.class.getName()); + + @Override + public void init() { + logger.fine("Initializing warm-up servlet."); + } + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) throws IOException { + logger.info("Executing warm-up request."); + // Ensure that all user jars have been processed by looking for a + // nonexistent file. + Thread.currentThread().getContextClassLoader().getResources("_ah_nonexistent"); + } +} diff --git a/api/src/test/java/com/google/appengine/api/mail/stdimpl/GMTransportTest.java b/api/src/test/java/com/google/appengine/api/mail/stdimpl/GMTransportTest.java index 36d9d2ebd..5b5fdc863 100644 --- a/api/src/test/java/com/google/appengine/api/mail/stdimpl/GMTransportTest.java +++ b/api/src/test/java/com/google/appengine/api/mail/stdimpl/GMTransportTest.java @@ -831,6 +831,14 @@ public void testSendWithHeaders() throws Exception { runHeadersTest(headers, headers); } + /** Tests that valid headers can be set. */ + @Test + public void testSendWithListUnsubribeHeaders() throws Exception { + String[] headers = + new String[] {"List-Unsubscribe", "List-Unsubscribe-Post"}; + runHeadersTest(headers, headers); + } + /** Tests that invalid headers are not set. */ @Test public void testSendWithInvalidHeaders() throws Exception { diff --git a/api_dev/pom.xml b/api_dev/pom.xml index 8264b580a..baf2cdc7e 100644 --- a/api_dev/pom.xml +++ b/api_dev/pom.xml @@ -23,11 +23,12 @@ com.google.appengine parent - 2.0.22-SNAPSHOT + 3.0.0-SNAPSHOT jar AppEngine :: appengine-apis-dev + https://github.com/GoogleCloudPlatform/appengine-java-standard/ SDK for dev_appserver (local development) true @@ -44,6 +45,10 @@ appengine-apis + com.google.appengine + appengine-init + + com.google.appengine mediautil @@ -189,6 +194,11 @@ mockito-junit-jupiter test + + jakarta.servlet + jakarta.servlet-api + jar + @@ -204,6 +214,23 @@ + + maven-compiler-plugin + + + + com.google.auto.service + auto-service + 1.1.1 + + + com.google.auto.value + auto-value + 1.11.0 + + + + diff --git a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/BlobUploadSession.java b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/BlobUploadSession.java index d255ce44b..7c4f41486 100644 --- a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/BlobUploadSession.java +++ b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/BlobUploadSession.java @@ -17,17 +17,16 @@ package com.google.appengine.api.blobstore.dev; /** - * {@code BlobUploadSession} is a simple data container that stores - * the state associated with an in-progress upload. - * + * {@code BlobUploadSession} is a simple data container that stores the state associated with an + * in-progress upload. */ -class BlobUploadSession { +public class BlobUploadSession { private final String successPath; private Long maxUploadSizeBytesPerBlob; private Long maxUploadSizeBytes; private String googleStorageBucket; - BlobUploadSession(String successPath) { + public BlobUploadSession(String successPath) { this.successPath = successPath; } diff --git a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/FileBlobStorage.java b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/FileBlobStorage.java index 55effcd3b..36f5384d2 100644 --- a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/FileBlobStorage.java +++ b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/FileBlobStorage.java @@ -24,10 +24,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; /** * {@code FileBlobStorage} provides durable persistence of blobs by storing blob content directly to @@ -46,45 +42,17 @@ class FileBlobStorage implements BlobStorage { @Override public boolean hasBlob(final BlobKey blobKey) { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Boolean run() { - return getFileForBlob(blobKey).exists(); - } - }); + return getFileForBlob(blobKey).exists(); } @Override public OutputStream storeBlob(final BlobKey blobKey) throws IOException { - try { - return AccessController.doPrivileged( - new PrivilegedExceptionAction() { - @Override - public FileOutputStream run() throws IOException { - return new FileOutputStream(getFileForBlob(blobKey)); - } - }); - } catch (PrivilegedActionException ex) { - Throwable cause = ex.getCause(); - throw (cause instanceof IOException) ? (IOException) cause : new IOException(cause); - } + return new FileOutputStream(getFileForBlob(blobKey)); } @Override public InputStream fetchBlob(final BlobKey blobKey) throws IOException { - try { - return AccessController.doPrivileged( - new PrivilegedExceptionAction() { - @Override - public FileInputStream run() throws IOException { - return new FileInputStream(getFileForBlob(blobKey)); - } - }); - } catch (PrivilegedActionException ex) { - Throwable cause = ex.getCause(); - throw (cause instanceof IOException) ? (IOException) cause : new IOException(cause); - } + return new FileInputStream(getFileForBlob(blobKey)); } @Override @@ -98,21 +66,9 @@ public void deleteBlob(final BlobKey blobKey) throws IOException { && blobInfoStorage.loadGsFileInfo(blobKey) == null) { throw new RuntimeException("Unknown blobkey: " + blobKey); } - try { - AccessController.doPrivileged( - new PrivilegedExceptionAction() { - @Override - public Void run() throws IOException { - File file = getFileForBlob(blobKey); - if (!file.delete()) { - throw new IOException("Could not delete: " + file); - } - return null; - } - }); - } catch (PrivilegedActionException ex) { - Throwable cause = ex.getCause(); - throw (cause instanceof IOException) ? (IOException) cause : new IOException(cause); + File file = getFileForBlob(blobKey); + if (!file.delete()) { + throw new IOException("Could not delete: " + file); } blobInfoStorage.deleteBlobInfo(blobKey); } diff --git a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/LocalBlobstoreService.java b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/LocalBlobstoreService.java index aa51fbef1..9656c9671 100644 --- a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/LocalBlobstoreService.java +++ b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/LocalBlobstoreService.java @@ -43,8 +43,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -157,23 +155,18 @@ public CreateUploadURLResponse createUploadURL(Status status, CreateUploadURLReq } public VoidProto deleteBlob(Status status, final DeleteBlobRequest request) { - AccessController.doPrivileged( - (PrivilegedAction) - () -> { - for (String blobKeyString : request.getBlobKeyList()) { - BlobKey blobKey = new BlobKey(blobKeyString); - if (blobStorage.hasBlob(blobKey)) { - try { - blobStorage.deleteBlob(blobKey); - } catch (IOException ex) { - logger.log(Level.WARNING, "Could not delete blob: " + blobKey, ex); - throw new ApiProxy.ApplicationException( - BlobstoreServiceError.ErrorCode.INTERNAL_ERROR_VALUE, ex.toString()); - } - } - } - return null; - }); + for (String blobKeyString : request.getBlobKeyList()) { + BlobKey blobKey = new BlobKey(blobKeyString); + if (blobStorage.hasBlob(blobKey)) { + try { + blobStorage.deleteBlob(blobKey); + } catch (IOException ex) { + logger.log(Level.WARNING, "Could not delete blob: " + blobKey, ex); + throw new ApiProxy.ApplicationException( + BlobstoreServiceError.ErrorCode.INTERNAL_ERROR_VALUE, ex.toString()); + } + } + } return VoidProto.getDefaultInstance(); } @@ -222,27 +215,21 @@ public FetchDataResponse fetchData(Status status, final FetchDataRequest request } else { // Safe to cast because index will never be above MAX_BLOB_FETCH_SIZE. final byte[] data = new byte[(int) (endIndex - request.getStartIndex() + 1)]; - AccessController.doPrivileged( - (PrivilegedAction) - () -> { - try { - boolean swallowDueToThrow = true; - InputStream stream = blobStorage.fetchBlob(blobKey); - try { - ByteStreams.skipFully(stream, request.getStartIndex()); - ByteStreams.readFully(stream, data); - swallowDueToThrow = false; - } finally { - Closeables.close(stream, swallowDueToThrow); - } - } catch (IOException ex) { - logger.log(Level.WARNING, "Could not fetch data: " + blobKey, ex); - throw new ApiProxy.ApplicationException( - BlobstoreServiceError.ErrorCode.INTERNAL_ERROR_VALUE, ex.toString()); - } - - return null; - }); + try { + boolean swallowDueToThrow = true; + InputStream stream = blobStorage.fetchBlob(blobKey); + try { + ByteStreams.skipFully(stream, request.getStartIndex()); + ByteStreams.readFully(stream, data); + swallowDueToThrow = false; + } finally { + Closeables.close(stream, swallowDueToThrow); + } + } catch (IOException ex) { + logger.log(Level.WARNING, "Could not fetch data: " + blobKey, ex); + throw new ApiProxy.ApplicationException( + BlobstoreServiceError.ErrorCode.INTERNAL_ERROR_VALUE, ex.toString()); + } response.setData(ByteString.copyFrom(data)); } diff --git a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/UploadBlobServlet.java b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/UploadBlobServlet.java index d4166529d..dbfd1b082 100644 --- a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/UploadBlobServlet.java +++ b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/UploadBlobServlet.java @@ -34,11 +34,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.security.AccessController; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -126,24 +123,7 @@ public void init() throws ServletException { @Override public void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { - try { - AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public Object run() throws ServletException, IOException { - handleUpload(req, resp); - return null; - } - }); - } catch (PrivilegedActionException ex) { - Throwable cause = ex.getCause(); - if (cause instanceof ServletException) { - throw (ServletException) cause; - } else if (cause instanceof IOException) { - throw (IOException) cause; - } else { - throw new ServletException(cause); - } - } + handleUpload(req, resp); } private String getSessionId(HttpServletRequest req) { diff --git a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/jakarta/ServeBlobFilter.java b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/jakarta/ServeBlobFilter.java new file mode 100644 index 000000000..e7c317c4b --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/jakarta/ServeBlobFilter.java @@ -0,0 +1,308 @@ +/* + * 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.dev.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.RangeFormatException; +import com.google.appengine.api.blobstore.dev.BlobInfoStorage; +import com.google.appengine.api.blobstore.dev.BlobStorage; +import com.google.appengine.api.blobstore.dev.BlobStorageFactory; +import com.google.appengine.api.blobstore.dev.LocalBlobstoreService; +import com.google.appengine.tools.development.ApiProxyLocal; +import com.google.common.io.Closeables; +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.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +/** + * {@code ServeBlobFilter} implements the ability to serve a blob in + * the development environment. In production, the {@code + * X-AppEngine-BlobKey} header is intercepted above the runtime and + * turned into a streaming response. However, in the development + * environment we need to implement this in-process. + * + */ +public final class ServeBlobFilter implements Filter { + private static final Logger logger = Logger.getLogger( + ServeBlobFilter.class.getName()); + + static final String SERVE_HEADER = "X-AppEngine-BlobKey"; + static final String BLOB_RANGE_HEADER = "X-AppEngine-BlobRange"; + static final String CONTENT_RANGE_HEADER = "Content-range"; + static final String RANGE_HEADER = "Range"; + static final String CONTENT_TYPE_HEADER = "Content-type"; + static final String CONTENT_RANGE_FORMAT = "bytes %d-%d/%d"; + private static final int BUF_SIZE = 4096; + + private BlobStorage blobStorage; + private BlobInfoStorage blobInfoStorage; + private ApiProxyLocal apiProxyLocal; + + @Override + public void init(FilterConfig config) { + blobInfoStorage = BlobStorageFactory.getBlobInfoStorage(); + apiProxyLocal = (ApiProxyLocal) config.getServletContext().getAttribute( + "com.google.appengine.devappserver.ApiProxyLocal"); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse) response); + chain.doFilter(request, wrapper); + + BlobKey blobKey = wrapper.getBlobKey(); + if (blobKey != null) { + serveBlob(blobKey, wrapper.hasContentType(), (HttpServletRequest)request, wrapper); + } + } + + @Override + public void destroy() { + } + + private BlobStorage getBlobStorage() { + if (blobStorage == null) { + // N.B.: We need to make sure that the blobstore stub + // has been initialized and has had a chance to initialize + // BlobStorageFactory using its properties. + apiProxyLocal.getService(LocalBlobstoreService.PACKAGE); + + blobStorage = BlobStorageFactory.getBlobStorage(); + } + return blobStorage; + } + + private void calculateContentRange(BlobInfo blobInfo, + HttpServletRequest request, + HttpServletResponse response) throws RangeFormatException { + ResponseWrapper responseWrapper = (ResponseWrapper) response; + String contentRangeHeader = request.getHeader(CONTENT_RANGE_HEADER); + long blobSize = blobInfo.getSize(); + String rangeHeader = responseWrapper.getBlobRangeHeader(); + if (rangeHeader != null) { + if (rangeHeader.isEmpty()) { + response.setHeader(BLOB_RANGE_HEADER, null); + rangeHeader = null; + } + } else { + rangeHeader = request.getHeader(RANGE_HEADER); + } + + if (rangeHeader != null) { + ByteRange byteRange = ByteRange.parse(rangeHeader); + if (byteRange.hasEnd()) { + contentRangeHeader = String.format(CONTENT_RANGE_FORMAT, + byteRange.getStart(), + byteRange.getEnd(), + blobSize); + } else { + long contentRangeStart; + if (byteRange.getStart() >= 0) { + contentRangeStart = byteRange.getStart(); + } else { + contentRangeStart = blobSize + byteRange.getStart(); + } + contentRangeHeader = String.format(CONTENT_RANGE_FORMAT, + contentRangeStart, + blobSize - 1, + blobSize); + } + response.setHeader(CONTENT_RANGE_HEADER, contentRangeHeader); + } + } + + private static void copy(InputStream from, OutputStream to, long size) throws IOException { + byte[] buf = new byte[BUF_SIZE]; + while (size > 0) { + int r = from.read(buf); + if (r == -1) { + return; + } + to.write(buf, 0, (int)Math.min(r, size)); + size -= r; + } + } + + private void serveBlob(BlobKey blobKey, + boolean hasContentType, + HttpServletRequest request, + HttpServletResponse response) + throws IOException { + if (response.isCommitted()) { + logger.severe("Asked to send blob " + blobKey + " but response was already committed."); + return; + } + + // Data presence in info storage is the primary clue of whether this + // is a valid blob. + BlobInfo blobInfo = blobInfoStorage.loadBlobInfo(blobKey); + if (blobInfo == null) { + blobInfo = blobInfoStorage.loadGsFileInfo(blobKey); + } + if (blobInfo == null) { + logger.severe("Could not find blob: " + blobKey); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // And the blob missing from storage is redundant (although for file + // storage could happen if the file was deleted). + if (!getBlobStorage().hasBlob(blobKey)) { + logger.severe("Blob " + blobKey + " missing. Did you delete the file?"); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + if (!hasContentType) { + response.setContentType(getContentType(blobKey)); + } + + try { + calculateContentRange(blobInfo, request, response); + + String contentRange = ((ResponseWrapper)response).getContentRangeHeader(); + long contentLength = blobInfo.getSize(); + long start = 0; + if (contentRange != null) { + ByteRange byteRange = ByteRange.parseContentRange(contentRange); + start = byteRange.getStart(); + contentLength = byteRange.getEnd() - byteRange.getStart() + 1; + response.setStatus(206); + } + response.setHeader("Content-Length", Long.toString(contentLength)); + + boolean swallowDueToThrow = true; + InputStream inStream = getBlobStorage().fetchBlob(blobKey); + try { + OutputStream outStream = response.getOutputStream(); + try { + inStream.skip(start); + copy(inStream, outStream, contentLength); + swallowDueToThrow = false; + } finally { + Closeables.close(outStream, swallowDueToThrow); + } + } finally { + Closeables.close(inStream, swallowDueToThrow); + } + } catch (RangeFormatException ex) { + // Errors become 416, as in production. + response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + + } + + private String getContentType(BlobKey blobKey) { + BlobInfo blobInfo = blobInfoStorage.loadBlobInfo(blobKey); + if (blobInfo != null) { + return blobInfo.getContentType(); + } else { + return "application/octet-stream"; + } + } + + public static class ResponseWrapper extends HttpServletResponseWrapper { + private BlobKey blobKey; + private boolean hasContentType; + private String contentRangeHeader; + private String blobRangeHeader; + + public ResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public void setContentType(String contentType) { + super.setContentType(contentType); + hasContentType = true; + } + + @Override + public void addHeader(String name, String value) { + if (name.equalsIgnoreCase(SERVE_HEADER)) { + blobKey = new BlobKey(value); + } else if( name.equalsIgnoreCase(CONTENT_RANGE_HEADER)) { + contentRangeHeader = value; + super.addHeader(name, value); + } else if( name.equalsIgnoreCase(BLOB_RANGE_HEADER)) { + blobRangeHeader = value; + super.addHeader(name, value); + } else if (name.equalsIgnoreCase(CONTENT_TYPE_HEADER)) { + hasContentType = true; + super.addHeader(name, value); + } else { + super.addHeader(name, value); + } + } + + @Override + public void setHeader(String name, String value) { + if (name.equalsIgnoreCase(SERVE_HEADER)) { + blobKey = new BlobKey(value); + } else if( name.equalsIgnoreCase(CONTENT_RANGE_HEADER)) { + contentRangeHeader = value; + super.setHeader(name, value); + } else if( name.equalsIgnoreCase(BLOB_RANGE_HEADER)) { + blobRangeHeader = value; + } else if (name.equalsIgnoreCase(CONTENT_TYPE_HEADER)) { + hasContentType = true; + super.setHeader(name, value); + } else { + super.setHeader(name, value); + } + } + + @Override + public boolean containsHeader(String name) { + if (name.equals(SERVE_HEADER)) { + return blobKey != null; + } else { + return super.containsHeader(name); + } + } + + public BlobKey getBlobKey() { + return blobKey; + } + + public boolean hasContentType() { + return hasContentType; + } + + public String getContentRangeHeader() { + return contentRangeHeader; + } + + public String getBlobRangeHeader() { + return blobRangeHeader; + } + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/jakarta/UploadBlobServlet.java b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/jakarta/UploadBlobServlet.java new file mode 100644 index 000000000..00d161f96 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/blobstore/dev/jakarta/UploadBlobServlet.java @@ -0,0 +1,493 @@ +/* + * 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.dev.jakarta; + +import static com.google.common.io.BaseEncoding.base64Url; + +import com.google.appengine.api.blobstore.BlobInfo; +import com.google.appengine.api.blobstore.BlobKey; +import com.google.appengine.api.blobstore.dev.BlobInfoStorage; +import com.google.appengine.api.blobstore.dev.BlobStorage; +import com.google.appengine.api.blobstore.dev.BlobStorageFactory; +import com.google.appengine.api.blobstore.dev.BlobUploadSession; +import com.google.appengine.api.blobstore.dev.BlobUploadSessionStorage; +import com.google.appengine.api.blobstore.dev.LocalBlobstoreService; +import com.google.appengine.tools.development.ApiProxyLocal; +import com.google.appengine.tools.development.Clock; +import com.google.apphosting.utils.servlet.jakarta.MultipartMimeUtils; +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Closeables; +// +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +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.activation.DataHandler; +import javax.activation.DataSource; +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.ParseException; + +/** + * {@code UploadBlobServlet} handles blob uploads in the development + * server. The stub implementation of {@link + * com.google.appengine.api.blobstore.BlobstoreService#createUploadUrl} + * returns URLs that are mapped to this servlet. + * + *

    Its primary responsibility is parsing multipart/form-data or + * multipart/mixed requests made by web browsers. To minimize + * dependencies in the SDK, it does using the MimeMultipart class + * included with JavaMail. + * + *

    After the files are extracted from the multipart request body, + * they are assigned {@code BlobKey} values and are committed to local + * storage. The multipart body parts are then replaced with + * message/external-body parts that specify the {@link BlobKey} as + * additional parameters in the Content-type header. + * + */ +public final class UploadBlobServlet extends HttpServlet { + private static final long serialVersionUID = -813190429684600745L; + private static final Logger logger = + Logger.getLogger(UploadBlobServlet.class.getName()); + + 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"; + + static final String UPLOAD_TOO_LARGE_RESPONSE = + "Your client issued a request that was too large."; + + static final String UPLOAD_BLOB_TOO_LARGE_RESPONSE = + UPLOAD_TOO_LARGE_RESPONSE + + " Maximum upload size per blob limit exceeded."; + + static final String UPLOAD_TOTAL_TOO_LARGE_RESPONSE = + UPLOAD_TOO_LARGE_RESPONSE + + " Maximum total upload size limit exceeded."; + + private BlobStorage blobStorage; + private BlobInfoStorage blobInfoStorage; + private BlobUploadSessionStorage uploadSessionStorage; + private SecureRandom secureRandom; + private ApiProxyLocal apiProxyLocal; + + @Override + public void init() throws ServletException { + super.init(); + blobStorage = BlobStorageFactory.getBlobStorage(); + blobInfoStorage = BlobStorageFactory.getBlobInfoStorage(); + uploadSessionStorage = new BlobUploadSessionStorage(); + secureRandom = new SecureRandom(); + apiProxyLocal = (ApiProxyLocal) getServletContext().getAttribute( + "com.google.appengine.devappserver.ApiProxyLocal"); + } + + @Override + public void doPost(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + handleUpload(req, resp); + } + + private String getSessionId(HttpServletRequest req) { + return req.getPathInfo().substring(1); + } + + private Map getInfoFromStorage(BlobKey key, BlobUploadSession uploadSession) { + BlobInfo blobInfo = blobInfoStorage.loadBlobInfo(key); + Map info = new HashMap(6); + info.put("key", key.getKeyString()); + info.put("content-type", blobInfo.getContentType()); + info.put("creation-date", new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss.SSS").format(blobInfo.getCreation())); + info.put("filename", blobInfo.getFilename()); + info.put("size", Long.toString(blobInfo.getSize())); + info.put("md5-hash", blobInfo.getMd5Hash()); + + if (uploadSession.hasGoogleStorageBucketName()) { + String encoded = key.getKeyString() + .substring(LocalBlobstoreService.GOOGLE_STORAGE_KEY_PREFIX.length()); + String decoded = new String(base64Url().omitPadding().decode(encoded)); + info.put("gs-name", decoded); + } + + return info; + } + + // + @SuppressWarnings("InputStreamSlowMultibyteRead") + private void handleUpload(final HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String sessionId = getSessionId(req); + BlobUploadSession session = uploadSessionStorage.loadSession(sessionId); + + if (session == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No upload session: " + sessionId); + return; + } + + Map> blobKeys = new HashMap>(); + Map>> blobInfos = + new HashMap>>(); + final Map> otherParams = new HashMap>(); + try { + MimeMultipart multipart = MultipartMimeUtils.parseMultipartRequest(req); + int parts = multipart.getCount(); + + // Check blob sizes upfront so we don't need to worry about rolling back + // partial uploads. + if (session.hasMaxUploadSizeBytes() || session.hasMaxUploadSizeBytesPerBlob()) { + int totalSize = 0; + int largestBlobSize = 0; + for (int i = 0; i < parts; i++) { + BodyPart part = multipart.getBodyPart(i); + if (part.getFileName() != null && !part.getFileName().isEmpty()) { + int size = part.getSize(); + if (size != -1) { + totalSize += size; + largestBlobSize = Math.max(size, largestBlobSize); + } else { + logger.log(Level.WARNING, + "Unable to determine size of upload part named " + + part.getFileName() + "." + + " Upload limit checks may not be accurate."); + } + } + } + if (session.hasMaxUploadSizeBytesPerBlob() && + session.getMaxUploadSizeBytesPerBlob() < largestBlobSize) { + resp.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, + UPLOAD_BLOB_TOO_LARGE_RESPONSE); + return; + } + if (session.hasMaxUploadSizeBytes() && + session.getMaxUploadSizeBytes() < totalSize) { + resp.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, + UPLOAD_TOTAL_TOO_LARGE_RESPONSE); + return; + } + } + + for (int i = 0; i < parts; i++) { + BodyPart part = multipart.getBodyPart(i); + String fieldName = MultipartMimeUtils.getFieldName(part); + if (part.getFileName() != null) { + if (part.getFileName().length() > 0) { + BlobKey blobKey = assignBlobKey(session); + List keys = blobKeys.get(fieldName); + if (keys == null) { + keys = new ArrayList(); + blobKeys.put(fieldName, keys); + } + keys.add(blobKey.getKeyString()); + + MessageDigest digest = MessageDigest.getInstance("MD5"); + boolean swallowDueToThrow = true; + OutputStream outStream = getBlobStorage().storeBlob(blobKey); + try { + InputStream inStream = part.getInputStream(); + try { + final int bufferSize = (1 << 16); + byte [] buffer = new byte[bufferSize]; + while (true) { + int bytesRead = inStream.read(buffer); + if (bytesRead == -1) { + break; + } + outStream.write(buffer, 0, bytesRead); + digest.update(buffer, 0, bytesRead); + } + outStream.close(); + byte[] hash = digest.digest(); + + StringBuilder hashString = new StringBuilder(); + for (int j = 0; j < hash.length; j++) { + String hexValue = Integer.toHexString(0xFF & hash[j]); + if (hexValue.length() == 1) { + hashString.append("0"); + } + hashString.append(hexValue); + } + + String originalContentType = part.getContentType(); + String newContentType = createContentType(blobKey); + DataSource dataSource = MultipartMimeUtils.createDataSource( + newContentType, new byte[0]); + part.setDataHandler(new DataHandler(dataSource)); + part.addHeader("Content-type", newContentType); + Clock clock = apiProxyLocal.getClock(); + blobInfoStorage.saveBlobInfo(new BlobInfo( + blobKey, + originalContentType, + new Date(clock.getCurrentTime()), + part.getFileName(), + part.getSize(), + hashString.toString())); + swallowDueToThrow = false; + } finally { + Closeables.close(inStream, swallowDueToThrow); + } + } finally { + Closeables.close(outStream, swallowDueToThrow); + } + + // This codes must be run after the BlobInfo is persisted locally. + List> infos = blobInfos.get(fieldName); + if (infos == null) { + infos = new ArrayList>(); + blobInfos.put(fieldName, infos); + } + infos.add(getInfoFromStorage(blobKey, session)); + } + } else { + List values = otherParams.get(fieldName); + if (values == null) { + values = new ArrayList(); + otherParams.put(fieldName, values); + } + values.add(MultipartMimeUtils.getTextContent(part)); + } + } + req.setAttribute(UPLOADED_BLOBKEY_ATTR, blobKeys); + req.setAttribute(UPLOADED_BLOBINFO_ATTR, blobInfos); + + uploadSessionStorage.deleteSession(sessionId); + + ByteArrayOutputStream modifiedRequest = new ByteArrayOutputStream(); + String oldValue = System.setProperty("mail.mime.foldtext", "false"); + try { + multipart.writeTo(modifiedRequest); + } finally { + if (oldValue == null) { + System.clearProperty("mail.mime.foldtext"); + } else { + System.setProperty("mail.mime.foldtext", oldValue); + } + } + + final byte[] modifiedRequestBytes = modifiedRequest.toByteArray(); + final ByteArrayInputStream modifiedRequestStream = + new ByteArrayInputStream(modifiedRequestBytes); + final BufferedReader modifiedReader = + new BufferedReader(new InputStreamReader(modifiedRequestStream)); + + HttpServletRequest wrappedRequest = + new HttpServletRequestWrapper(req) { + @Override + public String getHeader(String name) { + if (Ascii.equalsIgnoreCase(name, UPLOAD_HEADER)) { + return "true"; + } else if (Ascii.equalsIgnoreCase(name, "Content-Length")) { + return String.valueOf(modifiedRequestBytes.length); + } else { + return super.getHeader(name); + } + } + + @Override + public Enumeration getHeaderNames() { + List headers = Collections.list(super.getHeaderNames()); + headers.add(UPLOAD_HEADER); + return Collections.enumeration(headers); + } + + @Override + public Enumeration getHeaders(String name) { + if (Ascii.equalsIgnoreCase(name, UPLOAD_HEADER)) { + return Collections.enumeration(ImmutableList.of("true")); + } else if (Ascii.equalsIgnoreCase(name, "Content-Length")) { + return Collections.enumeration( + ImmutableList.of(String.valueOf(modifiedRequestBytes.length))); + } else { + return super.getHeaders(name); + } + } + + @Override + public int getIntHeader(String name) { + if (Ascii.equalsIgnoreCase(name, UPLOAD_HEADER)) { + throw new NumberFormatException(UPLOAD_HEADER + "does not have an integer value"); + } else if (Ascii.equalsIgnoreCase(name, "Content-Length")) { + return modifiedRequestBytes.length; + } else { + return super.getIntHeader(name); + } + } + + @Override + public ServletInputStream getInputStream() { + return new ServletInputStream() { + @Override + public int read() { + return modifiedRequestStream.read(); + } + + @Override + public void close() throws IOException { + modifiedRequestStream.close(); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public BufferedReader getReader() { + return modifiedReader; + } + + @Override + public Map getParameterMap() { + Map parameters = super.getParameterMap(); + if (otherParams.isEmpty()) { + return parameters; + } else { + // HttpServlet.getParameterMap() result is immutable so we need to take a copy. + Map map = new HashMap<>(parameters); + for (Map.Entry> entry : otherParams.entrySet()) { + map.put(entry.getKey(), entry.getValue().toArray(new String[0])); + } + // Maintain the semantic of ServletRequestWrapper by returning + // an immutable map. + return Collections.unmodifiableMap(map); + } + } + + @Override + public Enumeration getParameterNames() { + List allNames = new ArrayList<>(); + + Enumeration names = super.getParameterNames(); + while (names.hasMoreElements()) { + allNames.add(names.nextElement()); + } + allNames.addAll(otherParams.keySet()); + return Collections.enumeration(allNames); + } + + @Override + public String[] getParameterValues(String name) { + if (otherParams.containsKey(name)) { + return otherParams.get(name).toArray(new String[0]); + } else { + return super.getParameterValues(name); + } + } + + @Override + public String getParameter(String name) { + if (otherParams.containsKey(name)) { + return otherParams.get(name).get(0); + } else { + return super.getParameter(name); + } + } + }; + + String successPath = session.getSuccessPath(); + getServletContext().getRequestDispatcher(successPath).forward(wrappedRequest, + resp); + } catch (MessagingException | NoSuchAlgorithmException ex) { + throw new ServletException(ex); + } + } + + private BlobStorage getBlobStorage() { + if (blobStorage == null) { + // N.B.: We need to make sure that the blobstore stub + // has been initialized and has had a chance to initialize + // BlobStorageFactory using its properties. + apiProxyLocal.getService(LocalBlobstoreService.PACKAGE); + + blobStorage = BlobStorageFactory.getBlobStorage(); + } + return blobStorage; + } + + private String createContentType(BlobKey blobKey) throws ParseException { + ContentType contentType = new ContentType("message/external-body"); + contentType.setParameter("blob-key", blobKey.getKeyString()); + return contentType.toString(); + } + + /** + * Generate a random string to use as a blob key. + */ + private BlobKey assignBlobKey(BlobUploadSession session) { + // Python does this by generating an MD5 digest from a random + // floating point number and the current time stamp. Since + // SecureRandom is already doing something cryptographically + // secure and mixing in the current time, we should be able to get + // by with just base64 encoding the random bytes directly. We use + // the same number of bytes as Python, however (MD5 outputs 128 + // bits). + byte[] bytes = new byte[16]; + secureRandom.nextBytes(bytes); + String objectName = base64Url().omitPadding().encode(bytes); + // If this object is to be uploaded direct to a Google Storage bucket then + // the BlobKey needs to be of the same format as what is generated by + // LocalBlobstoreService.createEncodedGoogleStorageKey + if (session.hasGoogleStorageBucketName()) { + String fullName = "/gs/" + session.getGoogleStorageBucketName() + "/" + objectName; + String encodedName = base64Url().omitPadding().encode(fullName.getBytes()); + objectName = LocalBlobstoreService.GOOGLE_STORAGE_KEY_PREFIX + encodedName; + } + return new BlobKey(objectName); + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/EntityGroupPseudoKind.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/EntityGroupPseudoKind.java index 636e2adcc..3856481a2 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/EntityGroupPseudoKind.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/EntityGroupPseudoKind.java @@ -29,7 +29,7 @@ import com.google.storage.onestore.v3.OnestoreEntity.PropertyValue; import com.google.storage.onestore.v3.OnestoreEntity.Reference; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Pseudo-kind that returns metadata about an entity group. diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/KeyFilteredPseudoKind.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/KeyFilteredPseudoKind.java index e2650ca85..d5d4e3a7f 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/KeyFilteredPseudoKind.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/KeyFilteredPseudoKind.java @@ -30,7 +30,7 @@ import com.google.storage.onestore.v3.OnestoreEntity.EntityProto; import com.google.storage.onestore.v3.OnestoreEntity.Reference; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Generic pseudo-kind for pseudo-kinds that only understand filtering on __key__ and ordering by diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalCompositeIndexManager.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalCompositeIndexManager.java index d019eb11c..11a3868b8 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalCompositeIndexManager.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalCompositeIndexManager.java @@ -52,8 +52,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Writer; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -68,7 +66,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; // CAUTION: this is one of several files that implement parsing and @@ -339,20 +337,13 @@ private File getGeneratedIndexFile() { /** * Returns an input stream for the generated indexes file or {@code null} if it doesn't exist. */ - // @Nullable // @VisibleForTesting - InputStream getGeneratedIndexFileInputStream() { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public InputStream run() { - try { - return new FileInputStream(getGeneratedIndexFile()); - } catch (FileNotFoundException e) { - return null; - } - } - }); + @Nullable InputStream getGeneratedIndexFileInputStream() { + try { + return new FileInputStream(getGeneratedIndexFile()); + } catch (FileNotFoundException e) { + return null; + } } /** Returns a writer for the generated indexes file. */ diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreCostAnalysis.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreCostAnalysis.java index ec4f48593..f8a18dfd0 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreCostAnalysis.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreCostAnalysis.java @@ -35,7 +35,7 @@ import java.math.BigDecimal; import java.util.List; import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility class that can calculate the cost of writing (put or delete) a given {@link Entity}. diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreJob.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreJob.java index b5ee84bc3..4d06a107f 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreJob.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreJob.java @@ -20,7 +20,7 @@ import com.google.apphosting.datastore.DatastoreV3Pb.Cost; import com.google.storage.onestore.v3.OnestoreEntity.EntityProto; import com.google.storage.onestore.v3.OnestoreEntity.Reference; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Represents a job in the local datastore, which is a unit of transactional work to be performed diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreService.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreService.java index 7f6986cb6..f0c45b21e 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreService.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/LocalDatastoreService.java @@ -114,12 +114,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; -import java.security.AccessController; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -143,7 +139,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * A local implementation of the Datastore. @@ -410,21 +406,14 @@ private static ScheduledThreadPoolExecutor createScheduler() { .setNameFormat("LocalDatastoreService-%d") .build()); scheduler.setRemoveOnCancelPolicy(true); - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Object run() { - Runtime.getRuntime() - .addShutdownHook( - new Thread() { - @Override - public void run() { - cleanupActiveServices(); - } - }); - return null; - } - }); + Runtime.getRuntime() + .addShutdownHook( + new Thread() { + @Override + public void run() { + cleanupActiveServices(); + } + }); return scheduler; } @@ -633,14 +622,7 @@ private static int parseInt(String valStr, int defaultVal, String propName) { } public void start() { - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Object run() { - startInternal(); - return null; - } - }); + startInternal(); } private synchronized void startInternal() { @@ -657,8 +639,8 @@ public void run() { removeStaleQueries(clock.getCurrentTime()); } }, - maxQueryLifetimeMs * 5, - maxQueryLifetimeMs * 5, + maxQueryLifetimeMs * 5L, + maxQueryLifetimeMs * 5L, TimeUnit.MILLISECONDS)); scheduledTasks.add( @@ -669,8 +651,8 @@ public void run() { removeStaleTransactions(clock.getCurrentTime()); } }, - maxTransactionLifetimeMs * 5, - maxTransactionLifetimeMs * 5, + maxTransactionLifetimeMs * 5L, + maxTransactionLifetimeMs * 5L, TimeUnit.MILLISECONDS)); if (!noStorage) { @@ -1225,29 +1207,23 @@ public QueryResult runQuery(Status status, Query query) { String app = query.getApp(); Profile profile = getOrCreateProfile(app); - // The real datastore supports executing ancestor queries in transactions. - // For now we're just going to make sure the entity group of the ancestor - // is the same entity group with which the transaction is associated and - // skip providing a transactionally consistent result set. - synchronized (profile) { - // Having a transaction implies we have an ancestor, but having an - // ancestor does not imply we have a transaction. - if (query.hasTransaction() || query.hasAncestor()) { - // Query can only have a txn if it is an ancestor query. Either way we - // know we've got an ancestor. + + if (query.hasTransaction()) { + if (!app.equals(query.getTransaction().getApp())) { + throw newError( + ErrorCode.INTERNAL_ERROR, + "Can't query app " + + app + + "in a transaction on app " + + query.getTransaction().getApp()); + } + } + + if (query.hasAncestor()) { Path groupPath = getGroup(query.getAncestor()); Profile.EntityGroup eg = profile.getGroup(groupPath); if (query.hasTransaction()) { - if (!app.equals(query.getTransaction().getApp())) { - throw newError( - ErrorCode.INTERNAL_ERROR, - "Can't query app " - + app - + "in a transaction on app " - + query.getTransaction().getApp()); - } - LiveTxn liveTxn = profile.getTxn(query.getTransaction().getHandle()); // this will throw an exception if we attempt to read from // the wrong entity group @@ -1256,12 +1232,10 @@ public QueryResult runQuery(Status status, Query query) { profile = eg.getSnapshot(liveTxn); } - if (query.hasAncestor()) { - if (query.hasTransaction() || !query.hasFailoverMs()) { - // Either we have a transaction or the user has requested strongly - // consistent results. Either way, we need to apply jobs. - eg.rollForwardUnappliedJobs(); - } + if (query.hasTransaction() || !query.hasFailoverMs()) { + // Either we have a transaction or the user has requested strongly + // consistent results. Either way, we need to apply jobs. + eg.rollForwardUnappliedJobs(); } } @@ -1402,16 +1376,8 @@ public boolean apply(EntityProto entity) { // store the query and return the results LiveQuery liveQuery = new LiveQuery(queryEntities, versions, query, entityComparator, clock); - // CompositeIndexManager does some filesystem reads/writes, so needs to - // be privileged. - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Object run() { - LocalCompositeIndexManager.getInstance().processQuery(validatedQuery.getV3Query()); - return null; - } - }); + // CompositeIndexManager does some filesystem reads/writes + LocalCompositeIndexManager.getInstance().processQuery(validatedQuery.getV3Query()); // Using next function to prefetch results and return them from runQuery QueryResult result = @@ -3224,32 +3190,25 @@ Map getSpecialPropertyMap() { private void persist() { globalLock.writeLock().lock(); try { - AccessController.doPrivileged( - new PrivilegedExceptionAction() { - @Override - public Object run() throws IOException { - if (noStorage || !dirty) { - return null; - } + if (noStorage || !dirty) { + return; + } - long start = clock.getCurrentTime(); - try (ObjectOutputStream objectOut = - new ObjectOutputStream( - new BufferedOutputStream(new FileOutputStream(backingStore)))) { - objectOut.writeLong(-CURRENT_STORAGE_VERSION); - objectOut.writeLong(entityIdSequential.get()); - objectOut.writeLong(entityIdScattered.get()); - objectOut.writeObject(profiles); - } + long start = clock.getCurrentTime(); + try (ObjectOutputStream objectOut = + new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(backingStore)))) { + objectOut.writeLong(-CURRENT_STORAGE_VERSION); + objectOut.writeLong(entityIdSequential.get()); + objectOut.writeLong(entityIdScattered.get()); + objectOut.writeObject(profiles); + } - dirty = false; - long end = clock.getCurrentTime(); + dirty = false; + long end = clock.getCurrentTime(); - logger.log(Level.INFO, "Time to persist datastore: " + (end - start) + " ms"); - return null; - } - }); - } catch (PrivilegedActionException e) { + logger.log(Level.INFO, "Time to persist datastore: " + (end - start) + " ms"); + + } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof IOException) { logger.log(Level.SEVERE, "Unable to save the datastore", e); @@ -3269,7 +3228,7 @@ public Object run() throws IOException { * @return The number of queries removed. */ int expireOutstandingQueries() { - return removeStaleQueries(maxQueryLifetimeMs * 2 + clock.getCurrentTime()); + return removeStaleQueries(maxQueryLifetimeMs * 2L + clock.getCurrentTime()); } /** @@ -3298,7 +3257,7 @@ private int removeStaleQueries(long currentTime) { * @return The number of transactions removed. */ int expireOutstandingTransactions() { - return removeStaleTransactions(maxTransactionLifetimeMs * 2 + clock.getCurrentTime()); + return removeStaleTransactions(maxTransactionLifetimeMs * 2L + clock.getCurrentTime()); } /** diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKind.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKind.java index 715b7210c..21d195263 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKind.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKind.java @@ -22,7 +22,7 @@ import com.google.storage.onestore.v3.OnestoreEntity.EntityProto; import com.google.storage.onestore.v3.OnestoreEntity.Reference; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * A virtual datastore kind implemented programmatically. Each kind is identified by a name; diff --git a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKinds.java b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKinds.java index b65e6e5cc..62b722f4f 100644 --- a/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKinds.java +++ b/api_dev/src/main/java/com/google/appengine/api/datastore/dev/PseudoKinds.java @@ -27,7 +27,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Container for all known pseudo-kinds. diff --git a/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalBlobImageServlet.java b/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalBlobImageServlet.java index 593be79b8..6b8ab5e72 100644 --- a/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalBlobImageServlet.java +++ b/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalBlobImageServlet.java @@ -28,8 +28,6 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -248,82 +246,74 @@ private ParsedUrl() { /** * Transforms the given image specified in the {@code ParseUrl} argument. * - * Applies all the requested resize and crop operations to a valid image. + *

    Applies all the requested resize and crop operations to a valid image. * * @param request a valid {@code ParseUrl} instance - * * @return the transformed image in an Image class - * @throws ApiProxy.ApplicationException If the image cannot be opened, - * encoded, or if the transform is malformed + * @throws ApiProxy.ApplicationException If the image cannot be opened, encoded, or if the + * transform is malformed */ protected Image transformImage(final ParsedUrl request) { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Image run() { - // Obtain the image bytes as a BufferedImage - Status unusedStatus = new Status(); - ImageData imageData = - ImageData.newBuilder() - .setBlobKey(request.getBlobKey()) - .setContent(ByteString.EMPTY) - .build(); + // Obtain the image bytes as a BufferedImage + Status unusedStatus = new Status(); + ImageData imageData = + ImageData.newBuilder() + .setBlobKey(request.getBlobKey()) + .setContent(ByteString.EMPTY) + .build(); - String originalMimeType = imagesService.getMimeType(imageData); - BufferedImage img = imagesService.openImage(imageData, unusedStatus); + String originalMimeType = imagesService.getMimeType(imageData); + BufferedImage img = imagesService.openImage(imageData, unusedStatus); - // Apply the transform - if (request.hasOptions()) { - // Crop - if (request.getCrop()) { - Transform.Builder cropXform = null; - float width = img.getWidth(); - float height = img.getHeight(); - if (width > height) { - cropXform = Transform.newBuilder(); - float delta = (width - height) / (width * 2.0f); - cropXform.setCropLeftX(delta); - cropXform.setCropRightX(1.0f - delta); - } else if (width < height) { - cropXform = Transform.newBuilder(); - float delta = (height - width) / (height * 2.0f); - float topDelta = Math.max(0.0f, delta - 0.25f); - float bottomDelta = 1.0f - (2.0f * delta) + topDelta; - cropXform.setCropTopY(topDelta); - cropXform.setCropBottomY(bottomDelta); - } - if (cropXform != null) { - img = imagesService.processTransform(img, cropXform.build(), unusedStatus); - } - } + // Apply the transform + if (request.hasOptions()) { + // Crop + if (request.getCrop()) { + Transform.Builder cropXform = null; + float width = img.getWidth(); + float height = img.getHeight(); + if (width > height) { + cropXform = Transform.newBuilder(); + float delta = (width - height) / (width * 2.0f); + cropXform.setCropLeftX(delta); + cropXform.setCropRightX(1.0f - delta); + } else if (width < height) { + cropXform = Transform.newBuilder(); + float delta = (height - width) / (height * 2.0f); + float topDelta = Math.max(0.0f, delta - 0.25f); + float bottomDelta = 1.0f - (2.0f * delta) + topDelta; + cropXform.setCropTopY(topDelta); + cropXform.setCropBottomY(bottomDelta); + } + if (cropXform != null) { + img = imagesService.processTransform(img, cropXform.build(), unusedStatus); + } + } - // Resize - Transform resizeXform = - Transform.newBuilder() - .setWidth(request.getResize()) - .setHeight(request.getResize()) - .build(); - img = imagesService.processTransform(img, resizeXform, unusedStatus); - } else if (img.getWidth() > DEFAULT_SERVING_SIZE - || img.getHeight() > DEFAULT_SERVING_SIZE) { - // Resize down to default serving size. - Transform resizeXform = - Transform.newBuilder() - .setWidth(DEFAULT_SERVING_SIZE) - .setHeight(DEFAULT_SERVING_SIZE) - .build(); - img = imagesService.processTransform(img, resizeXform, unusedStatus); - } + // Resize + Transform resizeXform = + Transform.newBuilder() + .setWidth(request.getResize()) + .setHeight(request.getResize()) + .build(); + img = imagesService.processTransform(img, resizeXform, unusedStatus); + } else if (img.getWidth() > DEFAULT_SERVING_SIZE || img.getHeight() > DEFAULT_SERVING_SIZE) { + // Resize down to default serving size. + Transform resizeXform = + Transform.newBuilder() + .setWidth(DEFAULT_SERVING_SIZE) + .setHeight(DEFAULT_SERVING_SIZE) + .build(); + img = imagesService.processTransform(img, resizeXform, unusedStatus); + } - MIME_TYPE outputMimeType = MIME_TYPE.JPEG; - String outputMimeTypeString = "image/jpeg"; - if (transcodeToPng.contains(originalMimeType)) { - outputMimeType = MIME_TYPE.PNG; - outputMimeTypeString = "image/png"; - } - return new Image( - imagesService.saveImage(img, outputMimeType, unusedStatus), outputMimeTypeString); - } - }); + MIME_TYPE outputMimeType = MIME_TYPE.JPEG; + String outputMimeTypeString = "image/jpeg"; + if (transcodeToPng.contains(originalMimeType)) { + outputMimeType = MIME_TYPE.PNG; + outputMimeTypeString = "image/png"; + } + return new Image( + imagesService.saveImage(img, outputMimeType, unusedStatus), outputMimeTypeString); } } diff --git a/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalImagesService.java b/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalImagesService.java index ee0d27f8d..6d45317dc 100644 --- a/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalImagesService.java +++ b/api_dev/src/main/java/com/google/appengine/api/images/dev/LocalImagesService.java @@ -61,8 +61,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -173,94 +171,88 @@ public void stop() {} */ public ImagesTransformResponse transform( final Status status, final ImagesTransformRequest request) { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public ImagesTransformResponse run() { - BufferedImage img = openImage(request.getImage(), status); - if (request.getTransformCount() > ImagesService.MAX_TRANSFORMS_PER_REQUEST) { - // TODO: Do we need to set both fields *and* throw an - // exception? - status.setSuccessful(false); - status.setErrorCode(ErrorCode.BAD_TRANSFORM_DATA.getNumber()); - throw new ApiProxy.ApplicationException( - ErrorCode.BAD_TRANSFORM_DATA.getNumber(), - String.format( - "%d transforms were supplied; the maximum allowed is %d.", - request.getTransformCount(), ImagesService.MAX_TRANSFORMS_PER_REQUEST)); - } - int orientation = 1; - if (request.getInput().getCorrectExifOrientation() - == ORIENTATION_CORRECTION_TYPE.CORRECT_ORIENTATION) { - Exif exif = getExifMetadata(request.getImage()); - if (exif != null) { - Entry entry = exif.getTagValue(Exif.ORIENTATION, true); - if (entry != null) { - orientation = ((Integer) entry.getValue(0)).intValue(); - if (img.getHeight() > img.getWidth()) { - orientation = 1; - } - } - } - } - for (Transform transform : request.getTransformList()) { - // In production, orientation correction is done during the first - // transform. If the first transform is a crop or flip it is done - // after, otherwise it is done before. To be precise, the order - // of transformation within a single entry is: Crop, Flip, - // Rotate, Resize, (Crop-to-fit), Effects (e.g., autolevels). - // Orientation fix is done within the chain modifying flipping - // and rotation steps. - if (orientation != 1 - && !(transform.hasCropRightX() - || transform.hasCropTopY() - || transform.hasCropBottomY() - || transform.hasCropLeftX()) - && !transform.hasHorizontalFlip() - && !transform.hasVerticalFlip()) { - img = correctOrientation(img, status, orientation); - orientation = 1; - } - if (transform.getAllowStretch() && transform.getCropToFit()) { - // Process allow stretch first and then process the crop. - // This is similar to how it works in production and allows us - // to keep the dev processing pipeline straightforward for this - // combination of transforms. - Transform.Builder stretch = Transform.newBuilder(); - stretch - .setWidth(transform.getWidth()) - .setHeight(transform.getHeight()) - .setAllowStretch(true); - img = processTransform(img, stretch.build(), status); - // Create and process the new crop portion of the transform. - Transform.Builder crop = Transform.newBuilder(); - crop.setWidth(transform.getWidth()) - .setHeight(transform.getHeight()) - .setCropToFit(transform.getCropToFit()) - .setCropOffsetX(transform.getCropOffsetX()) - .setCropOffsetY(transform.getCropOffsetY()) - .setAllowStretch(false); - img = processTransform(img, crop.build(), status); - } else { - img = processTransform(img, transform, status); - } - if (orientation != 1) { - img = correctOrientation(img, status, orientation); - orientation = 1; - } - } - status.setSuccessful(true); - ImageData imageData = - ImageData.newBuilder() - .setContent( - ByteString.copyFrom( - saveImage(img, request.getOutput().getMimeType(), status))) - .setWidth(img.getWidth()) - .setHeight(img.getHeight()) - .build(); - return ImagesTransformResponse.newBuilder().setImage(imageData).build(); + BufferedImage img = openImage(request.getImage(), status); + if (request.getTransformCount() > ImagesService.MAX_TRANSFORMS_PER_REQUEST) { + // TODO: Do we need to set both fields *and* throw an + // exception? + status.setSuccessful(false); + status.setErrorCode(ErrorCode.BAD_TRANSFORM_DATA.getNumber()); + throw new ApiProxy.ApplicationException( + ErrorCode.BAD_TRANSFORM_DATA.getNumber(), + String.format( + "%d transforms were supplied; the maximum allowed is %d.", + request.getTransformCount(), ImagesService.MAX_TRANSFORMS_PER_REQUEST)); + } + int orientation = 1; + if (request.getInput().getCorrectExifOrientation() + == ORIENTATION_CORRECTION_TYPE.CORRECT_ORIENTATION) { + Exif exif = getExifMetadata(request.getImage()); + if (exif != null) { + Entry entry = exif.getTagValue(Exif.ORIENTATION, true); + if (entry != null) { + orientation = ((Integer) entry.getValue(0)).intValue(); + if (img.getHeight() > img.getWidth()) { + orientation = 1; } - }); + } + } + } + for (Transform transform : request.getTransformList()) { + // In production, orientation correction is done during the first + // transform. If the first transform is a crop or flip it is done + // after, otherwise it is done before. To be precise, the order + // of transformation within a single entry is: Crop, Flip, + // Rotate, Resize, (Crop-to-fit), Effects (e.g., autolevels). + // Orientation fix is done within the chain modifying flipping + // and rotation steps. + if (orientation != 1 + && !(transform.hasCropRightX() + || transform.hasCropTopY() + || transform.hasCropBottomY() + || transform.hasCropLeftX()) + && !transform.hasHorizontalFlip() + && !transform.hasVerticalFlip()) { + img = correctOrientation(img, status, orientation); + orientation = 1; + } + if (transform.getAllowStretch() && transform.getCropToFit()) { + // Process allow stretch first and then process the crop. + // This is similar to how it works in production and allows us + // to keep the dev processing pipeline straightforward for this + // combination of transforms. + Transform.Builder stretch = Transform.newBuilder(); + stretch + .setWidth(transform.getWidth()) + .setHeight(transform.getHeight()) + .setAllowStretch(true); + img = processTransform(img, stretch.build(), status); + // Create and process the new crop portion of the transform. + Transform.Builder crop = Transform.newBuilder(); + crop.setWidth(transform.getWidth()) + .setHeight(transform.getHeight()) + .setCropToFit(transform.getCropToFit()) + .setCropOffsetX(transform.getCropOffsetX()) + .setCropOffsetY(transform.getCropOffsetY()) + .setAllowStretch(false); + img = processTransform(img, crop.build(), status); + } else { + img = processTransform(img, transform, status); + } + if (orientation != 1) { + img = correctOrientation(img, status, orientation); + orientation = 1; + } + } + status.setSuccessful(true); + ImageData imageData = + ImageData.newBuilder() + .setContent( + ByteString.copyFrom( + saveImage(img, request.getOutput().getMimeType(), status))) + .setWidth(img.getWidth()) + .setHeight(img.getHeight()) + .build(); + return ImagesTransformResponse.newBuilder().setImage(imageData).build(); } /** @@ -270,62 +262,55 @@ public ImagesTransformResponse run() { */ public ImagesCompositeResponse composite( final Status status, final ImagesCompositeRequest request) { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public ImagesCompositeResponse run() { - List images = new ArrayList(request.getImageCount()); - for (int i = 0; i < request.getImageCount(); i++) { - images.add(openImage(request.getImage(i), status)); - } - if (request.getOptionsCount() > ImagesService.MAX_COMPOSITES_PER_REQUEST) { - status.setSuccessful(false); - status.setErrorCode(ErrorCode.BAD_TRANSFORM_DATA.getNumber()); - throw new ApiProxy.ApplicationException(ErrorCode.BAD_TRANSFORM_DATA.getNumber(), - String.format("%d composites were supplied; the maximum allowed is %d.", - request.getOptionsCount(), ImagesService.MAX_COMPOSITES_PER_REQUEST)); - } - int width = request.getCanvas().getWidth(); - int height = request.getCanvas().getHeight(); - int color = request.getCanvas().getColor(); - BufferedImage canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - for (int i = 0; i < height; i++) { - for (int j = 0; j < width; j++) { - canvas.setRGB(j, i, color); - } - } - for (int i = 0; i < request.getOptionsCount(); i++) { - CompositeImageOptions options = request.getOptions(i); - if (options.getSourceIndex() < 0 - || options.getSourceIndex() >= request.getImageCount()) { - throw new ApiProxy.ApplicationException(ErrorCode.BAD_TRANSFORM_DATA.getNumber(), - String.format("Invalid source image index %d", options.getSourceIndex())); - } - processComposite(canvas, options, images.get(options.getSourceIndex()), status); - } - status.setSuccessful(true); - return ImagesCompositeResponse - .newBuilder() - .setImage( - ImageData.newBuilder().setContent(ByteString.copyFrom(saveImage(canvas, request - .getCanvas() - .getOutput() - .getMimeType(), status)))) - .build(); - } - }); + List images = new ArrayList(request.getImageCount()); + for (int i = 0; i < request.getImageCount(); i++) { + images.add(openImage(request.getImage(i), status)); + } + if (request.getOptionsCount() > ImagesService.MAX_COMPOSITES_PER_REQUEST) { + status.setSuccessful(false); + status.setErrorCode(ErrorCode.BAD_TRANSFORM_DATA.getNumber()); + throw new ApiProxy.ApplicationException(ErrorCode.BAD_TRANSFORM_DATA.getNumber(), + String.format("%d composites were supplied; the maximum allowed is %d.", + request.getOptionsCount(), ImagesService.MAX_COMPOSITES_PER_REQUEST)); + } + int width = request.getCanvas().getWidth(); + int height = request.getCanvas().getHeight(); + int color = request.getCanvas().getColor(); + BufferedImage canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + canvas.setRGB(j, i, color); + } + } + for (int i = 0; i < request.getOptionsCount(); i++) { + CompositeImageOptions options = request.getOptions(i); + if (options.getSourceIndex() < 0 + || options.getSourceIndex() >= request.getImageCount()) { + throw new ApiProxy.ApplicationException(ErrorCode.BAD_TRANSFORM_DATA.getNumber(), + String.format("Invalid source image index %d", options.getSourceIndex())); + } + processComposite(canvas, options, images.get(options.getSourceIndex()), status); + } + status.setSuccessful(true); + return ImagesCompositeResponse + .newBuilder() + .setImage( + ImageData.newBuilder().setContent(ByteString.copyFrom(saveImage(canvas, request + .getCanvas() + .getOutput() + .getMimeType(), status)))) + .build(); } /** * Obtains the mime type of the image data. * * @param imageData a reference to the image - * - * @return a string representing the mime type. Valid return values include - * {@code inputFormats} in LocalImagesService.init(). + * @return a string representing the mime type. Valid return values include {@code inputFormats} + * in LocalImagesService.init(). * @throws ApiProxy.ApplicationException If the image cannot be opened */ - String getMimeType(ImageData imageData) { + public String getMimeType(ImageData imageData) { try { boolean swallowDueToThrow = true; ImageInputStream in = ImageIO.createImageInputStream(extractImageData(imageData)); @@ -392,7 +377,7 @@ Exif getExifMetadata(ImageData imageData) { * @return a {@link BufferedImage} of the image. * @throws ApiProxy.ApplicationException If the image cannot be opened. */ - BufferedImage openImage(ImageData imageData, Status status) { + public BufferedImage openImage(ImageData imageData, Status status) { InputStream in = null; try { try { @@ -436,7 +421,7 @@ BufferedImage openImage(ImageData imageData, Status status) { * @return A byte array representing an image. * @throws ApiProxy.ApplicationException If the image cannot be encoded. */ - byte[] saveImage(BufferedImage image, MIME_TYPE mimeType, Status status) { + public byte[] saveImage(BufferedImage image, MIME_TYPE mimeType, Status status) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { if (mimeType == MIME_TYPE.JPEG) { @@ -464,36 +449,30 @@ byte[] saveImage(BufferedImage image, MIME_TYPE mimeType, Status status) { */ public ImagesHistogramResponse histogram( final Status status, final ImagesHistogramRequest request) { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public ImagesHistogramResponse run() { - BufferedImage img = openImage(request.getImage(), status); - int[] red = new int[256]; - int[] green = new int[256]; - int[] blue = new int[256]; - int pixel; - for (int i = 0; i < img.getHeight(); i++) { - for (int j = 0; j < img.getWidth(); j++) { - pixel = img.getRGB(j, i); - // Premultiply by alpha to match thumbnailer. - red[(((pixel >> 16) & 0xff) * ((pixel >> 24) & 0xff)) / 255]++; - green[(((pixel >> 8) & 0xff) * ((pixel >> 24) & 0xff)) / 255]++; - blue[((pixel & 0xff) * ((pixel >> 24) & 0xff)) / 255]++; - } - } - ImagesHistogram.Builder imageHistogram = ImagesHistogram.newBuilder(); - for (int i = 0; i < 256; i++) { - imageHistogram.addRed(red[i]); - imageHistogram.addGreen(green[i]); - imageHistogram.addBlue(blue[i]); - } - return ImagesHistogramResponse - .newBuilder() - .setHistogram(imageHistogram) - .build(); - } - }); + BufferedImage img = openImage(request.getImage(), status); + int[] red = new int[256]; + int[] green = new int[256]; + int[] blue = new int[256]; + int pixel; + for (int i = 0; i < img.getHeight(); i++) { + for (int j = 0; j < img.getWidth(); j++) { + pixel = img.getRGB(j, i); + // Premultiply by alpha to match thumbnailer. + red[(((pixel >> 16) & 0xff) * ((pixel >> 24) & 0xff)) / 255]++; + green[(((pixel >> 8) & 0xff) * ((pixel >> 24) & 0xff)) / 255]++; + blue[((pixel & 0xff) * ((pixel >> 24) & 0xff)) / 255]++; + } + } + ImagesHistogram.Builder imageHistogram = ImagesHistogram.newBuilder(); + for (int i = 0; i < 256; i++) { + imageHistogram.addRed(red[i]); + imageHistogram.addGreen(green[i]); + imageHistogram.addBlue(blue[i]); + } + return ImagesHistogramResponse + .newBuilder() + .setHistogram(imageHistogram) + .build(); } /** @@ -506,46 +485,34 @@ public ImagesHistogramResponse run() { */ public ImagesGetUrlBaseResponse getUrlBase( final Status status, final ImagesGetUrlBaseRequest request) { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public ImagesGetUrlBaseResponse run() { - if (request.getCreateSecureUrl()) { - log.info( - "Secure URLs will not be created using the development " + "application server."); - } - // Detect the image mimetype to see if is a valid image. - ImageData imageData = - ImageData.newBuilder() - .setBlobKey(request.getBlobKey()) - .setContent(ByteString.EMPTY) - .build(); - // getMimeType is validating the blob is an image. - getMimeType(imageData); - // Note I am commenting out the following line - // because experimentats indicates that doing so resolves - // b/7031367 Tests time out with OOMs since 1.7.1 - // TODO Figure out why the following line causes this - // test to take over one minute to finish: - // jt/c/g/dotorg/onetoday/server/offer/selection:FriendsMatchingScorerTest - // addServingUrlEntry(request.getBlobKey()); - return ImagesGetUrlBaseResponse.newBuilder() - .setUrl(hostPrefix + "/_ah/img/" + request.getBlobKey()) - .build(); - } - }); + if (request.getCreateSecureUrl()) { + log.info( + "Secure URLs will not be created using the development " + "application server."); + } + // Detect the image mimetype to see if is a valid image. + ImageData imageData = + ImageData.newBuilder() + .setBlobKey(request.getBlobKey()) + .setContent(ByteString.EMPTY) + .build(); + // getMimeType is validating the blob is an image. + getMimeType(imageData); + // Note I am commenting out the following line + // because experimentats indicates that doing so resolves + // b/7031367 Tests time out with OOMs since 1.7.1 + // TODO Figure out why the following line causes this + // test to take over one minute to finish: + // jt/c/g/dotorg/onetoday/server/offer/selection:FriendsMatchingScorerTest + // addServingUrlEntry(request.getBlobKey()); + return ImagesGetUrlBaseResponse.newBuilder() + .setUrl(hostPrefix + "/_ah/img/" + request.getBlobKey()) + .build(); } public ImagesDeleteUrlBaseResponse deleteUrlBase( final Status status, final ImagesDeleteUrlBaseRequest request) { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public ImagesDeleteUrlBaseResponse run() { - deleteServingUrlEntry(request.getBlobKey()); - return ImagesDeleteUrlBaseResponse.newBuilder().build(); - } - }); + deleteServingUrlEntry(request.getBlobKey()); + return ImagesDeleteUrlBaseResponse.newBuilder().build(); } @Override @@ -594,7 +561,7 @@ BufferedImage correctOrientation(BufferedImage image, Status status, int orienta * @param status RPC status * @return processed image */ - BufferedImage processTransform(BufferedImage image, Transform transform, Status status) { + public BufferedImage processTransform(BufferedImage image, Transform transform, Status status) { AffineTransform affine = null; BufferedImage constraintImage = null; if (transform.hasWidth() || transform.hasHeight()) { diff --git a/api_dev/src/main/java/com/google/appengine/api/images/dev/jakarta/LocalBlobImageServlet.java b/api_dev/src/main/java/com/google/appengine/api/images/dev/jakarta/LocalBlobImageServlet.java new file mode 100644 index 000000000..562d22384 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/images/dev/jakarta/LocalBlobImageServlet.java @@ -0,0 +1,320 @@ +/* + * 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.images.dev.jakarta; + +import com.google.appengine.api.images.ImagesServicePb.ImageData; +import com.google.appengine.api.images.ImagesServicePb.ImagesServiceError.ErrorCode; +import com.google.appengine.api.images.ImagesServicePb.OutputSettings.MIME_TYPE; +import com.google.appengine.api.images.ImagesServicePb.Transform; +import com.google.appengine.api.images.dev.LocalImagesService; +import com.google.appengine.tools.development.ApiProxyLocal; +import com.google.appengine.tools.development.LocalRpcService.Status; +import com.google.apphosting.api.ApiProxy; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.ByteString; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Stubs out dynamic image server. + * + */ +public class LocalBlobImageServlet extends HttpServlet { + private static final long serialVersionUID = -12394724046108259L; + private static final Set transcodeToPng = ImmutableSet.of("png", "gif"); + private LocalImagesService imagesService; + private static final int DEFAULT_SERVING_SIZE = 512; + + @Override + public void init() throws ServletException { + super.init(); + imagesService = getLocalImagesService(); + } + + LocalImagesService getLocalImagesService() { + ApiProxyLocal apiProxyLocal = (ApiProxyLocal) getServletContext().getAttribute( + "com.google.appengine.devappserver.ApiProxyLocal"); + return(LocalImagesService) apiProxyLocal.getService(LocalImagesService.PACKAGE); + } + + /** + * Utility wrapper to return image bytes and its mime type. + */ + protected static class Image { + private byte[] image; + private String mimeType; + + Image(byte[] image, String mimeType) { + this.image = image; + this.mimeType = mimeType; + } + + public byte[] getImage() { + return image; + } + + public String getMimeType() { + return mimeType; + } + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + OutputStream out = resp.getOutputStream(); + try { + ParsedUrl parsedUrl = ParsedUrl.createParsedUrl(req.getRequestURI()); + // TODO: Revisit and possibly re-enable once b/7031367 is understood + // Key key = KeyFactory.createKey(ImagesReservedKinds.BLOB_SERVING_URL_KIND, + // parsedUrl.getBlobKey()); + // try { + // datastoreService.get(key); + // } catch (EntityNotFoundException ex) { + // // Not finding the key is only a warning at this stage to support + // // older apps. + // // TODO: Make this an error by returning SC_NOT_FOUND once + // // this code has been released for a few cycles. + // logger.log(Level.WARNING, "Missing serving URL key for blobKey " + key.toString() + + // ". Ensure that getServingUrl is called before serving a blob."); + // resp.sendError(HttpServletResponse.SC_NOT_FOUND); + // } + Image image = transformImage(parsedUrl); + resp.setContentType(image.getMimeType()); + out.write(image.getImage()); + } finally { + out.close(); + } + } catch (ApiProxy.ApplicationException e) { + ErrorCode code = ErrorCode.forNumber(e.getApplicationError()); + if (code == null) { + code = ErrorCode.UNSPECIFIED_ERROR; + } + switch (code) { + case NOT_IMAGE: + case INVALID_BLOB_KEY: + resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); + break; + default: + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + } + } catch(IllegalArgumentException e) { + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + } catch (IOException e) { + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + /** + * Utility class to parse a Local URL into its component parts. + * + * The Local url format is as follows: + * + * /_ah/img/SomeValidBlobKey[=options] + * + * where options is either "sX" where X is from ParsedUrl.uncroppedSizes or + * "sX-c" where X is from ParsedUrl.croppedSizes. + */ + protected static class ParsedUrl { + private String blobKey; + private String options; + private int resize; + private boolean crop; + private static final Pattern pattern = Pattern.compile( + "/_ah/img/([-\\w:]+)(=[-\\w]+)?"); + private static final Pattern optionsPattern = Pattern.compile( + "^s(\\d+)(-c)?"); + private static final int SIZE_LIMIT = 1600; + + /** + * Checks if the parsed url has options. + */ + public boolean hasOptions() { + if (options == null || options.length() == 0) { + return false; + } + return true; + } + + /** + * Returns the parsed BlobKey. + */ + public String getBlobKey() { + return blobKey; + } + + /** + * Returns the resize option. Only valid if hasOption() is {@code true}. + */ + public int getResize() { + return resize; + } + + /** + * Returns the crop option. Only valid if hasOption() is {@code true}. + */ + public boolean getCrop() { + return crop; + } + + /** + * Creates a {@code ParsedUrl} instance from the given URL. + * + * @param requestUri the requested URL + * + * @return an instance + */ + protected static ParsedUrl createParsedUrl(String requestUri) { + ParsedUrl parsedUrl = new ParsedUrl(); + parsedUrl.parse(requestUri); + return parsedUrl; + } + + /** + * Parses a Local URL to its component parts. + * + * @param requestUri the Local request URL + * @throws IllegalArgumentException for malformed URLs + */ + protected void parse(String requestUri) { + Matcher matcher = pattern.matcher(requestUri); + if (!matcher.matches()) { + throw new IllegalArgumentException("Malformed URL."); + } + blobKey = matcher.group(1); + options = matcher.group(2); + if (options != null && options.startsWith("=")) { + options = options.substring(1); + } + parseOptions(); + } + + /** + * Parses URL options to its component parts. + * + * @throws IllegalArgumentException for malformed options + */ + protected void parseOptions() { + try { + if (!hasOptions()) { + return; + } + Matcher matcher = optionsPattern.matcher(options); + if (!matcher.matches()) { + throw new IllegalArgumentException("Malformed URL Options"); + } + resize = Integer.parseInt(matcher.group(1)); + crop = false; + if (matcher.group(2) != null) { + crop = true; + } + + // Check resize against the allowlist + if (resize > SIZE_LIMIT || resize < 0) { + throw new IllegalArgumentException("Invalid resize"); + } + } catch (NumberFormatException e) { + options = null; + throw new IllegalArgumentException("Invalid resize", e); + } + } + + private ParsedUrl() { + } + } + + /** + * Transforms the given image specified in the {@code ParseUrl} argument. + * + *

    Applies all the requested resize and crop operations to a valid image. + * + * @param request a valid {@code ParseUrl} instance + * @return the transformed image in an Image class + * @throws ApiProxy.ApplicationException If the image cannot be opened, encoded, or if the + * transform is malformed + */ + protected Image transformImage(final ParsedUrl request) { + // Obtain the image bytes as a BufferedImage + Status unusedStatus = new Status(); + ImageData imageData = + ImageData.newBuilder() + .setBlobKey(request.getBlobKey()) + .setContent(ByteString.EMPTY) + .build(); + + String originalMimeType = imagesService.getMimeType(imageData); + BufferedImage img = imagesService.openImage(imageData, unusedStatus); + + // Apply the transform + if (request.hasOptions()) { + // Crop + if (request.getCrop()) { + Transform.Builder cropXform = null; + float width = img.getWidth(); + float height = img.getHeight(); + if (width > height) { + cropXform = Transform.newBuilder(); + float delta = (width - height) / (width * 2.0f); + cropXform.setCropLeftX(delta); + cropXform.setCropRightX(1.0f - delta); + } else if (width < height) { + cropXform = Transform.newBuilder(); + float delta = (height - width) / (height * 2.0f); + float topDelta = Math.max(0.0f, delta - 0.25f); + float bottomDelta = 1.0f - (2.0f * delta) + topDelta; + cropXform.setCropTopY(topDelta); + cropXform.setCropBottomY(bottomDelta); + } + if (cropXform != null) { + img = imagesService.processTransform(img, cropXform.build(), unusedStatus); + } + } + + // Resize + Transform resizeXform = + Transform.newBuilder() + .setWidth(request.getResize()) + .setHeight(request.getResize()) + .build(); + img = imagesService.processTransform(img, resizeXform, unusedStatus); + } else if (img.getWidth() > DEFAULT_SERVING_SIZE || img.getHeight() > DEFAULT_SERVING_SIZE) { + // Resize down to default serving size. + Transform resizeXform = + Transform.newBuilder() + .setWidth(DEFAULT_SERVING_SIZE) + .setHeight(DEFAULT_SERVING_SIZE) + .build(); + img = imagesService.processTransform(img, resizeXform, unusedStatus); + } + + MIME_TYPE outputMimeType = MIME_TYPE.JPEG; + String outputMimeTypeString = "image/jpeg"; + if (transcodeToPng.contains(originalMimeType)) { + outputMimeType = MIME_TYPE.PNG; + outputMimeTypeString = "image/png"; + } + return new Image( + imagesService.saveImage(img, outputMimeType, unusedStatus), outputMimeTypeString); + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/log/dev/LocalLogService.java b/api_dev/src/main/java/com/google/appengine/api/log/dev/LocalLogService.java index 0cd06a27c..b65303c5c 100644 --- a/api_dev/src/main/java/com/google/appengine/api/log/dev/LocalLogService.java +++ b/api_dev/src/main/java/com/google/appengine/api/log/dev/LocalLogService.java @@ -39,7 +39,7 @@ import java.util.Set; import java.util.TimeZone; import java.util.logging.Handler; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Implementation of local log service. diff --git a/api_dev/src/main/java/com/google/appengine/api/search/dev/LocalSearchService.java b/api_dev/src/main/java/com/google/appengine/api/search/dev/LocalSearchService.java index 099d441a3..7a62b1e2a 100644 --- a/api_dev/src/main/java/com/google/appengine/api/search/dev/LocalSearchService.java +++ b/api_dev/src/main/java/com/google/appengine/api/search/dev/LocalSearchService.java @@ -48,9 +48,6 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.nio.charset.StandardCharsets; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -843,17 +840,11 @@ private void clearIndexes(final File indexDirectory) { } else { closeIndexWriters(); try { - AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public Object run() throws IOException { - if (indexDirectory.exists()) { - recursiveDelete(indexDirectory); - } - indexDirectory.mkdirs(); - return null; - } - }); - } catch (PrivilegedActionException e) { + if (indexDirectory.exists()) { + recursiveDelete(indexDirectory); + } + indexDirectory.mkdirs(); + } catch (IOException e) { throw new RuntimeException(e); } dirMap = new LuceneDirectoryMap.FileBased(indexDirectory); diff --git a/api_dev/src/main/java/com/google/appengine/api/search/dev/WordSeparatorAnalyzer.java b/api_dev/src/main/java/com/google/appengine/api/search/dev/WordSeparatorAnalyzer.java index 7b7227aa9..fc85e61cd 100644 --- a/api_dev/src/main/java/com/google/appengine/api/search/dev/WordSeparatorAnalyzer.java +++ b/api_dev/src/main/java/com/google/appengine/api/search/dev/WordSeparatorAnalyzer.java @@ -75,7 +75,7 @@ protected char normalize(char c) { /** Collect characters that are not in our word separator set. */ @Override protected boolean isTokenChar(char c) { - return !LuceneUtils.WORD_SEPARATORS.contains(new Character(c)); + return !LuceneUtils.WORD_SEPARATORS.contains(c); } } diff --git a/api_dev/src/main/java/com/google/appengine/api/taskqueue/dev/LocalTaskQueue.java b/api_dev/src/main/java/com/google/appengine/api/taskqueue/dev/LocalTaskQueue.java index 0c2ea1020..a1d11da80 100644 --- a/api_dev/src/main/java/com/google/appengine/api/taskqueue/dev/LocalTaskQueue.java +++ b/api_dev/src/main/java/com/google/appengine/api/taskqueue/dev/LocalTaskQueue.java @@ -57,8 +57,6 @@ import java.net.Inet6Address; import java.net.InetAddress; import java.nio.file.Paths; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; @@ -69,7 +67,7 @@ import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.impl.StdSchedulerFactory; @@ -272,14 +270,7 @@ void setQueueXml(QueueXml queueXml) { @Override public void start() { - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Object run() { - start_(); - return null; - } - }); + start_(); } private void start_() { @@ -347,14 +338,7 @@ static String getBaseUrl(LocalServerEnvironment localServerEnvironment) { public void stop() { // Avoid removing the shutdownHook while a JVM shutdown is in progress. if (shutdownHook != null) { - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Void run() { - Runtime.getRuntime().removeShutdownHook(shutdownHook); - return null; - } - }); + Runtime.getRuntime().removeShutdownHook(shutdownHook); shutdownHook = null; } stop_(); @@ -511,7 +495,8 @@ public TaskQueueBulkAddResponse bulkAdd(Status status, TaskQueueBulkAddRequest b DevQueue queue = getQueueByName(bulkAddRequestBuilder.getAddRequest(0).getQueueName().toStringUtf8()); - Map chosenNames = new IdentityHashMap<>(); + IdentityHashMap chosenNames = + new IdentityHashMap<>(); boolean errorFound = false; for (TaskQueueAddRequest.Builder addRequest : diff --git a/api_dev/src/main/java/com/google/appengine/api/urlfetch/dev/LocalURLFetchService.java b/api_dev/src/main/java/com/google/appengine/api/urlfetch/dev/LocalURLFetchService.java index 6ee62cdee..997e11700 100644 --- a/api_dev/src/main/java/com/google/appengine/api/urlfetch/dev/LocalURLFetchService.java +++ b/api_dev/src/main/java/com/google/appengine/api/urlfetch/dev/LocalURLFetchService.java @@ -35,13 +35,10 @@ import java.net.ProxySelector; import java.net.SocketTimeoutException; import java.net.URL; -import java.security.AccessController; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; @@ -465,43 +462,26 @@ private HttpResponse doPrivilegedExecute( final HttpRequestBase method, final URLFetchResponse.Builder response) throws IOException { - try { - return AccessController.doPrivileged( - new PrivilegedExceptionAction() { - @Override - public HttpResponse run() throws IOException { - HttpContext context = new BasicHttpContext(); - // Does some thread ops we need to do in a privileged block. - HttpResponse httpResponse; - // TODO: Default behavior reverted to not validating cert for - // 1.4.2 CP due to wildcard cert validation problems. Revert for - // 1.4.4 after we're confident that the new HttpClient has fixed the - // behavior. - if (request.hasMustValidateServerCertificate() - && request.getMustValidateServerCertificate()) { - httpResponse = getValidatingClient().execute(method, context); - } else { - httpResponse = getNonValidatingClient().execute(method, context); - } - response.setStatusCode(httpResponse.getStatusLine().getStatusCode()); - HttpHost lastHost = - (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); - HttpUriRequest lastReq = - (HttpUriRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST); - String lastUrl = lastHost.toURI() + lastReq.getURI(); - if (!lastUrl.equals(method.getURI().toString())) { - response.setFinalUrl(lastUrl); - } - return httpResponse; - } - }); - } catch (PrivilegedActionException e) { - Throwable t = e.getCause(); - if (t instanceof IOException) { - throw (IOException) t; - } - throw new RuntimeException(e); + HttpContext context = new BasicHttpContext(); + // Does some thread ops we need to do in a privileged block. + HttpResponse httpResponse; + // TODO: Default behavior reverted to not validating cert for + // 1.4.2 CP due to wildcard cert validation problems. Revert for + // 1.4.4 after we're confident that the new HttpClient has fixed the + // behavior. + if (request.hasMustValidateServerCertificate() && request.getMustValidateServerCertificate()) { + httpResponse = getValidatingClient().execute(method, context); + } else { + httpResponse = getNonValidatingClient().execute(method, context); + } + response.setStatusCode(httpResponse.getStatusLine().getStatusCode()); + HttpHost lastHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); + HttpUriRequest lastReq = (HttpUriRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST); + String lastUrl = lastHost.toURI() + lastReq.getURI(); + if (!lastUrl.equals(method.getURI().toString())) { + response.setFinalUrl(lastUrl); } + return httpResponse; } boolean isAllowedPort(int port) { diff --git a/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalLoginServlet.java b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalLoginServlet.java new file mode 100644 index 000000000..4825f0a5b --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalLoginServlet.java @@ -0,0 +1,111 @@ +/* + * 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.users.dev.jakarta; + +import com.google.common.html.HtmlEscapers; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * {@code LocalLoginServlet} is the servlet responsible for implementing + * the fake authentication provided by the Development AppServer. + * + *

    This servlet responds to both {@code GET} and {@code POST} + * requests. {@code GET} requests result in a simple HTML form that + * asks for an email address and whether or not the user is an + * administrator. {@code POST} requests expect to receive the output + * from this form, and set a cookie that contains the same data. + * + *

    After the user has been logged in, they are redirected to the URL + * specified in the {@code "continue"} request parameter. + * + */ +public final class LocalLoginServlet extends HttpServlet { + private static final long serialVersionUID = 3436539147212984827L; + + private static final String BLUE_BOX_STYLE = "width: 20em;" + + "margin: 1em auto;" + + "text-align: left;" + + "padding: 0 2em 1.25em 2em;" + + "background-color: #d6e9f8;" + + "border: 2px solid #67a7e3;"; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String continueUrl = req.getParameter("continue"); + if (continueUrl == null) { + continueUrl = ""; + } + String email = "test@example.com"; + String isAdminChecked = ""; + LoginCookieUtils.CookieData cookieData = LoginCookieUtils.getCookieData(req); + if (cookieData != null) { + email = cookieData.getEmail(); + if (cookieData.isAdmin()) { + isAdminChecked = " checked='true'"; + } + } + resp.setContentType("text/html"); + + // TODO: We may want to move this to a JSP. + PrintWriter out = resp.getWriter(); + out.println(""); + out.println(""); + out.println("

    "); + out.printf("
    \n", BLUE_BOX_STYLE); + out.println("

    Not logged in

    "); + out.println("

    "); + out.println(""); + out.printf(" \n", email); + out.println("

    "); + out.println("

    "); + out.printf("\n", isAdminChecked); + out.println(" "); + out.println("

    "); + out.printf("\n", + HtmlEscapers.htmlEscaper().escape(continueUrl)); + out.println("

    "); + out.println(""); + out.println(""); + out.println("

    "); + out.println("
    "); + out.println("
    "); + out.println(""); + out.println(""); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String continueUrl = req.getParameter("continue"); + String email = req.getParameter("email"); + boolean logout = "Log Out".equalsIgnoreCase(req.getParameter("action")); + boolean isAdmin = "on".equalsIgnoreCase(req.getParameter("isAdmin")); + + if (logout) { + LoginCookieUtils.removeCookie(req, resp); + } else { + // Add our fake authentication cookie. + resp.addCookie(LoginCookieUtils.createCookie(email, isAdmin)); + } + + // Redirect the user to their original continue URL. + resp.sendRedirect(continueUrl); + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalLogoutServlet.java b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalLogoutServlet.java new file mode 100644 index 000000000..b7ce61593 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalLogoutServlet.java @@ -0,0 +1,48 @@ +/* + * 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.users.dev.jakarta; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * {@code LocalLogoutServlet} is the servlet responsible for logging + * the current user out of the fake authentication provided by the + * Development AppServer. It does this by removing a cookie used to + * store the authentication data. + * + *

    After the user has been logged out, they are redirected to the URL + * specified in the {@code "continue"} request parameter. + * + */ +public final class LocalLogoutServlet extends HttpServlet { + private static final long serialVersionUID = -1222014300866646022L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + String continueUrl = req.getParameter("continue"); + + // Remove our fake authentication cookie. + LoginCookieUtils.removeCookie(req, resp); + + // Now redirect them to their continue URL. + resp.sendRedirect(continueUrl); + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthAccessTokenServlet.java b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthAccessTokenServlet.java new file mode 100644 index 000000000..bc0ec7fed --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthAccessTokenServlet.java @@ -0,0 +1,53 @@ +/* + * 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.users.dev.jakarta; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * {@code LocalOAuthAccessTokenServlet} is the servlet responsible for + * implementing the access token acquisition step of the fake OAuth + * authentication flow provided by the Development AppServer. + * + */ +public class LocalOAuthAccessTokenServlet extends HttpServlet { + private static final long serialVersionUID = -2295106902703316041L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + handleRequest(resp); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + handleRequest(resp); + } + + // TODO: Validate the incoming request and issue an actual token, + // using the datastore for token storage. + private void handleRequest(HttpServletResponse resp) throws IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("oauth_token=ACCESS_TOKEN"); + resp.getWriter().print("&"); + resp.getWriter().print("oauth_token_secret=ACCESS_TOKEN_SECRET"); + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthAuthorizeTokenServlet.java b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthAuthorizeTokenServlet.java new file mode 100644 index 000000000..e45a38c5c --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthAuthorizeTokenServlet.java @@ -0,0 +1,92 @@ +/* + * 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.users.dev.jakarta; + +import com.google.common.html.HtmlEscapers; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * {@code LocalOAuthAuthorizeTokenServlet} is the servlet responsible for + * implementing the token authorization step of the fake OAuth authentication + * flow provided by the Development AppServer. + *

    + * This serlvet will redirect to the URL specified in the 'oauth_callback' + * parameter after access is granted. It does not currently support callback + * URLs provided during the request token acquisition step. + * + */ +public class LocalOAuthAuthorizeTokenServlet extends HttpServlet { + private static final long serialVersionUID = 1789085416447898108L; + private static final String BLUE_BOX_STYLE = "width: 20em;" + + "margin: 1em auto;" + + "text-align: left;" + + "padding: 0 2em 1.25em 2em;" + + "background-color: #d6e9f8;" + + "font: 13px sans-serif;" + + "border: 2px solid #67a7e3"; + + // TODO: Validate that the token exists. + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String oauthCallback = req.getParameter("oauth_callback"); + if (oauthCallback == null) { + oauthCallback = ""; + } + + // TODO: Move to a JSP? + resp.setContentType("text/html"); + PrintWriter out = resp.getWriter(); + out.println(""); + out.println(""); + out.println("

    "); + out.printf("
    \n", BLUE_BOX_STYLE); + out.println("

    OAuth Access Request

    "); + out.printf("\n", + HtmlEscapers.htmlEscaper().escape(oauthCallback)); + out.println("

    "); + out.println(""); + out.println("

    "); + out.println("
    "); + out.println("
    "); + out.println(""); + out.println(""); + } + + // TODO: Mark the token as approved. + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String oauthCallback = req.getParameter("oauth_callback"); + if (oauthCallback != null && oauthCallback.length() > 0) { + resp.sendRedirect(oauthCallback); + } else { + // TODO: Move to a JSP? + resp.setContentType("text/html"); + PrintWriter out = resp.getWriter(); + out.println(""); + out.println(""); + out.printf("
    \n", BLUE_BOX_STYLE); + out.println("

    OAuth Access Granted

    "); + out.println("
    "); + out.println(""); + out.println(""); + } + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthRequestTokenServlet.java b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthRequestTokenServlet.java new file mode 100644 index 000000000..c9580ee8c --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LocalOAuthRequestTokenServlet.java @@ -0,0 +1,53 @@ +/* + * 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.users.dev.jakarta; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * {@code LocalOAuthRequestTokenServlet} is the servlet responsible for + * implementing the request token acquisition step of the fake OAuth + * authentication flow provided by the Development AppServer. + * + */ +public class LocalOAuthRequestTokenServlet extends HttpServlet { + private static final long serialVersionUID = -4775143023488708165L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + handleRequest(resp); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + handleRequest(resp); + } + + // TODO: Validate the incoming request and issue an actual token, + // using the datastore for token storage. + private void handleRequest(HttpServletResponse resp) throws IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("oauth_token=REQUEST_TOKEN"); + resp.getWriter().print("&"); + resp.getWriter().print("oauth_token_secret=REQUEST_TOKEN_SECRET"); + } +} diff --git a/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LoginCookieUtils.java b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LoginCookieUtils.java new file mode 100644 index 000000000..59156ef31 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/api/users/dev/jakarta/LoginCookieUtils.java @@ -0,0 +1,174 @@ +/* + * 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.users.dev.jakarta; + +// +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * {@code LoginCookieUtils} encapsulates the creation, deletion, and + * parsing of the fake authentication cookie used by the Development + * Appserver to simulate login. + * + */ +public final class LoginCookieUtils { + /** + * The URL path for the authentication cookie. + */ + public static final String COOKIE_PATH = "/"; + + /** + * The name of the authentication cookie. + */ + public static final String COOKIE_NAME = "dev_appserver_login"; + + /** + * The age of the authentication cookie. -1 means the cookie should + * not be persisted to disk, and will be erased when the browser is + * restarted. + */ + private static final int COOKIE_AGE = -1; + + /** + * Create a fake authentication {@link Cookie} with the specified data. + */ + public static Cookie createCookie(String email, boolean isAdmin) { + String userId = encodeEmailAsUserId(email); + + Cookie cookie = new Cookie(COOKIE_NAME, email + ":" + isAdmin + ":" + userId); + cookie.setPath(COOKIE_PATH); + cookie.setMaxAge(COOKIE_AGE); + return cookie; + } + + /** + * Remove the fake authentication {@link Cookie}, if present. + */ + public static void removeCookie(HttpServletRequest req, HttpServletResponse resp) { + Cookie cookie = findCookie(req); + if (cookie != null) { + // The browser doesn't send the original path, but it's part of + // the cookie's identity, so we need to re-set it if we want to + // delete the same cookie. + cookie.setPath(COOKIE_PATH); + + // This causes the cookie to expire immediately (i.e. to be deleted). + cookie.setMaxAge(0); + + // Now we need to send the cookie back to the client so it knows + // we deleted it. + resp.addCookie(cookie); + } + } + + /** + * Parse the fake authentication {@link Cookie}. + * + * @return A parsed {@link CookieData}, or {@code null} if the + * user is not logged in. + */ + public static CookieData getCookieData(HttpServletRequest req) { + Cookie cookie = findCookie(req); + if (cookie == null) { + return null; + } else { + return parseCookie(cookie); + } + } + + // + public static String encodeEmailAsUserId(String email) { + // This is sort of a weird way of doing this, but it matches + // Python. See dev_appserver_login.py, method CreateCookieData + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(email.toLowerCase().getBytes(StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(); + builder.append("1"); + for (byte b : md5.digest()) { + builder.append(String.format("%02d", b & 0xff)); + } + // This is structured differently from its python equivalent, since here + // the substring method is called after prefixing it with a "1". + return builder.toString().substring(0, 21); + } catch (NoSuchAlgorithmException ex) { + return ""; + } + } + + /** + * Parse the specified {@link Cookie} into a {@link CookieData}. + */ + private static CookieData parseCookie(Cookie cookie) { + String value = cookie.getValue(); + String[] parts = value.split(":"); + String userId = null; + if (parts.length > 2) { + userId = parts[2]; + } + return new CookieData(parts[0], Boolean.parseBoolean(parts[1]), userId); + } + + private static Cookie findCookie(HttpServletRequest req) { + Cookie[] cookies = req.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(COOKIE_NAME)) { + return cookie; + } + } + } + return null; + } + + private LoginCookieUtils() { + // Utility class -- do not instantiate. + } + + /** + * {@code CookieData} encapsulates all of the data contained in the + * fake authentication cookie. + */ + public static final class CookieData { + private final String email; + private final boolean isAdmin; + private final String userId; + + CookieData(String email, boolean isAdmin, String userId) { + this.email = email; + this.isAdmin = isAdmin; + this.userId = userId; + } + + public String getEmail() { + return email; + } + + public boolean isAdmin() { + return isAdmin; + } + + public String getUserId() { + return userId; + } + } +} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/AbstractContainerService.java b/api_dev/src/main/java/com/google/appengine/tools/development/AbstractContainerService.java index b0dab180b..63e81ebfb 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/AbstractContainerService.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/AbstractContainerService.java @@ -455,8 +455,8 @@ public static void installLocalInitializationEnvironment(AppEngineWebXml appEngi environment.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + defaultModuleMainPort); ApiProxy.setEnvironmentForCurrentThread(environment); - DevAppServerModulesFilter.injectBackendServiceCurrentApiInfo(backendName, backendInstance, - portMapping); + DevAppServerModulesCommon.injectBackendServiceCurrentApiInfo( + backendName, backendInstance, portMapping); } /** diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ApiProxyLocalImpl.java b/api_dev/src/main/java/com/google/appengine/tools/development/ApiProxyLocalImpl.java index 7eac86349..fb4295320 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/ApiProxyLocalImpl.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ApiProxyLocalImpl.java @@ -26,8 +26,6 @@ import com.google.apphosting.api.ApiProxy.UnknownException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -42,19 +40,16 @@ import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Implements ApiProxy.Delegate such that the requests are dispatched to local service * implementations. Used for both the {@link com.google.appengine.tools.development.DevAppServer} * and for unit testing services. - * */ -class ApiProxyLocalImpl implements ApiProxyLocal, DevServices { +public class ApiProxyLocalImpl implements ApiProxyLocal, DevServices { /** * The maximum size of any given API request. */ @@ -63,7 +58,7 @@ class ApiProxyLocalImpl implements ApiProxyLocal, DevServices { private static final String API_DEADLINE_KEY = "com.google.apphosting.api.ApiProxy.api_deadline_key"; - static final String IS_OFFLINE_REQUEST_KEY = "com.google.appengine.request.offline"; + public static final String IS_OFFLINE_REQUEST_KEY = "com.google.appengine.request.offline"; /** * Implementation of the {@link LocalServiceContext} interface @@ -110,8 +105,7 @@ public LocalRpcService getLocalService(String packageName) { private static final Logger logger = Logger.getLogger(ApiProxyLocalImpl.class.getName()); - private final Map serviceCache = - new ConcurrentHashMap(); + private final Map serviceCache = new ConcurrentHashMap<>(); private final Map methodCache = new ConcurrentHashMap(); final Map latencySimulatorCache = @@ -218,15 +212,9 @@ public Future makeAsyncCall( boolean offline = environment.getAttributes().get(IS_OFFLINE_REQUEST_KEY) != null; boolean success = false; try { - // Despite the name, privilegedCallable() just arranges for this - // callable to be run with the current privileges. - Callable callable = Executors.privilegedCallable(asyncApiCall); - - // Now we need to escalate privileges so we have permission to - // spin up new threads, if necessary. The callable itself will - // run with the previous privileges. - Future resultFuture = AccessController.doPrivileged( - new PrivilegedApiAction(callable, asyncApiCall)); + Callable callable = asyncApiCall; + Future resultFuture = apiExecutor.submit(callable); + success = true; if (context.getLocalServerEnvironment().enforceApiDeadlines()) { long deadlineMillis = (long) (1000.0 * resolveDeadline(packageName, apiConfig, offline)); @@ -276,67 +264,6 @@ private double resolveDeadline( return Math.min(deadline, maxDeadline); } - private class PrivilegedApiAction implements PrivilegedAction> { - - private final Callable callable; - private final AsyncApiCall asyncApiCall; - - PrivilegedApiAction(Callable callable, AsyncApiCall asyncApiCall) { - this.callable = callable; - this.asyncApiCall = asyncApiCall; - } - - @Override - public Future run() { - // TODO: Return something that implements - // ApiProxy.ApiResultFuture so we can attach real wallclock - // time information here (although CPU time is irrelevant). - final Future result = apiExecutor.submit(callable); - return new Future() { - @Override - public boolean cancel(final boolean mayInterruptIfRunning) { - // Cancel may interrupt another thread so we need to escalate privileges to avoid - // sandbox restrictions. - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Boolean run() { - // If we cancel the task before it runs it's up to us to - // release the semaphore. If we cancel the task after it - // runs we know the task released the semaphore. However, - // we can't reliably know the state of the task and it's - // bad news if the semaphore gets released twice. This - // method ensures that the semaphore only gets released once. - asyncApiCall.tryReleaseSemaphore(); - return result.cancel(mayInterruptIfRunning); - } - }); - } - - @Override - public boolean isCancelled() { - return result.isCancelled(); - } - - @Override - public boolean isDone() { - return result.isDone(); - } - - @Override - public byte[] get() throws InterruptedException, ExecutionException { - return result.get(); - } - - @Override - public byte[] get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - return result.get(timeout, unit); - } - }; - } - } - @Override public void setProperty(String serviceProperty, String value) { if (serviceProperty == null) { @@ -570,13 +497,7 @@ public final synchronized LocalRpcService getService(final String pkg) { return cachedService; } - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public LocalRpcService run() { - return startServices(pkg); - } - }); + return startServices(pkg); } @Override diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/BackendServers.java b/api_dev/src/main/java/com/google/appengine/tools/development/BackendServers.java deleted file mode 100644 index 35db7e6a8..000000000 --- a/api_dev/src/main/java/com/google/appengine/tools/development/BackendServers.java +++ /dev/null @@ -1,43 +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.appengine.tools.development; - -import com.google.common.annotations.VisibleForTesting; - -/** - * Controls backend servers configured in appengine-web.xml. Each server is - * started on a separate port. All servers run the same code as the main app. - * - * - */ -public class BackendServers extends AbstractBackendServers { - - - // Singleton so BackendServers can to be accessed from the - // {@ link DevAppServerModulesFilter} configured in the webdefaults.xml file. - // The filter is configured in the xml file to ensure that it runs after the - // StaticFileFilter but before any other filters. - private static BackendServers instance = new BackendServers(); - - public static BackendServers getInstance() { - return instance; - } - - @VisibleForTesting - BackendServers() { - } -} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/AbstractBackendServers.java b/api_dev/src/main/java/com/google/appengine/tools/development/BackendServersBase.java similarity index 94% rename from api_dev/src/main/java/com/google/appengine/tools/development/AbstractBackendServers.java rename to api_dev/src/main/java/com/google/appengine/tools/development/BackendServersBase.java index 91ab70f74..a65b03905 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/AbstractBackendServers.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/BackendServersBase.java @@ -21,12 +21,13 @@ import com.google.appengine.tools.development.AbstractContainerService.PortMappingProvider; import com.google.appengine.tools.development.ApplicationConfigurationManager.ModuleConfigurationHandle; import com.google.appengine.tools.development.InstanceStateHolder.InstanceState; +import com.google.appengine.tools.info.AppengineSdk; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.utils.config.BackendsXml; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import java.io.File; -import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,19 +35,15 @@ import java.util.TreeMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; import java.util.logging.Logger; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; /** - * Controls backend servers configured in appengine-web.xml. Each server is - * started on a separate port. All servers run the same code as the main app. - * - * + * Controls backend servers configured in appengine-web.xml. Each server is started on a separate + * port. All servers run the same code as the main app. */ -public abstract class AbstractBackendServers implements BackendContainer, - LocalServerController, PortMappingProvider { +public class BackendServersBase + implements BackendContainer, LocalServerController, PortMappingProvider { public static final String SYSTEM_PROPERTY_STATIC_PORT_NUM_PREFIX = "com.google.appengine.devappserver."; @@ -67,18 +64,45 @@ public abstract class AbstractBackendServers implements BackendContainer, private ModuleConfigurationHandle moduleConfigurationHandle; private File externalResourceDir; private Map containerConfigProperties; - private Map backendServers = + private ImmutableMap backendServers = ImmutableMap.copyOf(new HashMap()); private Map portMapping = ImmutableMap.copyOf(new HashMap()); // Should not be used until startup() is called. - protected Logger logger = Logger.getLogger(AbstractBackendServers.class.getName()); + protected Logger logger = Logger.getLogger(BackendServersBase.class.getName()); private Map serviceProperties = new HashMap(); // A reference to the devAppServer that initiated this BackendServers instance. private DevAppServer devAppServer; private ApiProxyLocal apiProxyLocal; + // Singleton so BackendServers can to be accessed from the + // {@ link DevAppServerModulesFilter} configured in the webdefaults.xml file. + // The filter is configured in the xml file to ensure that it runs after the + // StaticFileFilter but before any other filters. + private static BackendServersBase instance; + + public static BackendServersBase getInstance() { + if (instance == null) { + try { + instance = + Class.forName(AppengineSdk.getSdk().getBackendServersClassName()) + .asSubclass(BackendServersBase.class) + .getDeclaredConstructor() + .newInstance(); + + } catch (ClassNotFoundException + | IllegalAccessException + | IllegalArgumentException + | InstantiationException + | NoSuchMethodException + | SecurityException + | InvocationTargetException ex) { + Logger.getLogger(BackendServersBase.class.getName()).log(Level.SEVERE, null, ex); + } + } + return instance; + } @Override public void init(String address, ModuleConfigurationHandle moduleConfigurationHandle, @@ -216,7 +240,7 @@ public void configureAll(ApiProxyLocal local) throws Exception { } logger.finer("Found " + servers.size() + " configured backends."); - Map serverMap = Maps.newHashMap(); + Map serverMap = Maps.newHashMap(); for (BackendsXml.Entry entry : servers) { entry = resolveDefaults(entry); @@ -300,18 +324,6 @@ private BackendsXml.Entry resolveDefaults(BackendsXml.Entry entry) { entry.getState() == null ? BackendsXml.State.STOP : entry.getState()); } - /** - * Forward a request to a specific server and instance. This will call the - * specified instance request dispatcher so the request is handled in the - * right server context. - */ - void forwardToServer(String requestedServer, int instance, HttpServletRequest hrequest, - HttpServletResponse hresponse) throws IOException, ServletException { - ServerWrapper server = getServerWrapper(requestedServer, instance); - logger.finest("forwarding request to server: " + server); - server.getContainer().forwardToServer(hrequest, hresponse); - } - /** * This method guards access to servers to limit the number of concurrent * requests. Each request running on a server must acquire a serving permit. @@ -436,7 +448,7 @@ int addToShortestInstanceQueue(String requestedServer) { } } } catch (InterruptedException e) { - logger.finer("interupted while queued at server " + instanceWithShortestQueue); + logger.finer("interrupted while queued at server " + instanceWithShortestQueue); } return -1; } @@ -775,7 +787,7 @@ public void run() { */ boolean acquireServingPermit(int maxWaitTimeInMs) throws InterruptedException { logger.finest( - this + ": accuiring serving permit, available: " + servingQueue.availablePermits()); + this + ": acquiring serving permit, available: " + servingQueue.availablePermits()); return servingQueue.tryAcquire(maxWaitTimeInMs, TimeUnit.MILLISECONDS); } diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/BackendServersEE8.java b/api_dev/src/main/java/com/google/appengine/tools/development/BackendServersEE8.java new file mode 100644 index 000000000..336d38bda --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/tools/development/BackendServersEE8.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; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Controls backend servers configured in appengine-web.xml. Each server is started on a separate + * port. All servers run the same code as the main app. This one is serving javax.servlet based + * applications. + */ +public class BackendServersEE8 extends BackendServersBase { + + /** + * Forward a request to a specific server and instance. This will call the specified instance + * request dispatcher so the request is handled in the right server context. + */ + public void forwardToServer( + String requestedServer, + int instance, + HttpServletRequest hrequest, + HttpServletResponse hresponse) + throws IOException, ServletException { + ServerWrapper server = getServerWrapper(requestedServer, instance); + logger.finest("forwarding request to server: " + server); + ((ContainerServiceEE8) server.getContainer()).forwardToServer(hrequest, hresponse); + } +} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/BackgroundThreadFactory.java b/api_dev/src/main/java/com/google/appengine/tools/development/BackgroundThreadFactory.java index 6ae5be95f..01315d635 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/BackgroundThreadFactory.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/BackgroundThreadFactory.java @@ -17,8 +17,6 @@ package com.google.appengine.tools.development; import com.google.apphosting.api.ApiProxy; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.concurrent.ThreadFactory; import java.util.logging.Level; import java.util.logging.Logger; @@ -55,28 +53,22 @@ public Thread newThread(final Runnable runnable) { LocalEnvironment.getCurrentInstance(), LocalEnvironment.getCurrentPort()); sleepUninterruptably(API_CALL_LATENCY_MS); - return AccessController.doPrivileged( - new PrivilegedAction() { + // TODO: Only allow this to be used from a backend. + Thread thread = + new Thread(runnable) { @Override - public Thread run() { - // TODO: Only allow this to be used from a backend. - Thread thread = - new Thread(runnable) { - @Override - public void run() { - sleepUninterruptably(THREAD_STARTUP_LATENCY_MS); - ApiProxy.setEnvironmentForCurrentThread(environment); - try { - runnable.run(); - } finally { - environment.callRequestEndListeners(); - } - } - }; - System.setProperty("devappserver-thread-" + thread.getName(), "true"); - return thread; + public void run() { + sleepUninterruptably(THREAD_STARTUP_LATENCY_MS); + ApiProxy.setEnvironmentForCurrentThread(environment); + try { + runnable.run(); + } finally { + environment.callRequestEndListeners(); + } } - }); + }; + System.setProperty("devappserver-thread-" + thread.getName(), "true"); + return thread; } final String getAppId() { diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ContainerService.java b/api_dev/src/main/java/com/google/appengine/tools/development/ContainerService.java index 66e4577bb..4ed5b064d 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/ContainerService.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ContainerService.java @@ -20,11 +20,7 @@ import com.google.apphosting.api.ApiProxy; import com.google.apphosting.utils.config.AppEngineWebXml; import java.io.File; -import java.io.IOException; import java.util.Map; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; /** * Provides the backing servlet container support for the {@link DevAppServer}, @@ -131,10 +127,4 @@ LocalServerEnvironment configure(String devAppServerVersion, String address, int */ Map getServiceProperties(); - /** - * Forwards an HttpRequest request to this container. - */ - void forwardToServer(HttpServletRequest hrequest, HttpServletResponse hresponse) - throws IOException, ServletException; - } diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ContainerServiceEE8.java b/api_dev/src/main/java/com/google/appengine/tools/development/ContainerServiceEE8.java new file mode 100644 index 000000000..c5ad6a690 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ContainerServiceEE8.java @@ -0,0 +1,36 @@ +/* + * 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; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Provides the backing servlet container support for the {@link DevAppServer}, as discovered via + * {@link ServiceProvider}. + * + *

    More specifically, this interface encapsulates the interactions between the {@link + * DevAppServer} and the underlying servlet container, which by default uses Jetty. + */ +public interface ContainerServiceEE8 extends ContainerService { + + /** Forwards an HttpRequest request to this container. */ + void forwardToServer(HttpServletRequest hrequest, HttpServletResponse hresponse) + throws IOException, ServletException; +} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ContainerUtils.java b/api_dev/src/main/java/com/google/appengine/tools/development/ContainerUtils.java index 1b62e30a1..02d6566ea 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/ContainerUtils.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ContainerUtils.java @@ -16,12 +16,10 @@ package com.google.appengine.tools.development; +import com.google.appengine.tools.info.AppengineSdk; + /** helper to load a {@link ContainerService} instance */ public class ContainerUtils { - private static final String JETTY9SERVICE = - "com.google.appengine.tools.development.jetty9.JettyContainerService"; - private static final String JETTY12SERVICE = - "com.google.appengine.tools.development.jetty.JettyContainerService"; /** * Load a {@link ContainerService} instance based on the implementation: Jetty9 or Jetty12. @@ -33,28 +31,19 @@ public static ContainerService loadContainer() { ContainerService result; // Try to load the correct Jetty service. - - if (Boolean.getBoolean("appengine.use.jetty12")) { - try { - result = - (ContainerService) - Class.forName(JETTY12SERVICE, true, DevAppServerImpl.class.getClassLoader()) - .newInstance(); - } catch (ReflectiveOperationException e) { - throw new IllegalArgumentException("Cannot load any servlet container.", e); - } - return result; - } else { try { - result = - (ContainerService) - Class.forName(JETTY9SERVICE, true, DevAppServerImpl.class.getClassLoader()) - .newInstance(); + result = + Class.forName( + AppengineSdk.getSdk().getJettyContainerService(), + true, + DevAppServerImpl.class.getClassLoader()) + .asSubclass(ContainerService.class) + .getDeclaredConstructor() + .newInstance(); } catch (ReflectiveOperationException e) { throw new IllegalArgumentException("Cannot load any servlet container.", e); } return result; - } } /** Returns the server info string with the dev-appserver version. */ diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DelegatingModulesFilterHelper.java b/api_dev/src/main/java/com/google/appengine/tools/development/DelegatingModulesFilterHelper.java index 723cf0a40..746f07c93 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/DelegatingModulesFilterHelper.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DelegatingModulesFilterHelper.java @@ -16,21 +16,16 @@ package com.google.appengine.tools.development; -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - /** * A {@link ModulesFilterHelper} for delegating requests to either * {@link BackendServers} for backends or {@link Modules} for module instances. */ public class DelegatingModulesFilterHelper implements ModulesFilterHelper { - private final AbstractBackendServers backendServers; - private final Modules modules; + protected final BackendServersBase backendServers; + protected final Modules modules; - public DelegatingModulesFilterHelper(AbstractBackendServers backendServers, Modules modules) { + public DelegatingModulesFilterHelper(BackendServersBase backendServers, Modules modules) { this.backendServers = backendServers; this.modules = modules; } @@ -99,18 +94,7 @@ public boolean checkInstanceStopped(String moduleOrBackendName, int instance) { return modules.checkInstanceStopped(moduleOrBackendName, instance); } } - - @Override - public void forwardToInstance(String moduleOrBackendName, int instance, - HttpServletRequest hrequest, HttpServletResponse response) - throws IOException, ServletException { - if (isBackend(moduleOrBackendName)) { - backendServers.forwardToServer(moduleOrBackendName, instance, hrequest, response); - } else { - modules.forwardToInstance(moduleOrBackendName, instance, hrequest, response); - } - } - + @Override public boolean isLoadBalancingInstance(String moduleOrBackendName, int instance) { if (isBackend(moduleOrBackendName)) { @@ -120,7 +104,7 @@ public boolean isLoadBalancingInstance(String moduleOrBackendName, int instance) } } - private boolean isBackend(String moduleOrBackendName) { + protected boolean isBackend(String moduleOrBackendName) { return backendServers.checkServerExists(moduleOrBackendName); } diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DelegatingModulesFilterHelperEE8.java b/api_dev/src/main/java/com/google/appengine/tools/development/DelegatingModulesFilterHelperEE8.java new file mode 100644 index 000000000..7cc204177 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DelegatingModulesFilterHelperEE8.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; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** */ +public class DelegatingModulesFilterHelperEE8 extends DelegatingModulesFilterHelper + implements ModulesFilterHelperEE8 { + + public DelegatingModulesFilterHelperEE8(BackendServersBase backendServers, Modules modules) { + super(backendServers, modules); + } + + @Override + public void forwardToInstance( + String moduleOrBackendName, + int instance, + HttpServletRequest hrequest, + HttpServletResponse response) + throws IOException, ServletException { + if (isBackend(moduleOrBackendName)) { + ((BackendServersEE8) backendServers) + .forwardToServer(moduleOrBackendName, instance, hrequest, response); + } else { + ((ModulesEE8) modules).forwardToInstance(moduleOrBackendName, instance, hrequest, response); + } + } +} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerClassLoader.java b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerClassLoader.java index 932ddd1ce..1f1f046e4 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerClassLoader.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerClassLoader.java @@ -60,16 +60,31 @@ class DevAppServerClassLoader extends URLClassLoader { * classes will be loaded (e.g. DevAppServer). */ public static DevAppServerClassLoader newClassLoader(ClassLoader delegate) { + List sharedLibs = AppengineSdk.getSdk().getSharedLibs(); + List implLibs = AppengineSdk.getSdk().getImplLibs(); + List userJspLibs = AppengineSdk.getSdk().getUserJspLibs(); + // NB Doing shared, then impl, in order, allows us to prefer - // returning shared classes when asked by other classloaders. This makes - // it so that we don't have to have the impl and shared classes - // be a strictly disjoint set. - List libs = new ArrayList<>(AppengineSdk.getSdk().getSharedLibs()); - libs.addAll(AppengineSdk.getSdk().getImplLibs()); - // Needed by admin console servlets, which are loaded by this - // ClassLoader - libs.addAll(AppengineSdk.getSdk().getUserJspLibs()); - return new DevAppServerClassLoader(libs.toArray(new URL[libs.size()]), delegate); + // returning shared classes when asked by other classloaders. + // This makes it so that we don't have to have the impl and + // shared classes be a strictly disjoint set. + List libs = new ArrayList<>(sharedLibs); + addLibs(libs, implLibs); + + // Needed by admin console servlets, which are loaded by this ClassLoader. + addLibs(libs, userJspLibs); + + return new DevAppServerClassLoader(libs.toArray(new URL[0]), delegate); + } + + private static void addLibs(List libs, List toAdd) + { + for (URL url : toAdd) + { + if (libs.contains(url)) + continue; + libs.add(url); + } } // NB diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerFactory.java b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerFactory.java index 368b966de..e9041cbb1 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerFactory.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerFactory.java @@ -16,6 +16,10 @@ package com.google.appengine.tools.development; +import com.google.appengine.init.AppEngineWebXmlInitialParse; +import com.google.appengine.tools.info.AppengineSdk; +import com.google.apphosting.utils.config.WebXml; +import com.google.apphosting.utils.config.WebXmlReader; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -31,8 +35,17 @@ public class DevAppServerFactory { static final String DEV_APP_SERVER_CLASS = "com.google.appengine.tools.development.DevAppServerImpl"; - private static final Class[] DEV_APPSERVER_CTOR_ARG_TYPES = {File.class, File.class, - File.class, File.class, String.class, Integer.TYPE, Boolean.TYPE, Map.class, String.class}; + private static final Class[] devAppserverCtorArgTypes = { + File.class, + File.class, + File.class, + File.class, + String.class, + Integer.TYPE, + Boolean.TYPE, + Map.class, + String.class + }; private static final String USER_CODE_CLASSPATH_MANAGER_PROP = "devappserver.userCodeClasspathManager"; @@ -57,7 +70,7 @@ public DevAppServer createDevAppServer(File appDir, String address, int port) { * * @param appDir The top-level directory of the web application to be run * @param externalResourceDir If not {@code null}, a resource directory external to the appDir. - * This paramater is now ignored. + * This parameter is now ignored. * @param address Address to bind to * @param port Port to bind to * @return a {@code DevAppServer} @@ -72,7 +85,7 @@ public DevAppServer createDevAppServer( address, port, true, - /* installSecurityManager*/ false, + /* installSecurityManager= */ false, new HashMap(), false); } @@ -82,7 +95,7 @@ public DevAppServer createDevAppServer( * * @param appDir The top-level directory of the web application to be run * @param externalResourceDir If not {@code null}, a resource directory external to the appDir. - * This paramater is now ignored. + * This parameter is now ignored. * @param address Address to bind to * @param port Port to bind to * @param noJavaAgent whether to disable detection of the Java agent or not @@ -340,23 +353,33 @@ private DevAppServer doCreateDevAppServer( boolean useCustomStreamHandler, Map containerConfigProperties, String applicationId) { - if (webXmlLocation == null) { webXmlLocation = new File(appDir, "WEB-INF/web.xml"); } if (appEngineWebXmlLocation == null) { appEngineWebXmlLocation = new File(appDir, "WEB-INF/appengine-web.xml"); } + new AppEngineWebXmlInitialParse(appEngineWebXmlLocation.getAbsolutePath()) + .handleRuntimeProperties(); + if (Boolean.getBoolean("appengine.use.EE8") + || Boolean.getBoolean("appengine.use.EE10") + || Boolean.getBoolean("appengine.use.EE11")) { + AppengineSdk.resetSdk(); + } + if (webXmlLocation.exists()) { + WebXmlReader webXmlReader = new WebXmlReader(webXmlLocation.getAbsolutePath(), ""); - DevAppServerClassLoader loader = DevAppServerClassLoader.newClassLoader( - DevAppServerFactory.class.getClassLoader()); + WebXml webXml = webXmlReader.readWebXml(); + webXml.validate(); + } + DevAppServerClassLoader loader = + DevAppServerClassLoader.newClassLoader(DevAppServerFactory.class.getClassLoader()); DevAppServer devAppServer; try { Class devAppServerClass = Class.forName(DEV_APP_SERVER_CLASS, false, loader); - - Constructor cons = devAppServerClass.getConstructor(DEV_APPSERVER_CTOR_ARG_TYPES); + Constructor cons = devAppServerClass.getConstructor(devAppserverCtorArgTypes); cons.setAccessible(true); devAppServer = (DevAppServer) @@ -379,5 +402,4 @@ private DevAppServer doCreateDevAppServer( } return devAppServer; } - } diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerImpl.java b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerImpl.java index 187b5fc3f..dc20912ab 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerImpl.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerImpl.java @@ -24,14 +24,11 @@ import com.google.apphosting.utils.config.AppEngineConfigException; import com.google.apphosting.utils.config.EarHelper; import com.google.common.base.Joiner; +import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.File; import java.net.BindException; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; @@ -48,13 +45,11 @@ import java.util.logging.Logger; /** - * {@code DevAppServer} launches a local Jetty server (by default) with a single - * hosted web application. It can be invoked from the command-line by - * providing the path to the directory in which the application resides as the - * only argument. - * + * {@code DevAppServer} launches a local Jetty server (by default) with a single hosted web + * application. It can be invoked from the command-line by providing the path to the directory in + * which the application resides as the only argument. */ -class DevAppServerImpl implements DevAppServer { +public class DevAppServerImpl implements DevAppServer { // Keep this in sync with // com.google.apphosting.tests.usercode.testservlets.LoadOnStartupServlet // .MODULES_FILTER_HELPER_PROPERTY. @@ -77,12 +72,11 @@ enum ServerState { INITIALIZING, RUNNING, STOPPING, SHUTDOWN } private ServerState serverState = ServerState.INITIALIZING; /** - * Contains the backend servers configured as part of the "Servers" feature. - * Each backend server is started on a separate port and keep their own - * internal state. Memcache, datastore, and other API services are shared by - * all servers, including the "main" server. + * Contains the backend servers configured as part of the "Servers" feature. Each backend server + * is started on a separate port and keep their own internal state. Memcache, datastore, and other + * API services are shared by all servers, including the "main" server. */ - private final BackendServers backendContainer; + private final BackendServersBase backendContainer; /** * The api proxy we created when we started the web containers. Not initialized until after @@ -134,8 +128,8 @@ public DevAppServerImpl(File appDir, File externalResourceDir, File webXmlLocati if (useCustomStreamHandler) { StreamHandlerFactory.install(); } - - backendContainer = BackendServers.getInstance(); + + backendContainer = BackendServersBase.getInstance(); requestedPort = port; customApplicationId = applicationId; ApplicationConfigurationManager tempManager = null; @@ -165,8 +159,17 @@ public DevAppServerImpl(File appDir, File externalResourceDir, File webXmlLocati this.modules = Modules.createModules( applicationConfigurationManager, "dev", externalResourceDir, address, this); - DelegatingModulesFilterHelper modulesFilterHelper = - new DelegatingModulesFilterHelper(backendContainer, modules); + + DelegatingModulesFilterHelper modulesFilterHelper; + try { + modulesFilterHelper = + Class.forName(AppengineSdk.getSdk().getDelegatingModulesFilterHelperClassName()) + .asSubclass(DelegatingModulesFilterHelper.class) + .getDeclaredConstructor(BackendServersBase.class, Modules.class) + .newInstance(backendContainer, modules); + } catch (Exception ex) { + throw new VerifyException("Cannot find a DelegatingModulesFilterHelper class", ex); + } this.containerConfigProperties = ImmutableMap.builder() .putAll(requestedContainerConfigProperties) @@ -215,23 +218,14 @@ public Map getServiceProperties() { /** * Starts the server. * - * @throws IllegalStateException If the server has already been started or - * shutdown. + * @throws IllegalStateException If the server has already been started or shutdown. * @throws AppEngineConfigException If no WEB-INF directory can be found or - * WEB-INF/appengine-web.xml does not exist. + * WEB-INF/appengine-web.xml does not exist. * @return a latch that will be decremented to zero when the server is shutdown. */ @Override public CountDownLatch start() throws Exception { - try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override public CountDownLatch run() throws Exception { - return doStart(); - } - }); - } catch (PrivilegedActionException e) { - throw e.getException(); - } + return doStart(); } private CountDownLatch doStart() throws Exception { @@ -367,24 +361,16 @@ public CountDownLatch restart() throws Exception { if (serverState != ServerState.RUNNING) { throw new IllegalStateException("Cannot restart a server that is not currently running."); } - try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override public CountDownLatch run() throws Exception { - modules.shutdown(); - backendContainer.shutdownAll(); - shutdownLatch.countDown(); - modules.createConnections(); - backendContainer.configureAll(apiProxyLocal); - modules.setApiProxyDelegate(apiProxyLocal); - modules.startup(); - backendContainer.startupAll(); - shutdownLatch = new CountDownLatch(1); - return shutdownLatch; - } - }); - } catch (PrivilegedActionException e) { - throw e.getException(); - } + modules.shutdown(); + backendContainer.shutdownAll(); + shutdownLatch.countDown(); + modules.createConnections(); + backendContainer.configureAll(apiProxyLocal); + modules.setApiProxyDelegate(apiProxyLocal); + modules.startup(); + backendContainer.startupAll(); + shutdownLatch = new CountDownLatch(1); + return shutdownLatch; } @Override @@ -392,46 +378,29 @@ public void shutdown() throws Exception { if (serverState != ServerState.RUNNING) { throw new IllegalStateException("Cannot shutdown a server that is not currently running."); } - try { - AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override public Void run() throws Exception { - modules.shutdown(); - backendContainer.shutdownAll(); - ApiProxy.setDelegate(null); - apiProxyLocal = null; - serverState = ServerState.SHUTDOWN; - shutdownLatch.countDown(); - return null; - } - }); - } catch (PrivilegedActionException e) { - throw e.getException(); - } + modules.shutdown(); + backendContainer.shutdownAll(); + ApiProxy.setDelegate(null); + apiProxyLocal = null; + serverState = ServerState.SHUTDOWN; + shutdownLatch.countDown(); } @Override public void gracefulShutdown() throws IllegalStateException { // TODO: Do an actual graceful shutdown rather than just delaying. - // Requires a privileged block since this may be invoked from a servlet - // that lives in the user's classloader and may result in the creation of - // a thread. - AccessController.doPrivileged( - new PrivilegedAction>() { - @Override - public Future run() { - return shutdownScheduler.schedule( - new Callable() { - @Override - public Void call() throws Exception { - shutdown(); - return null; - } - }, - 1000, - TimeUnit.MILLISECONDS); - } - }); + Future unused = + shutdownScheduler.schedule( + new Callable() { + @Override + public Void call() throws Exception { + shutdown(); + return null; + } + }, + 1000, + TimeUnit.MILLISECONDS); } @Override diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerMain.java b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerMain.java index 9aa6ad7d2..5d5980721 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerMain.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerMain.java @@ -137,7 +137,7 @@ public void apply() { } @Override - public List getHelpLines() { + public ImmutableList getHelpLines() { return ImmutableList.of( " --default_gcs_bucket=NAME Set the default Google Cloud Storage bucket" + " name."); @@ -294,6 +294,8 @@ public void run() { TreeSet contextRootNames = new TreeSet<>(); for (String service : services) { File serviceFile = new File(service); + // Set the correct runtimeId from appengine-web.xml. + configureRuntime(serviceFile); fw.write(""); fw.write(""); // Absolute URI for the given service/module. @@ -354,7 +356,7 @@ public void apply() { validateWarPath(appDir); configureRuntime(appDir); - + DevAppServer server = new DevAppServerFactory() .createDevAppServer( diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerModulesCommon.java b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerModulesCommon.java new file mode 100644 index 000000000..bd807c726 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerModulesCommon.java @@ -0,0 +1,185 @@ +/* + * 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; + +import com.google.appengine.api.backends.BackendService; +import com.google.appengine.api.backends.dev.LocalServerController; +import com.google.appengine.api.modules.ModulesException; +import com.google.appengine.api.modules.ModulesService; +import com.google.appengine.api.modules.ModulesServiceFactory; +import com.google.apphosting.api.ApiProxy; +import com.google.common.annotations.VisibleForTesting; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This filter intercepts all request sent to all module instances. + * + *

    There are 6 different request types that this filter will see: + * + *

    * DIRECT_BACKEND_REQUEST: a client request sent to a serving (non load balancing) backend + * instance. + * + *

    * REDIRECT_REQUESTED: a request requesting a redirect in one of three ways 1) The request + * contains a BackendService.REQUEST_HEADER_BACKEND_REDIRECT header or parameter 2) The request is + * sent to a load balancing module instance. 3) The request is sent to a load balancing backend + * instance. + * + *

    If the request specifies an instance with the BackendService.REQUEST_HEADER_INSTANCE_REDIRECT + * request header or parameter the filter verifies that the instance is available, obtains a serving + * permit and forwards the requests. If the instance is not available the filter responds with a 500 + * error. + * + *

    If the request does not specify an instance the filter picks one, obtains a serving permit, + * and forwards the request. If no instance is available this filter responds with a 500 error. + * + *

    * DIRECT_MODULE_REQUEST: a request sent directly to the listening port of a specific serving + * module instance. The filter verifies that the instance is available, obtains a serving permit and + * sends the request to the handler. If no instance is available this filter responds with a 500 + * error. + * + *

    * REDIRECTED_BACKEND_REQUEST: a request redirected to a backend instance. The filter sends the + * request to the handler. The serving permit has already been obtained by this filter when + * performing the redirect. + * + *

    * REDIRECTED_MODULE_REQUEST: a request redirected to a specific module instance. The filter + * sends the request to the handler. The serving permit has already been obtained when by filter + * performing the redirect. + * + *

    * STARTUP_REQUEST: Internally generated startup request. The filter passes the request to the + * handler without obtaining a serving permit. + */ +public class DevAppServerModulesCommon { + + protected static final String BACKEND_REDIRECT_ATTRIBUTE = + "com.google.appengine.backend.BackendName"; + protected static final String BACKEND_INSTANCE_REDIRECT_ATTRIBUTE = + "com.google.appengine.backend.BackendInstance"; + + @VisibleForTesting + protected static final String MODULE_INSTANCE_REDIRECT_ATTRIBUTE = + "com.google.appengine.module.ModuleInstance"; + + protected final BackendServersBase backendServersManager; + protected final ModulesService modulesService; + + protected final Logger logger = Logger.getLogger(DevAppServerModulesFilter.class.getName()); + + @VisibleForTesting + protected DevAppServerModulesCommon( + BackendServersBase backendServers, ModulesService modulesService) { + this.backendServersManager = backendServers; + this.modulesService = modulesService; + } + + public DevAppServerModulesCommon() { + this(BackendServersBase.getInstance(), ModulesServiceFactory.getModulesService()); + } + + protected boolean isLoadBalancingRequest() { + ModulesFilterHelper modulesFilterHelper = getModulesFilterHelper(); + String module = modulesService.getCurrentModule(); + int instance = getCurrentModuleInstance(); + return modulesFilterHelper.isLoadBalancingInstance(module, instance); + } + + protected boolean expectsGeneratedStartRequests(String backendName, int requestPort) { + String moduleOrBackendName = backendName; + if (moduleOrBackendName == null) { + moduleOrBackendName = modulesService.getCurrentModule(); + } + + int instance = + backendName == null + ? getCurrentModuleInstance() + : backendServersManager.getServerInstanceFromPort(requestPort); + ModulesFilterHelper modulesFilterHelper = getModulesFilterHelper(); + return modulesFilterHelper.expectsGeneratedStartRequests(moduleOrBackendName, instance); + } + + /** + * Returns the instance id for the module instance handling the current request or -1 if a back + * end server or load balancing server is handling the request. + */ + protected int getCurrentModuleInstance() { + String instance = "-1"; + try { + instance = modulesService.getCurrentInstanceId(); + } catch (ModulesException me) { + logger.log(Level.FINEST, "Ignoring Exception getting module instance and continuing", me); + } + return Integer.parseInt(instance); + } + + protected ModulesFilterHelper getModulesFilterHelper() { + Map attributes = ApiProxy.getCurrentEnvironment().getAttributes(); + return (ModulesFilterHelper) attributes.get(DevAppServerImpl.MODULES_FILTER_HELPER_PROPERTY); + } + + /** + * Inject information about the current backend server setup so it is available to the + * BackendService API. This information is stored in the threadLocalAttributes in the current + * environment. + * + * @param backendName The server that is handling the request + * @param instance The server instance that is handling the request + */ + protected void injectApiInfo(String backendName, int instance) { + Map portMapping = backendServersManager.getPortMapping(); + if (portMapping == null) { + throw new IllegalStateException("backendServersManager.getPortMapping() is null"); + } + injectBackendServiceCurrentApiInfo(backendName, instance, portMapping); + + Map threadLocalAttributes = ApiProxy.getCurrentEnvironment().getAttributes(); + + // We inject backendServersManager which is not injected by + // injectBackendServiceCurrentApiInfo as it is not needed by BackendsService + // but is needed by the admin console for handling HTTP requests. + if (!portMapping.isEmpty()) { + threadLocalAttributes.put( + LocalServerController.BACKEND_CONTROLLER_ATTRIBUTE_KEY, backendServersManager); + } + + threadLocalAttributes.put( + ModulesController.MODULES_CONTROLLER_ATTRIBUTE_KEY, Modules.getInstance()); + } + + /** Sets up {@link ApiProxy} attributes needed {@link BackendService}. */ + public static void injectBackendServiceCurrentApiInfo( + String backendName, int backendInstance, Map portMapping) { + Map threadLocalAttributes = ApiProxy.getCurrentEnvironment().getAttributes(); + if (backendInstance != -1) { + threadLocalAttributes.put(BackendService.INSTANCE_ID_ENV_ATTRIBUTE, backendInstance + ""); + } + if (backendName != null) { + threadLocalAttributes.put(BackendService.BACKEND_ID_ENV_ATTRIBUTE, backendName); + } + threadLocalAttributes.put(BackendService.DEVAPPSERVER_PORTMAPPING_KEY, portMapping); + } + + @VisibleForTesting + public static enum RequestType { + DIRECT_MODULE_REQUEST, + REDIRECT_REQUESTED, + DIRECT_BACKEND_REQUEST, + REDIRECTED_BACKEND_REQUEST, + REDIRECTED_MODULE_REQUEST, + STARTUP_REQUEST; + } +} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerModulesFilter.java b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerModulesFilter.java index 1afeb29eb..2fb7f0b8a 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerModulesFilter.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/DevAppServerModulesFilter.java @@ -17,16 +17,11 @@ package com.google.appengine.tools.development; import com.google.appengine.api.backends.BackendService; -import com.google.appengine.api.backends.dev.LocalServerController; -import com.google.appengine.api.modules.ModulesException; import com.google.appengine.api.modules.ModulesService; import com.google.appengine.api.modules.ModulesServiceFactory; import com.google.apphosting.api.ApiProxy; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -39,52 +34,41 @@ /** * This filter intercepts all request sent to all module instances. * - * There are 6 different request types that this filter will see: + *

    There are 6 different request types that this filter will see: * - * * DIRECT_BACKEND_REQUEST: a client request sent to a serving (non load balancing) - * backend instance. + *

    * DIRECT_BACKEND_REQUEST: a client request sent to a serving (non load balancing) backend + * instance. * - * * REDIRECT_REQUESTED: a request requesting a redirect in one of three ways - * 1) The request contains a BackendService.REQUEST_HEADER_BACKEND_REDIRECT - * header or parameter - * 2) The request is sent to a load balancing module instance. - * 3) The request is sent to a load balancing backend instance. + *

    * REDIRECT_REQUESTED: a request requesting a redirect in one of three ways 1) The request + * contains a BackendService.REQUEST_HEADER_BACKEND_REDIRECT header or parameter 2) The request is + * sent to a load balancing module instance. 3) The request is sent to a load balancing backend + * instance. * - * If the request specifies an instance with the BackendService.REQUEST_HEADER_INSTANCE_REDIRECT - * request header or parameter the filter verifies that the instance is available, - * obtains a serving permit and forwards the requests. If the instance is not available - * the filter responds with a 500 error. + *

    If the request specifies an instance with the BackendService.REQUEST_HEADER_INSTANCE_REDIRECT + * request header or parameter the filter verifies that the instance is available, obtains a serving + * permit and forwards the requests. If the instance is not available the filter responds with a 500 + * error. * - * If the request does not specify an instance the filter picks one, - * obtains a serving permit, and and forwards the request. If no instance is - * available this filter responds with a 500 error. + *

    If the request does not specify an instance the filter picks one, obtains a serving permit, + * and forwards the request. If no instance is available this filter responds with a 500 error. * - * * DIRECT_MODULE_REQUEST: a request sent directly to the listening port of a - * specific serving module instance. The filter verifies that the instance is - * available, obtains a serving permit and sends the request to the handler. - * If no instance is available this filter responds with a 500 error. + *

    * DIRECT_MODULE_REQUEST: a request sent directly to the listening port of a specific serving + * module instance. The filter verifies that the instance is available, obtains a serving permit and + * sends the request to the handler. If no instance is available this filter responds with a 500 + * error. * - * * REDIRECTED_BACKEND_REQUEST: a request redirected to a backend instance. - * The filter sends the request to the handler. The serving permit has - * already been obtained by this filter when performing the redirect. - * - * * REDIRECTED_MODULE_REQUEST: a request redirected to a specific module instance. - * The filter sends the request to the handler. The serving permit has - * already been obtained when by filter performing the redirect. - * - * * STARTUP_REQUEST: Internally generated startup request. The filter - * passes the request to the handler without obtaining a serving permit. + *

    * REDIRECTED_BACKEND_REQUEST: a request redirected to a backend instance. The filter sends the + * request to the handler. The serving permit has already been obtained by this filter when + * performing the redirect. * + *

    * REDIRECTED_MODULE_REQUEST: a request redirected to a specific module instance. The filter + * sends the request to the handler. The serving permit has already been obtained when by filter + * performing the redirect. * + *

    * STARTUP_REQUEST: Internally generated startup request. The filter passes the request to the + * handler without obtaining a serving permit. */ -public class DevAppServerModulesFilter implements Filter { - - static final String BACKEND_REDIRECT_ATTRIBUTE = "com.google.appengine.backend.BackendName"; - static final String BACKEND_INSTANCE_REDIRECT_ATTRIBUTE = - "com.google.appengine.backend.BackendInstance"; - @VisibleForTesting - static final String MODULE_INSTANCE_REDIRECT_ATTRIBUTE = - "com.google.appengine.module.ModuleInstance"; +public class DevAppServerModulesFilter extends DevAppServerModulesCommon implements Filter { // In prod instances return 500 (Internal Server Error) when busy static final int INSTANCE_BUSY_ERROR_CODE = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; @@ -94,19 +78,13 @@ public class DevAppServerModulesFilter implements Filter { static final int MODULE_MISSING_ERROR_CODE = HttpServletResponse.SC_BAD_GATEWAY; - private final AbstractBackendServers backendServersManager; - private final ModulesService modulesService; - - private final Logger logger = Logger.getLogger(DevAppServerModulesFilter.class.getName()); - @VisibleForTesting - DevAppServerModulesFilter(AbstractBackendServers backendServers, ModulesService modulesService) { - this.backendServersManager = backendServers; - this.modulesService = modulesService; + DevAppServerModulesFilter(BackendServersBase backendServers, ModulesService modulesService) { + super(backendServers, modulesService); } public DevAppServerModulesFilter() { - this(BackendServers.getInstance(), ModulesServiceFactory.getModulesService()); + this(BackendServersBase.getInstance(), ModulesServiceFactory.getModulesService()); } @Override @@ -188,46 +166,7 @@ RequestType getRequestType(HttpServletRequest hrequest) { } } } - - private boolean isLoadBalancingRequest() { - ModulesFilterHelper modulesFilterHelper = getModulesFilterHelper(); - String module = modulesService.getCurrentModule(); - int instance = getCurrentModuleInstance(); - return modulesFilterHelper.isLoadBalancingInstance(module, instance); - } - - private boolean expectsGeneratedStartRequests(String backendName, - int requestPort) { - String moduleOrBackendName = backendName; - if (moduleOrBackendName == null) { - moduleOrBackendName = modulesService.getCurrentModule(); - } - - int instance = backendName == null ? getCurrentModuleInstance() : - backendServersManager.getServerInstanceFromPort(requestPort); - ModulesFilterHelper modulesFilterHelper = getModulesFilterHelper(); - return modulesFilterHelper.expectsGeneratedStartRequests(moduleOrBackendName, instance); - } - - /** - * Returns the instance id for the module instance handling the current request or -1 - * if a back end server or load balancing server is handling the request. - */ - private int getCurrentModuleInstance() { - String instance = "-1"; - try { - instance = modulesService.getCurrentInstanceId(); - } catch (ModulesException me) { - logger.log(Level.FINEST, "Ignoring Exception getting module instance and continuing", me); - } - return Integer.parseInt(instance); - } - - private ModulesFilterHelper getModulesFilterHelper() { - Map attributes = ApiProxy.getCurrentEnvironment().getAttributes(); - return (ModulesFilterHelper) attributes.get(DevAppServerImpl.MODULES_FILTER_HELPER_PROPERTY); - } - + private boolean tryToAcquireServingPermit( String moduleOrBackendName, int instance, HttpServletResponse hresponse) throws IOException { ModulesFilterHelper modulesFilterHelper = getModulesFilterHelper(); @@ -287,7 +226,7 @@ private void doRedirect(HttpServletRequest hrequest, HttpServletResponse hrespon moduleOrBackendName = modulesService.getCurrentModule(); isLoadBalancingModuleInstance = true; } - ModulesFilterHelper modulesFilterHelper = getModulesFilterHelper(); + ModulesFilterHelperEE8 modulesFilterHelper = (ModulesFilterHelperEE8) getModulesFilterHelper(); int instance = getInstanceIdFromRequest(hrequest); logger.finest(String.format("redirect request to module: %d.%s", instance, moduleOrBackendName)); @@ -446,51 +385,6 @@ private void doStartupRequest( public void init(FilterConfig filterConfig) throws ServletException { } - /** - * Inject information about the current backend server setup so it is available - * to the BackendService API. This information is stored in the threadLocalAttributes - * in the current environment. - * - * @param backendName The server that is handling the request - * @param instance The server instance that is handling the request - */ - private void injectApiInfo(String backendName, int instance) { - Map portMapping = backendServersManager.getPortMapping(); - if (portMapping == null) { - throw new IllegalStateException("backendServersManager.getPortMapping() is null"); - } - injectBackendServiceCurrentApiInfo(backendName, instance, portMapping); - - Map threadLocalAttributes = ApiProxy.getCurrentEnvironment().getAttributes(); - - // We inject backendServersManager which is not injected by - // injectBackendServiceCurrentApiInfo as it is not needed by BackendsService - // but is needed by the admin console for handling HTTP requests. - if (!portMapping.isEmpty()) { - threadLocalAttributes.put( - LocalServerController.BACKEND_CONTROLLER_ATTRIBUTE_KEY, backendServersManager); - } - - threadLocalAttributes.put( - ModulesController.MODULES_CONTROLLER_ATTRIBUTE_KEY, - Modules.getInstance()); - } - - /** - * Sets up {@link ApiProxy} attributes needed {@link BackendService}. - */ - public static void injectBackendServiceCurrentApiInfo( - String backendName, int backendInstance, Map portMapping) { - Map threadLocalAttributes = ApiProxy.getCurrentEnvironment().getAttributes(); - if (backendInstance != -1) { - threadLocalAttributes.put(BackendService.INSTANCE_ID_ENV_ATTRIBUTE, backendInstance + ""); - } - if (backendName != null) { - threadLocalAttributes.put(BackendService.BACKEND_ID_ENV_ATTRIBUTE, backendName); - } - threadLocalAttributes.put(BackendService.DEVAPPSERVER_PORTMAPPING_KEY, portMapping); - } - /** * Checks the request headers and request parameters for the specified key */ @@ -523,10 +417,4 @@ static int getInstanceIdFromRequest(HttpServletRequest request) { return -1; } } - - @VisibleForTesting - static enum RequestType { - DIRECT_MODULE_REQUEST, REDIRECT_REQUESTED, DIRECT_BACKEND_REQUEST, REDIRECTED_BACKEND_REQUEST, - REDIRECTED_MODULE_REQUEST, STARTUP_REQUEST; - } } diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/EnvironmentVariableChecker.java b/api_dev/src/main/java/com/google/appengine/tools/development/EnvironmentVariableChecker.java index e44512809..52521b1c6 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/EnvironmentVariableChecker.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/EnvironmentVariableChecker.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; import java.util.logging.Logger; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * Checker for reporting differences between environment variables specified diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHelper.java b/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHelper.java index 25ba46051..0903815e2 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHelper.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHelper.java @@ -71,12 +71,11 @@ public class InstanceHelper { /** * Triggers an HTTP GET to /_ah/start in a background thread * - * This method will keep on trying until it receives a non-error response - * code from the server. + *

    This method will keep on trying until it receives a non-error response code from the server. * * @param runOnSuccess {@link Runnable#run} invoked when the startup request succeeds. */ - void sendStartRequest(final Runnable runOnSuccess) { + public void sendStartRequest(final Runnable runOnSuccess) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Entering send start request for serverOrBackendName=" + serverOrBackendName + " instance=" + instance, @@ -256,12 +255,12 @@ private void triggerLifecycleShutdownHookImpl() { /** * Shut down the server. * - * Will trigger any shutdown hooks installed by the - * {@link com.google.appengine.api.LifecycleManager} + *

    Will trigger any shutdown hooks installed by the {@link + * com.google.appengine.api.LifecycleManager} * * @throws Exception */ - void shutdown() throws Exception { + public void shutdown() throws Exception { synchronized (instanceStateHolder) { // TODO: This calls user code, can we do this outside the synchronized block. if (instanceStateHolder.test(InstanceState.RUNNING, InstanceState.RUNNING_START_REQUEST)) { diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHolder.java b/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHolder.java index 195d5b730..8644a7e7f 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHolder.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/InstanceHolder.java @@ -16,10 +16,8 @@ package com.google.appengine.tools.development; -/** - * Holder for per module instance state. - */ -interface InstanceHolder { +/** Holder for per module instance state. */ +public interface InstanceHolder { /** * Returns the {@link ContainerService} for this instance. diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/InstanceStateHolder.java b/api_dev/src/main/java/com/google/appengine/tools/development/InstanceStateHolder.java index 399723fd2..3361a9239 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/InstanceStateHolder.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/InstanceStateHolder.java @@ -47,7 +47,7 @@ public class InstanceStateHolder { * * STOPPED: Incoming requests get a 500 error response. */ - static enum InstanceState { + public static enum InstanceState { INITIALIZING, SLEEPING, RUNNING_START_REQUEST, RUNNING, STOPPED, SHUTDOWN; } @@ -63,24 +63,22 @@ static enum InstanceState { * @param moduleOrBackendName For module instances the module name and for backend instances the * backend name. * @param instance The instance number or -1 for load balancing instances and automatic module - * instances. + * instances. */ - InstanceStateHolder(String moduleOrBackendName, int instance) { + public InstanceStateHolder(String moduleOrBackendName, int instance) { this.moduleOrBackendName = moduleOrBackendName; this.instance = instance; } /** - * Updates the current instance state and verifies that the previous state is - * what is expected. + * Updates the current instance state and verifies that the previous state is what is expected. * * @param newState The new state to change to * @param acceptablePreviousStates Acceptable previous states - * @throws IllegalStateException If the current state is not one of the - * acceptable previous states + * @throws IllegalStateException If the current state is not one of the acceptable previous states */ - void testAndSet(InstanceState newState, - InstanceState... acceptablePreviousStates) throws IllegalStateException { + public void testAndSet(InstanceState newState, InstanceState... acceptablePreviousStates) + throws IllegalStateException { InstanceState invalidState = testAndSetIf(newState, acceptablePreviousStates); if (invalidState != null) { @@ -119,10 +117,8 @@ synchronized InstanceState testAndSetIf(InstanceState newState, return result; } - /** - * Returns true if current state is one of the provided acceptable states. - */ - synchronized boolean test(InstanceState... acceptableStates) { + /** Returns true if current state is one of the provided acceptable states. */ + public synchronized boolean test(InstanceState... acceptableStates) { for (InstanceState acceptable : acceptableStates) { if (currentState == acceptable) { return true; @@ -148,16 +144,14 @@ synchronized void requireState(String operation, InstanceState... acceptableStat * * @return true if the instance can accept incoming requests, false otherwise. */ - synchronized boolean acceptsConnections() { + public synchronized boolean acceptsConnections() { return (currentState == InstanceState.RUNNING || currentState == InstanceState.RUNNING_START_REQUEST || currentState == InstanceState.SLEEPING); } - /** - * Returns the display name for the current state. - */ - synchronized String getDisplayName() { + /** Returns the display name for the current state. */ + public synchronized String getDisplayName() { return currentState.name().toLowerCase(); } diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/IsolatedAppClassLoader.java b/api_dev/src/main/java/com/google/appengine/tools/development/IsolatedAppClassLoader.java index f214694e1..9ed4723d5 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/IsolatedAppClassLoader.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/IsolatedAppClassLoader.java @@ -26,9 +26,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.security.AccessController; import java.security.CodeSource; -import java.security.PrivilegedAction; import java.util.HashSet; import java.util.Set; import java.util.logging.Level; @@ -46,21 +44,6 @@ public class IsolatedAppClassLoader extends URLClassLoader { private static final Logger logger = Logger.getLogger(IsolatedAppClassLoader.class.getName()); - // Web-default.xml files for Jetty9 based devappserver. - private static final String WEB_DEFAULT_LOCATION_DEVAPPSERVER1 = - "com/google/appengine/tools/development/jetty9/webdefault.xml"; - // Web-default.xml files for Jetty12 based devappserver. - private static final String WEB_DEFAULT_LOCATION_DEVAPPSERVER2 = - "com/google/appengine/tools/development/jetty/webdefault.xml"; - - // This task queue related servlet should be loaded by the application classloader when the - // api jar is used by the application, and default to the runtime classloader when the application - // does not have the API jar in the classpath so that the Jetty container can boot, even if the - // servlet is not used by the application. - // This change is required now with the new Jetty 9.4 classloader which is more strict. - private static final String DEFERRED_TASK_SERVLET = - "com.google.apphosting.utils.servlet.DeferredTaskServlet"; - // Session Data class must be loaded by the runtime classloader, as it is only used by the runtime // servlet session management. For Jetty9.4, the newer session management has a cleaner // classloading implementation. @@ -77,10 +60,7 @@ public IsolatedAppClassLoader(File appRoot, File externalResourceDir, URL[] urls checkWorkingDirectory(appRoot, externalResourceDir); this.devAppServerClassLoader = devAppServerClassLoader; this.sharedCodeLibs = new HashSet<>(AppengineSdk.getSdk().getSharedLibs()); - String webDefault = WEB_DEFAULT_LOCATION_DEVAPPSERVER1; - if (Boolean.getBoolean("appengine.use.jetty12")) { - webDefault = WEB_DEFAULT_LOCATION_DEVAPPSERVER2; - } + String webDefault = AppengineSdk.getSdk().getWebDefaultLocation(); this.classesToBeLoadedByTheRuntimeClassLoader = new ImmutableSet.Builder() .add(SESSION_DATA_CLASS) @@ -169,7 +149,14 @@ public URL getResource(String name) { protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.equals(DEFERRED_TASK_SERVLET)) { + // This task queue related servlet should be loaded by the application classloader when the + // api jar is used by the application, and default to the runtime classloader when the + // application + // does not have the API jar in the classpath so that the Jetty container can boot, even if the + // servlet is not used by the application. + // This change is required now with the new Jetty 9.4 classloader which is more strict. + // "com.google.apphosting.utils.servlet.DeferredTaskServlet" or EE10 related. + if (name.contains("DeferredTaskServlet")) { try { return super.loadClass(name, resolve); } catch (ClassNotFoundException ignore) { @@ -183,13 +170,7 @@ protected synchronized Class loadClass(String name, boolean resolve) final Class c = devAppServerClassLoader.loadClass(name); // See where it came from. - CodeSource source = AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public CodeSource run() { - return c.getProtectionDomain().getCodeSource(); - } - }); + CodeSource source = c.getProtectionDomain().getCodeSource(); // Load classes from the JRE. // We can't just block non-allowlisted classes from being loaded. The JVM diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/LocalEnvironment.java b/api_dev/src/main/java/com/google/appengine/tools/development/LocalEnvironment.java index f92595afa..3b2d1f73d 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/LocalEnvironment.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/LocalEnvironment.java @@ -36,7 +36,7 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * {@code LocalEnvironment} is a simple @@ -54,7 +54,7 @@ abstract public class LocalEnvironment implements ApiProxy.Environment { NamespaceManager.class.getName() + ".appsNamespace"; // Default port for tests that do not specify a port. - public static final Integer TESTING_DEFAULT_PORT = new Integer(8080); + public static final Integer TESTING_DEFAULT_PORT = Integer.valueOf(8080); // Environment attribute key where the instance id is stored. Keep in sync // with ModulesServiceImpl.INSTANCE_ID_ENV_ATTRIBUTE. @@ -194,10 +194,8 @@ protected LocalEnvironment(String appId, String moduleName, String majorVersionI moduleName, majorVersionId)); } - /** - * Sets the instance for the provided attributes. - */ - static void setInstance(Map attributes, int instance) { + /** Sets the instance for the provided attributes. */ + public static void setInstance(Map attributes, int instance) { // First we remove the old value if there is one. attributes.remove(INSTANCE_ID_ENV_ATTRIBUTE); // Next we set the new value if needed. @@ -207,10 +205,10 @@ static void setInstance(Map attributes, int instance) { } /** - * Sets the {@link #PORT_ID_ENV_ATTRIBUTE} value to the provided port value or - * clears it if port is null. + * Sets the {@link #PORT_ID_ENV_ATTRIBUTE} value to the provided port value or clears it if port + * is null. */ - static void setPort(Map attributes, Integer port) { + public static void setPort(Map attributes, Integer port) { if (port == null) { attributes.remove(PORT_ID_ENV_ATTRIBUTE); } else { diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/LocalURLFetchServiceStreamHandler.java b/api_dev/src/main/java/com/google/appengine/tools/development/LocalURLFetchServiceStreamHandler.java index b6c3c86a1..da96f1b97 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/LocalURLFetchServiceStreamHandler.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/LocalURLFetchServiceStreamHandler.java @@ -29,7 +29,7 @@ import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; /** * Extension to {@link URLFetchServiceStreamHandler} that can fall back to a diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ManualInstanceHolder.java b/api_dev/src/main/java/com/google/appengine/tools/development/ManualInstanceHolder.java index c0453d200..48289b84d 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/ManualInstanceHolder.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ManualInstanceHolder.java @@ -19,8 +19,6 @@ import com.google.appengine.tools.development.ApplicationConfigurationManager.ModuleConfigurationHandle; import com.google.appengine.tools.development.InstanceStateHolder.InstanceState; import java.io.File; -import java.security.AccessController; -import java.security.PrivilegedExceptionAction; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -157,18 +155,7 @@ void stopServing() throws Exception { startRequestLatch = new CountDownLatch(1); doConfigure(); createConnection(); - // We call ContainerService.startup inside a PrivilegedExceptionAction - // so threads created by the contained Jetty instance - // will not inherit our callers protection domains. See - // http://docs.oracle.com/javase/7/docs/technotes/guides/security/spec/security-spec.doc4.html - // section 4.3 for details. - AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public Object run() throws Exception { - getContainerService().startup(); - return null; - } - }); + getContainerService().startup(); stateHolder.testAndSet(InstanceState.STOPPED, InstanceState.INITIALIZING); } } diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/Modules.java b/api_dev/src/main/java/com/google/appengine/tools/development/Modules.java index 22abae9a3..9f74fab1c 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/Modules.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/Modules.java @@ -17,6 +17,7 @@ package com.google.appengine.tools.development; import com.google.appengine.api.modules.ModulesServicePb.ModulesServiceError; +import com.google.appengine.tools.info.AppengineSdk; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.ApplicationException; import com.google.apphosting.utils.config.AppEngineWebXml; @@ -24,7 +25,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.File; -import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -33,9 +34,6 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; /** * Manager for {@link DevAppServer} servers. @@ -62,27 +60,45 @@ public static Modules createModules( String serverInfo, File externalResourceDir, String address, DevAppServer devAppServer) { ImmutableList.Builder builder = ImmutableList.builder(); for (ApplicationConfigurationManager.ModuleConfigurationHandle moduleConfigurationHandle : - applicationConfigurationManager.getModuleConfigurationHandles()) { - AppEngineWebXml appEngineWebXml = - moduleConfigurationHandle.getModule().getAppEngineWebXml(); + applicationConfigurationManager.getModuleConfigurationHandles()) { + AppEngineWebXml appEngineWebXml = moduleConfigurationHandle.getModule().getAppEngineWebXml(); Module module = null; if (!appEngineWebXml.getBasicScaling().isEmpty()) { - module = new BasicModule(moduleConfigurationHandle, serverInfo, address, devAppServer, - appEngineWebXml); + module = + new BasicModule( + moduleConfigurationHandle, serverInfo, address, devAppServer, appEngineWebXml); } else if (!appEngineWebXml.getManualScaling().isEmpty()) { - module = new ManualModule(moduleConfigurationHandle, serverInfo, address, devAppServer, - appEngineWebXml); + module = + new ManualModule( + moduleConfigurationHandle, serverInfo, address, devAppServer, appEngineWebXml); } else { - module = new AutomaticModule(moduleConfigurationHandle, serverInfo, externalResourceDir, - address, devAppServer); + module = + new AutomaticModule( + moduleConfigurationHandle, serverInfo, externalResourceDir, address, devAppServer); } builder.add(module); // Clear values that apply to the primary container only externalResourceDir = null; } - instance.set(new Modules(builder.build())); - return instance.get(); + try { + ImmutableList lm = builder.build(); + instance.set( + Class.forName(AppengineSdk.getSdk().getModulesClassName()) + .asSubclass(Modules.class) + .getDeclaredConstructor(List.class) + .newInstance(lm)); + return instance.get(); + } catch (ClassNotFoundException + | IllegalAccessException + | IllegalArgumentException + | InstantiationException + | NoSuchMethodException + | SecurityException + | InvocationTargetException ex) { + Logger.getLogger(Modules.class.getName()).log(Level.SEVERE, null, ex); + } + return null; } public static Modules getInstance() { @@ -123,7 +139,7 @@ public Module getMainModule() { return modules.get(0); } - private Modules(List modules) { + public Modules(List modules) { if (modules.size() < 1) { throw new IllegalArgumentException("modules must not be empty."); } @@ -377,14 +393,6 @@ public boolean checkInstanceStopped(String moduleName, int instance) { return instanceHolder.isStopped(); } - @Override - public void forwardToInstance(String requestedModule, int instance, HttpServletRequest hrequest, - HttpServletResponse hresponse) throws IOException, ServletException { - Module module = getModule(requestedModule); - InstanceHolder instanceHolder = module.getInstanceHolder(instance); - instanceHolder.getContainerService().forwardToServer(hrequest, hresponse); - } - @Override public boolean isLoadBalancingInstance(String moduleName, int instance) { Module module = getModule(moduleName); diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ModulesEE8.java b/api_dev/src/main/java/com/google/appengine/tools/development/ModulesEE8.java new file mode 100644 index 000000000..b23116217 --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ModulesEE8.java @@ -0,0 +1,44 @@ +/* + * 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; + +import java.io.IOException; +import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Manager for {@link DevAppServer} servers. */ +public class ModulesEE8 extends Modules { + + public ModulesEE8(List modules) { + super(modules); + } + + public void forwardToInstance( + String requestedModule, + int instance, + HttpServletRequest hrequest, + HttpServletResponse hresponse) + throws IOException, ServletException { + Module module = getModule(requestedModule); + InstanceHolder instanceHolder = module.getInstanceHolder(instance); + ((ContainerServiceEE8) instanceHolder.getContainerService()) + .forwardToServer(hrequest, hresponse); + } + +} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ModulesFilterHelper.java b/api_dev/src/main/java/com/google/appengine/tools/development/ModulesFilterHelper.java index d27e7f25f..f689b9a43 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/ModulesFilterHelper.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ModulesFilterHelper.java @@ -16,11 +16,6 @@ package com.google.appengine.tools.development; -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - /** * Support interface for {@link DevAppServerModulesFilter}. */ @@ -99,16 +94,6 @@ boolean acquireServingPermit(String moduleOrBackendName, int instanceNumber, */ boolean checkInstanceStopped(String moduleOrBackendName, int instance); - /** - * Forward a request to a specified module or backend instance. Calls the - * request dispatcher for the requested instance with the instance - * context. The caller must hold a serving permit for the requested - * instance before calling this method. - */ - void forwardToInstance(String requestedModuleOrBackendName, int instance, - HttpServletRequest hrequest, HttpServletResponse hresponse) - throws IOException, ServletException; - /** * Returns true if the specified module or backend instance is a load balancing * instance which will forward requests to an available instance. diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ModulesFilterHelperEE8.java b/api_dev/src/main/java/com/google/appengine/tools/development/ModulesFilterHelperEE8.java new file mode 100644 index 000000000..77120e71d --- /dev/null +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ModulesFilterHelperEE8.java @@ -0,0 +1,37 @@ +/* + * 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; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** */ +public interface ModulesFilterHelperEE8 extends ModulesFilterHelper { + /** + * Forward a request to a specified module or backend instance. Calls the request dispatcher for + * the requested instance with the instance context. The caller must hold a serving permit for the + * requested instance before calling this method. + */ + void forwardToInstance( + String requestedModuleOrBackendName, + int instance, + HttpServletRequest hrequest, + HttpServletResponse hresponse) + throws IOException, ServletException; +} diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/RequestThreadFactory.java b/api_dev/src/main/java/com/google/appengine/tools/development/RequestThreadFactory.java index e59f0ced7..28b3f966c 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/RequestThreadFactory.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/RequestThreadFactory.java @@ -17,9 +17,6 @@ package com.google.appengine.tools.development; import com.google.apphosting.api.ApiProxy; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Date; import java.util.Map; import java.util.concurrent.ThreadFactory; @@ -44,105 +41,93 @@ public class RequestThreadFactory implements ThreadFactory { @Override public Thread newThread(final Runnable runnable) { - final AccessControlContext context = AccessController.getContext(); - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Thread run() { - final ApiProxy.Environment environment = ApiProxy.getCurrentEnvironment(); - Thread thread = - new Thread() { - /** - * If the thread is started, install a {@link RequestEndListener} to interrupt the - * thread at the end of the request. We don't yet enforce request deadlines in the - * DevAppServer so we don't need to handle other interrupt cases yet. - */ + final ApiProxy.Environment environment = ApiProxy.getCurrentEnvironment(); + + Thread thread = + new Thread() { + /** + * If the thread is started, install a {@link RequestEndListener} to interrupt the + * thread at the end of the request. We don't yet enforce request deadlines in the + * DevAppServer so we don't need to handle other interrupt cases yet. + */ + @Override + public synchronized void start() { + try { + Thread.sleep(THREAD_STARTUP_LATENCY_MS); + } catch (InterruptedException ex) { + // We can't propagate the exception from here so + // just log, reset the bit, and continue. + logger.log( + Level.INFO, "Interrupted while simulating thread startup latency", ex); + Thread.currentThread().interrupt(); + } + super.start(); + final Thread thread = this; // Thread.this doesn't work from an anon subclass + RequestEndListenerHelper.register( + new RequestEndListener() { @Override - public synchronized void start() { - try { - Thread.sleep(THREAD_STARTUP_LATENCY_MS); - } catch (InterruptedException ex) { - // We can't propagate the exception from here so - // just log, reset the bit, and continue. - logger.log( - Level.INFO, "Interrupted while simulating thread startup latency", ex); - Thread.currentThread().interrupt(); + public void onRequestEnd(ApiProxy.Environment environment) { + if (thread.isAlive()) { + logger.info("Interrupting request thread: " + thread); + thread.interrupt(); + logger.info("Waiting up to 100ms for thread to complete: " + thread); + try { + thread.join(100); + } catch (InterruptedException ex) { + logger.info("Interrupted while waiting."); + } + if (thread.isAlive()) { + logger.info("Interrupting request thread again: " + thread); + thread.interrupt(); + long remaining = getRemainingDeadlineMillis(environment); + logger.info( + "Waiting up to " + + remaining + + " ms for thread to complete: " + + thread); + try { + thread.join(remaining); + } catch (InterruptedException ex) { + logger.info("Interrupted while waiting."); + } + if (thread.isAlive()) { + Throwable stack = new Throwable(); + stack.setStackTrace(thread.getStackTrace()); + logger.log( + Level.SEVERE, + "Thread left running: " + + thread + + ". " + + "In production this will cause the request to fail.", + stack); + } + } } - super.start(); - final Thread thread = this; // Thread.this doesn't work from an anon subclass - RequestEndListenerHelper.register( - new RequestEndListener() { - @Override - public void onRequestEnd(ApiProxy.Environment environment) { - if (thread.isAlive()) { - logger.info("Interrupting request thread: " + thread); - thread.interrupt(); - logger.info("Waiting up to 100ms for thread to complete: " + thread); - try { - thread.join(100); - } catch (InterruptedException ex) { - logger.info("Interrupted while waiting."); - } - if (thread.isAlive()) { - logger.info("Interrupting request thread again: " + thread); - thread.interrupt(); - long remaining = getRemainingDeadlineMillis(environment); - logger.info( - "Waiting up to " - + remaining - + " ms for thread to complete: " - + thread); - try { - thread.join(remaining); - } catch (InterruptedException ex) { - logger.info("Interrupted while waiting."); - } - if (thread.isAlive()) { - Throwable stack = new Throwable(); - stack.setStackTrace(thread.getStackTrace()); - logger.log( - Level.SEVERE, - "Thread left running: " - + thread - + ". " - + "In production this will cause the request to fail.", - stack); - } - } - } - } - }); } + }); + } - @Override - public void run() { - // Copy the current environment to the new thread. - ApiProxy.setEnvironmentForCurrentThread(environment); - // Switch back to the calling context before running the user's code. - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Object run() { - runnable.run(); - return null; - } - }, - context); - // Don't bother unsetting the environment. We're - // not going to reuse this thread and we want the - // environment still to be set during any - // UncaughtExceptionHandler (which happens after - // run() completes/throws). - } - }; - // This system property is used to check if the thread is - // running user code (ugly, I know). This thread is now - // running user code so we set it as well. - System.setProperty("devappserver-thread-" + thread.getName(), "true"); - return thread; + @Override + public void run() { + // Copy the current environment to the new thread. + ApiProxy.setEnvironmentForCurrentThread(environment); + // Switch back to the calling context before running the user's code. + runnable.run(); + // Don't bother unsetting the environment. We're + // not going to reuse this thread and we want the + // environment still to be set during any + // UncaughtExceptionHandler (which happens after + // run() completes/throws). } - }); + }; + // This system property is used to check if the thread is + // running user code (ugly, I know). This thread is now + // running user code so we set it as well. + System.setProperty("devappserver-thread-" + thread.getName(), "true"); + return thread; + + } private long getRemainingDeadlineMillis(ApiProxy.Environment environment) { diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/ResponseRewriterFilter.java b/api_dev/src/main/java/com/google/appengine/tools/development/ResponseRewriterFilter.java index a2efa07bc..a58b7d2d6 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/ResponseRewriterFilter.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/ResponseRewriterFilter.java @@ -21,6 +21,7 @@ import com.google.common.base.Preconditions; import com.google.common.html.HtmlEscapers; import com.google.common.net.HttpHeaders; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -728,7 +729,7 @@ public void sendError(int sc, String msg) throws IOException { checkNotCommitted(); // This has to be re-implemented to avoid committing the response. setStatus(sc, msg); - setErrorBody(sc + " " + HtmlEscapers.htmlEscaper().escape(msg)); + setErrorBody(sc + " " + (msg == null ? "" : HtmlEscapers.htmlEscaper().escape(msg))); } /** Sets the response body to an HTML page with an error message. diff --git a/api_dev/src/main/java/com/google/appengine/tools/development/SharedMain.java b/api_dev/src/main/java/com/google/appengine/tools/development/SharedMain.java index 8d2933af3..8b367dbb2 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/development/SharedMain.java +++ b/api_dev/src/main/java/com/google/appengine/tools/development/SharedMain.java @@ -31,7 +31,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Properties; import java.util.TimeZone; @@ -48,6 +47,16 @@ public abstract class SharedMain { private String runtime = null; private List propertyOptions = null; + /** + * An exception class that is thrown to indicate that command-line processing should be aborted. + */ + private static class TerminationException extends RuntimeException { + + TerminationException() { + super(); + } + } + /** * Returns the list of built-in {@link Option Options} that apply to both the monolithic dev app * server (in the Java SDK) and instances running under the Python devappserver2. @@ -58,7 +67,7 @@ protected List