diff --git a/.kokoro/tests/run_test_java.sh b/.kokoro/tests/run_test_java.sh index 9d427468238..2fa9b2c42ad 100755 --- a/.kokoro/tests/run_test_java.sh +++ b/.kokoro/tests/run_test_java.sh @@ -20,7 +20,7 @@ SCRIPT_DIR="$(dirname $0)/" # Fail the tests if no Java version was found. POM_JAVA=$(grep -oP '(?<=).*?(?=)' pom.xml) -ALLOWED_VERSIONS=("1.8" "11" "17" "21") +ALLOWED_VERSIONS=("1.8" "11" "17" "21", "25") # shellcheck disable=SC2199 # shellcheck disable=SC2076 if [[ "$POM_JAVA" = "" ]] || [[ ! " ${ALLOWED_VERSIONS[*]} " =~ " ${POM_JAVA} " ]]; then diff --git a/appengine-java25/ee11/analytics/README.md b/appengine-java25/ee11/analytics/README.md new file mode 100644 index 00000000000..8e4f59e0668 --- /dev/null +++ b/appengine-java25/ee11/analytics/README.md @@ -0,0 +1,24 @@ +# Google Analytics sample for Google App Engine + + +Open in Cloud Shell + +Integrating App Engine with Google Analytics using EE11. + +## Project setup, installation, and configuration + +- Register for [Google Analytics](http://www.google.com/analytics/), create +an application, and get a tracking Id. +- [Find your tracking Id](https://support.google.com/analytics/answer/1008080?hl=en) +and set it as an environment variable in [`appengine-web.xml`](src/main/webapp/WEB-INF/appengine-web.xml). + +## Running locally +This example uses the +[Maven Cloud CLI based plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + $ mvn appengine:run + +## Deploying + + $ mvn clean package appengine:deploy diff --git a/appengine-java25/ee11/analytics/pom.xml b/appengine-java25/ee11/analytics/pom.xml new file mode 100644 index 00000000000..3a2b604bb33 --- /dev/null +++ b/appengine-java25/ee11/analytics/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-analytics-j25-ee11 + + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + 25 + 25 + 3.0.1 + + + + + + libraries-bom + com.google.cloud + import + pom + 26.28.0 + + + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + jstl + jstl + 1.2 + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + jar + provided + + + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + com.google.truth + truth + 1.1.5 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + com.google.cloud.tools + appengine-maven-plugin + 2.7.0 + + GCLOUD_CONFIG + analytics + true + true + + + + + diff --git a/appengine-java25/ee11/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java b/appengine-java25/ee11/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java new file mode 100644 index 00000000000..31b9e4dc53f --- /dev/null +++ b/appengine-java25/ee11/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.analytics; + +// [START gae_java25_analytics_track] + +import com.google.appengine.api.urlfetch.URLFetchService; +import com.google.appengine.api.urlfetch.URLFetchServiceFactory; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import org.apache.http.client.utils.URIBuilder; + +@SuppressWarnings("serial") +// With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required. +@WebServlet( + name = "analytics", + description = "Analytics: Send Analytics Event to Google Analytics", + urlPatterns = "/analytics") +public class AnalyticsServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + String trackingId = System.getenv("GA_TRACKING_ID"); + URIBuilder builder = new URIBuilder(); + builder + .setScheme("http") + .setHost("www.google-analytics.com") + .setPath("/collect") + .addParameter("v", "1") // API Version. + .addParameter("tid", trackingId) // Tracking ID / Property ID. + // Anonymous Client Identifier. Ideally, this should be a UUID that + // is associated with particular user, device, or browser instance. + .addParameter("cid", "555") + .addParameter("t", "event") // Event hit type. + .addParameter("ec", "example") // Event category. + .addParameter("ea", "test action"); // Event action. + URI uri = null; + try { + uri = builder.build(); + } catch (URISyntaxException e) { + throw new ServletException("Problem building URI", e); + } + URLFetchService fetcher = URLFetchServiceFactory.getURLFetchService(); + URL url = uri.toURL(); + fetcher.fetch(url); + resp.getWriter().println("Event tracked."); + } +} +// [END gae_java25_analytics_track] diff --git a/appengine-java25/ee11/analytics/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java25/ee11/analytics/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..e8c0dcf01e1 --- /dev/null +++ b/appengine-java25/ee11/analytics/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,26 @@ + + + + + + java25 + true + appengine-analytics-j25-ee11 + + + + + + + diff --git a/appengine-java25/ee11/analytics/src/main/webapp/WEB-INF/web.xml b/appengine-java25/ee11/analytics/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..ae585c88628 --- /dev/null +++ b/appengine-java25/ee11/analytics/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + + + + + analytics + + diff --git a/appengine-java25/ee8/analytics/README.md b/appengine-java25/ee8/analytics/README.md new file mode 100644 index 00000000000..80b915b940f --- /dev/null +++ b/appengine-java25/ee8/analytics/README.md @@ -0,0 +1,24 @@ +# Google Analytics sample for Google App Engine + + +Open in Cloud Shell + +Integrating App Engine with Google Analytics using EE8. + +## Project setup, installation, and configuration + +- Register for [Google Analytics](http://www.google.com/analytics/), create +an application, and get a tracking Id. +- [Find your tracking Id](https://support.google.com/analytics/answer/1008080?hl=en) +and set it as an environment variable in [`appengine-web.xml`](src/main/webapp/WEB-INF/appengine-web.xml). + +## Running locally +This example uses the +[Maven Cloud CLI based plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + $ mvn appengine:run + +## Deploying + + $ mvn clean package appengine:deploy diff --git a/appengine-java25/ee8/analytics/pom.xml b/appengine-java25/ee8/analytics/pom.xml new file mode 100644 index 00000000000..b2f2d1d367b --- /dev/null +++ b/appengine-java25/ee8/analytics/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-analytics-j25-ee8 + + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + 25 + 25 + 2.0.29 + + + + + + libraries-bom + com.google.cloud + import + pom + 26.28.0 + + + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + jstl + jstl + 1.2 + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + com.google.truth + truth + 1.1.5 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + com.google.cloud.tools + appengine-maven-plugin + 2.7.0 + + GCLOUD_CONFIG + analytics + true + true + + + + + diff --git a/appengine-java25/ee8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java b/appengine-java25/ee8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java new file mode 100644 index 00000000000..d9e9650acfe --- /dev/null +++ b/appengine-java25/ee8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.analytics; + +// [START gae_java21_analytics_track] +import com.google.appengine.api.urlfetch.URLFetchService; +import com.google.appengine.api.urlfetch.URLFetchServiceFactory; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.http.client.utils.URIBuilder; + +@SuppressWarnings("serial") +// With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required. +@WebServlet( + name = "analytics", + description = "Analytics: Send Analytics Event to Google Analytics", + urlPatterns = "/analytics") +public class AnalyticsServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + String trackingId = System.getenv("GA_TRACKING_ID"); + URIBuilder builder = new URIBuilder(); + builder + .setScheme("http") + .setHost("www.google-analytics.com") + .setPath("/collect") + .addParameter("v", "1") // API Version. + .addParameter("tid", trackingId) // Tracking ID / Property ID. + // Anonymous Client Identifier. Ideally, this should be a UUID that + // is associated with particular user, device, or browser instance. + .addParameter("cid", "555") + .addParameter("t", "event") // Event hit type. + .addParameter("ec", "example") // Event category. + .addParameter("ea", "test action"); // Event action. + URI uri = null; + try { + uri = builder.build(); + } catch (URISyntaxException e) { + throw new ServletException("Problem building URI", e); + } + URLFetchService fetcher = URLFetchServiceFactory.getURLFetchService(); + URL url = uri.toURL(); + fetcher.fetch(url); + resp.getWriter().println("Event tracked."); + } +} +// [END gae_java21_analytics_track] diff --git a/appengine-java25/ee8/analytics/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java25/ee8/analytics/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..29d349f8757 --- /dev/null +++ b/appengine-java25/ee8/analytics/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,26 @@ + + + + + + java25 + true + appengine-analytics-j25-ee9 + + + + + + + diff --git a/appengine-java25/ee8/analytics/src/main/webapp/WEB-INF/web.xml b/appengine-java25/ee8/analytics/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..e11902f8708 --- /dev/null +++ b/appengine-java25/ee8/analytics/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + + + + + analytics + + diff --git a/appengine-java25/helloworld/README.md b/appengine-java25/helloworld/README.md new file mode 100644 index 00000000000..b90c452ee8e --- /dev/null +++ b/appengine-java25/helloworld/README.md @@ -0,0 +1,81 @@ +HelloWorld for App Engine Standard (Java 25) +============================ + +This sample demonstrates how to deploy an application on Google App Engine. + +See the [Google App Engine standard environment documentation][ae-docs] for more +detailed instructions. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + + +* [Java 25](https://www.oracle.com/java/technologies/downloads/) +* [Maven](https://maven.apache.org/download.cgi) (at least 3.5) +* [Google Cloud SDK](https://cloud.google.com/sdk/) (aka gcloud) + +## Setup + +• Download and initialize the [Cloud SDK](https://cloud.google.com/sdk/) + +``` +gcloud init +``` + +* Create an App Engine app within the current Google Cloud Project + +``` +gcloud app create +``` + +* In the `pom.xml`, update the [App Engine Maven Plugin](https://cloud.google.com/appengine/docs/standard/java/tools/maven-reference) +with your Google Cloud Project Id: + +``` + + com.google.cloud.tools + appengine-maven-plugin + 2.8.3 + + myProjectId + GCLOUD_CONFIG + + +``` +**Note:** `GCLOUD_CONFIG` is a special version for autogenerating an App Engine +version. Change this field to specify a specific version name. + +## Maven +### Running locally + + mvn package appengine:run + +To use visit: http://localhost:8080/ + +### Deploying + + mvn package appengine:deploy + +To use visit: https://YOUR-PROJECT-ID.appspot.com + +### Testing + + mvn verify + +As you add / modify the source code (`src/main/java/...`) it's very useful to add [unit testing](https://cloud.google.com/appengine/docs/java/tools/localunittesting) +to (`src/main/test/...`). The following resources are quite useful: + +* [Junit4](http://junit.org/junit4/) +* [Mockito](http://mockito.org/) +* [Truth](http://google.github.io/truth/) + +## Gradle + +### Running locally + + ./gradlew appengineRun + +To use visit: http://localhost:8080/ + +### Deploying + + ./gradlew appengineDeploy diff --git a/appengine-java25/helloworld/build.gradle b/appengine-java25/helloworld/build.gradle new file mode 100644 index 00000000000..f9e6ed67ead --- /dev/null +++ b/appengine-java25/helloworld/build.gradle @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START gae_standard25_gradle] +apply plugin: 'java' +apply plugin: 'war' + +buildscript { + repositories { + // gretty plugin is in Maven Central + mavenCentral() + } + dependencies { + classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.8.1' + classpath 'org.gretty:gretty:4.1.5' + } +} +apply plugin: 'org.gretty' +apply plugin: 'com.google.cloud.tools.appengine' + +repositories { + mavenCentral() +} + +appengine { + deploy { // deploy configuration + stopPreviousVersion = true // default - stop the current version + promote = true // default - & make this the current version + projectId = 'GCLOUD_CONFIG' + version = 'GCLOUD_CONFIG' + } +} + +sourceSets { + // In Gradle 8, the default location is app/src/java, which does not match + // Maven's directory structure. + main.java.srcDirs = ['src/main/java'] + main.resources.srcDirs = ['src/main/resources', 'src/main/webapp'] + test.java.srcDirs = ['src/test/java'] +} + +dependencies { + implementation 'com.google.appengine:appengine-api-1.0-sdk:3.0.1' + implementation 'jakarta.servlet:jakarta.servlet-api:6.1.0' + + // Test Dependencies + testImplementation 'com.google.appengine:appengine-testing:3.0.1' + testImplementation 'com.google.appengine:appengine-api-stubs:3.0.1' + testImplementation 'com.google.appengine:appengine-tools-sdk:3.0.1' + + testImplementation 'com.google.truth:truth:1.1.5' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.11.0' +} +// [END gae_standard25_gradle] diff --git a/appengine-java25/helloworld/gradle/libs.versions.toml b/appengine-java25/helloworld/gradle/libs.versions.toml new file mode 100644 index 00000000000..e74f3857bde --- /dev/null +++ b/appengine-java25/helloworld/gradle/libs.versions.toml @@ -0,0 +1,10 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +guava = "33.2.1-jre" +junit = "4.13.2" + +[libraries] +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit = { module = "junit:junit", version.ref = "junit" } diff --git a/appengine-java25/helloworld/gradle/wrapper/gradle-wrapper.properties b/appengine-java25/helloworld/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..48b43d35063 --- /dev/null +++ b/appengine-java25/helloworld/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/appengine-java25/helloworld/gradlew b/appengine-java25/helloworld/gradlew new file mode 100755 index 00000000000..f5feea6d6b1 --- /dev/null +++ b/appengine-java25/helloworld/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/appengine-java25/helloworld/gradlew.bat b/appengine-java25/helloworld/gradlew.bat new file mode 100644 index 00000000000..9d21a21834d --- /dev/null +++ b/appengine-java25/helloworld/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/appengine-java25/helloworld/pom.xml b/appengine-java25/helloworld/pom.xml new file mode 100644 index 00000000000..55d2d40530d --- /dev/null +++ b/appengine-java25/helloworld/pom.xml @@ -0,0 +1,115 @@ + + + + + 4.0.0 + + war + com.example.appengine + helloworld-jdk25 + 1.0-SNAPSHOT + + + com.google.cloud.samples + shared-configuration + 1.2.2 + + + + 25 + 25 + + + + + + + com.google.appengine + appengine-api-1.0-sdk + 3.0.1 + + + jakarta.servlet + jakarta.servlet-api + 6.1.0 + jar + provided + + + + + com.google.appengine + appengine-testing + 2.0.23 + test + + + com.google.appengine + appengine-api-stubs + 2.0.23 + test + + + com.google.appengine + appengine-tools-sdk + 2.0.23 + test + + + + com.google.truth + truth + 1.1.5 + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + com.google.cloud.tools + appengine-maven-plugin + 2.8.3 + + + myProjectId + + GCLOUD_CONFIG + + + + + diff --git a/appengine-java25/helloworld/settings.gradle b/appengine-java25/helloworld/settings.gradle new file mode 100644 index 00000000000..749d70d9e06 --- /dev/null +++ b/appengine-java25/helloworld/settings.gradle @@ -0,0 +1,32 @@ +pluginManagement { + repositories { + gradlePluginPortal() + } +} +// Copyright 2024 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 +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.10/userguide/multi_project_builds.html in the Gradle documentation. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +rootProject.name = 'helloworld' diff --git a/appengine-java25/helloworld/src/main/java/com/example/appengine/java25/HelloAppEngine.java b/appengine-java25/helloworld/src/main/java/com/example/appengine/java25/HelloAppEngine.java new file mode 100644 index 00000000000..1f7c1ba4656 --- /dev/null +++ b/appengine-java25/helloworld/src/main/java/com/example/appengine/java25/HelloAppEngine.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.java25; + +import com.google.appengine.api.utils.SystemProperty; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Properties; + +// With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required. +@WebServlet(name = "HelloAppEngine", value = "/hello") +public class HelloAppEngine extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException { + Properties properties = System.getProperties(); + + response.setContentType("text/plain"); + response.getWriter().println("Hello App Engine - Standard using " + + SystemProperty.version.get() + " Java " + + properties.get("java.specification.version")); + } +} diff --git a/appengine-java25/helloworld/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java25/helloworld/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..2324276a7a4 --- /dev/null +++ b/appengine-java25/helloworld/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + + + java25 + true + + diff --git a/appengine-java25/helloworld/src/main/webapp/WEB-INF/web.xml b/appengine-java25/helloworld/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..bc146d3fcad --- /dev/null +++ b/appengine-java25/helloworld/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + + helloworld + com.example.appengine.java25.HelloAppEngine + + + helloworld + /* + + \ No newline at end of file diff --git a/appengine-java25/helloworld/src/test/java/com/example/appengine/java25/HelloAppEngineTest.java b/appengine-java25/helloworld/src/test/java/com/example/appengine/java25/HelloAppEngineTest.java new file mode 100644 index 00000000000..70b351d99c0 --- /dev/null +++ b/appengine-java25/helloworld/src/test/java/com/example/appengine/java25/HelloAppEngineTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine.java25; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.io.StringWriter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests for {@link HelloAppEngine}. + */ +@RunWith(JUnit4.class) +public class HelloAppEngineTest { + + private static final String FAKE_URL = "fake.fk/hello"; + // Set up a helper so that the ApiProxy returns a valid environment for local testing. + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock + private HttpServletRequest mockRequest; + @Mock + private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private HelloAppEngine servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + helper.setUp(); + + // Set up some fake HTTP requests + when(mockRequest.getRequestURI()).thenReturn(FAKE_URL); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new HelloAppEngine(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGetWritesResponse() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + // We expect our hello world response. + assertThat(responseWriter.toString()) + .contains("Hello App Engine - Standard "); + } +} diff --git a/flexible/java-25/micronaut-helloworld/README.md b/flexible/java-25/micronaut-helloworld/README.md new file mode 100644 index 00000000000..72c32c4383f --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/README.md @@ -0,0 +1,16 @@ +# Micronaut Application on Google App Engine Flex with Java 25 + +This sample shows how to deploy a [Micronaut](https://micronaut.io/) +application to Google App Engine Flex. + +## Deploying + +```bash +gcloud app deploy +``` + +To view your app, use command: +``` +gcloud app browse +``` +Or navigate to `https://.appspot.com`. diff --git a/flexible/java-25/micronaut-helloworld/micronaut-cli.yml b/flexible/java-25/micronaut-helloworld/micronaut-cli.yml new file mode 100644 index 00000000000..2c08db76694 --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/micronaut-cli.yml @@ -0,0 +1,19 @@ +# Copyright 2023 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +profile: service +defaultPackage: com.example.appengine +--- +testFramework: junit +sourceLanguage: java diff --git a/flexible/java-25/micronaut-helloworld/pom.xml b/flexible/java-25/micronaut-helloworld/pom.xml new file mode 100644 index 00000000000..8657747cee9 --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/pom.xml @@ -0,0 +1,188 @@ + + + + 4.0.0 + com.example.appengine.flexible + micronaut-helloworld + 0.1 + + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + UTF-8 + com.example.appengine.Application + 21 + 21 + 3.10.3 + + + + + io.micronaut + micronaut-inject + ${micronaut.version} + compile + + + io.micronaut + micronaut-validation + ${micronaut.version} + compile + + + io.micronaut + micronaut-runtime + ${micronaut.version} + compile + + + io.micronaut + micronaut-http-client + ${micronaut.version} + compile + + + javax.annotation + javax.annotation-api + 1.3.2 + compile + + + io.micronaut + micronaut-http-server-netty + ${micronaut.version} + compile + + + junit + junit + 4.13.2 + test + + + + + + + com.google.cloud.tools + appengine-maven-plugin + 2.8.0 + + GCLOUD_CONFIG + micronaut-helloworld + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + ${exec.mainClass} + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + java + + -noverify + -XX:TieredStopAtLevel=1 + -Dcom.sun.management.jmxremote + -classpath + + ${exec.mainClass} + + + + + maven-surefire-plugin + 3.2.5 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + UTF-8 + + -parameters + + + + io.micronaut + micronaut-inject-java + ${micronaut.version} + + + io.micronaut + micronaut-validation + ${micronaut.version} + + + + + + test-compile + + testCompile + + + + -parameters + + + + io.micronaut + micronaut-inject-java + ${micronaut.version} + + + io.micronaut + micronaut-validation + ${micronaut.version} + + + + + + + + + + diff --git a/flexible/java-25/micronaut-helloworld/src/main/appengine/app.yaml b/flexible/java-25/micronaut-helloworld/src/main/appengine/app.yaml new file mode 100644 index 00000000000..db82585bc79 --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/src/main/appengine/app.yaml @@ -0,0 +1,27 @@ +# Copyright 2023 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# [START gae_flex_java25_yaml] +runtime: java +env: flex +service: helloworld-java25-flex +runtime_config: + operating_system: ubuntu24 + runtime_version: 25 +handlers: +- url: /.* + script: this field is required, but ignored + +manual_scaling: + instances: 1 +# [END gae_flex_java25_yaml] diff --git a/flexible/java-25/micronaut-helloworld/src/main/java/com/example/appengine/Application.java b/flexible/java-25/micronaut-helloworld/src/main/java/com/example/appengine/Application.java new file mode 100644 index 00000000000..e99fbde8f39 --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/src/main/java/com/example/appengine/Application.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class); + } +} diff --git a/flexible/java-25/micronaut-helloworld/src/main/java/com/example/appengine/HelloController.java b/flexible/java-25/micronaut-helloworld/src/main/java/com/example/appengine/HelloController.java new file mode 100644 index 00000000000..ac32f9ab102 --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/src/main/java/com/example/appengine/HelloController.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine; + +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; + +@Controller("/") +public class HelloController { + @Get("/") + @Produces(MediaType.TEXT_PLAIN) + public String index() { + return "Hello World!"; + } +} diff --git a/flexible/java-25/micronaut-helloworld/src/main/resources/application.yml b/flexible/java-25/micronaut-helloworld/src/main/resources/application.yml new file mode 100644 index 00000000000..854340b8361 --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/src/main/resources/application.yml @@ -0,0 +1,17 @@ +# Copyright 2023 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +micronaut: + application: + name: micronaut diff --git a/flexible/java-25/micronaut-helloworld/src/main/resources/logback.xml b/flexible/java-25/micronaut-helloworld/src/main/resources/logback.xml new file mode 100644 index 00000000000..4f0363b57df --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/src/main/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/flexible/java-25/micronaut-helloworld/src/test/java/com/example/appengine/HelloControllerTest.java b/flexible/java-25/micronaut-helloworld/src/test/java/com/example/appengine/HelloControllerTest.java new file mode 100644 index 00000000000..44571ee72cb --- /dev/null +++ b/flexible/java-25/micronaut-helloworld/src/test/java/com/example/appengine/HelloControllerTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.appengine; + +import static org.junit.Assert.assertEquals; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.client.HttpClient; +import io.micronaut.runtime.server.EmbeddedServer; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class HelloControllerTest { + private static EmbeddedServer server; + private static HttpClient client; + + @BeforeClass + public static void setupServer() { + + server = ApplicationContext.run(EmbeddedServer.class); + + client = server.getApplicationContext().createBean(HttpClient.class, server.getURL()); + } + + @AfterClass + public static void stopServer() { + if (client != null) { + client.stop(); + } + if (server != null) { + server.stop(); + } + } + + @Test + public void testHelloWorldResponse() { + String response = client.toBlocking().retrieve(HttpRequest.GET("/")); + assertEquals("Hello World!", response); + } +} diff --git a/flexible/java-25/websocket-jetty/README.md b/flexible/java-25/websocket-jetty/README.md new file mode 100644 index 00000000000..8a3ff4e0f2f --- /dev/null +++ b/flexible/java-25/websocket-jetty/README.md @@ -0,0 +1,54 @@ +# App Engine Flexible Environment - Web Socket Example + +This sample demonstrates how to use +[Websockets](https://tools.ietf.org/html/rfc6455) on [Google App Engine Flexible +Environment](https://cloud.google.com/appengine/docs/flexible/java/) using Java. +The sample uses the [native Jetty WebSocket Server +API](http://www.eclipse.org/jetty/documentation/9.4.x/jetty-websocket-server-api.html) +to create a server-side socket and the [native Jetty WebSocket Client +API](http://www.eclipse.org/jetty/documentation/9.4.x/jetty-websocket-client-api.html). + +## Sample application workflow + +1. The sample application creates a server socket using the endpoint `/echo`. +1. The homepage (`/`) provides a form to submit a text message to the server +socket. This creates a client-side socket and sends the message to the server. +1. The server on receiving the message, echoes the message back to the client. +1. The message received by the client is stored in an in-memory cache and is + viewable on the homepage. + +The sample also provides a Javascript +[client](src/main/webapp/js_client.jsp)(`/js_client.jsp`) that you can use to +test against the Websocket server. + +## Setup + +- [Install](https://cloud.google.com/sdk/) and initialize GCloud SDK. This will + + ```sh + gcloud init + ``` + +- If this is your first time creating an app engine application + + ```sh + gcloud appengine create + ``` + +## Deploy + +The sample application is packaged as a war, and hence will be automatically run +using the [Java 8/Jetty 9 with Servlet 3.1 +Runtime](https://cloud.google.com/appengine/docs/flexible/java/dev-jetty9). + +```sh +mvn clean package appengine:deploy +``` + +You can then direct your browser to `https://YOUR_PROJECT_ID.appspot.com/` + +To test the Javascript client, access +`https://YOUR_PROJECT_ID.appspot.com/js_client.jsp` + +Note: This application constructs a Web Socket URL using `getWebSocketAddress` +in the [SendServlet Class](src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java). The application assumes the latest version of the service. diff --git a/flexible/java-25/websocket-jetty/pom.xml b/flexible/java-25/websocket-jetty/pom.xml new file mode 100644 index 00000000000..e216f50b057 --- /dev/null +++ b/flexible/java-25/websocket-jetty/pom.xml @@ -0,0 +1,208 @@ + + + 4.0.0 + + org.eclipse.jetty.demo + native-jetty-websocket-example + 1.0-SNAPSHOT + jar + + + + com.google.cloud.samples + shared-configuration + 1.2.2 + + + + 17 + 17 + false + 9.4.57.v20241219 + 2.7.18 + + + + + + com.google.cloud + libraries-bom + 26.32.0 + pom + import + + + + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty + jetty-webapp + ${jetty.version} + jar + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.eclipse.jetty + jetty-annotations + ${jetty.version} + + + + org.eclipse.jetty + apache-jsp + ${jetty.version} + nolog + + + javax.servlet + javax.servlet-api + 4.0.1 + jar + provided + + + + org.eclipse.jetty.websocket + websocket-client + ${jetty.version} + provided + + + org.eclipse.jetty.websocket + websocket-server + ${jetty.version} + provided + + + org.eclipse.jetty.websocket + websocket-servlet + ${jetty.version} + provided + + + com.google.guava + guava + + + org.slf4j + slf4j-simple + 2.0.12 + + + junit + junit + 4.13.2 + test + + + + + + + ${basedir}/src/main/webapp + false + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + + com.google.cloud.tools + appengine-maven-plugin + 2.8.0 + + GCLOUD_CONFIG + GCLOUD_CONFIG + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + + + java + + + + + com.example.flexible.websocket.jettynative.Main + + + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.version} + + jar + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + com.example.flexible.websocket.jettynative.Main + + + + jar-with-dependencies + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + diff --git a/flexible/java-25/websocket-jetty/src/main/appengine/app.yaml b/flexible/java-25/websocket-jetty/src/main/appengine/app.yaml new file mode 100644 index 00000000000..b31b02a557e --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/appengine/app.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +runtime: java +env: flex +runtime_config: + operating_system: ubuntu22 + runtime_version: 17 +manual_scaling: + instances: 1 +handlers: +- url: /.* + script: this field is required, but ignored + + + +# For applications which can take advantage of session affinity +# (where the load balancer will attempt to route multiple connections from +# the same user to the same App Engine instance), uncomment the folowing: + +# network: +# session_affinity: true diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ClientSocket.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ClientSocket.java new file mode 100644 index 00000000000..84360e4a904 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ClientSocket.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.flexible.websocket.jettynative; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.logging.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +/** + * Basic Echo Client Socket. + */ +@WebSocket(maxTextMessageSize = 64 * 1024) +public class ClientSocket { + private Logger logger = Logger.getLogger(ClientSocket.class.getName()); + private Session session; + // stores the messages in-memory. + // Note : this is currently an in-memory store for demonstration, + // not recommended for production use-cases. + private static Collection messages = new ConcurrentLinkedDeque<>(); + + @OnWebSocketClose + public void onClose(int statusCode, String reason) { + logger.fine("Connection closed: " + statusCode + ":" + reason); + this.session = null; + } + + @OnWebSocketConnect + public void onConnect(Session session) { + this.session = session; + } + + @OnWebSocketMessage + public void onMessage(String msg) { + logger.fine("Message Received : " + msg); + messages.add(msg); + } + + // Retrieve all received messages. + public static Collection getReceivedMessages() { + return Collections.unmodifiableCollection(messages); + } +} diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java new file mode 100644 index 00000000000..32358f14268 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.flexible.websocket.jettynative; + +import javax.servlet.annotation.WebServlet; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.servlet.WebSocketServlet; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; + +/* + * Server-side WebSocket upgraded on /echo servlet. + */ +@SuppressWarnings("serial") +@WebServlet( + name = "Echo WebSocket Servlet", + urlPatterns = {"/echo"}) +public class EchoServlet extends WebSocketServlet implements WebSocketCreator { + @Override + public void configure(WebSocketServletFactory factory) { + factory.setCreator(this); + } + + @Override + public Object createWebSocket( + ServletUpgradeRequest servletUpgradeRequest, ServletUpgradeResponse servletUpgradeResponse) { + return new ServerSocket(); + } +} diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/Main.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/Main.java new file mode 100644 index 00000000000..43212718164 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/Main.java @@ -0,0 +1,149 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.flexible.websocket.jettynative; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import org.apache.tomcat.util.scan.StandardJarScanFilter; +import org.apache.tomcat.util.scan.StandardJarScanner; +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.apache.jsp.JettyJasperInitializer; +import org.eclipse.jetty.jsp.JettyJspServlet; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.webapp.WebInfConfiguration; + +/** + * Starts up the server, including a DefaultServlet that handles static files, and any servlet + * classes annotated with the @WebServlet annotation. + */ +public class Main { + + public static void main(String[] args) throws Exception { + + // Create a server that listens on port 8080. + Server server = new Server(8080); + WebAppContext webAppContext = new WebAppContext(); + server.setHandler(webAppContext); + + // Load static content from inside the jar file. + URL webAppDir = Main.class.getClassLoader().getResource("WEB-INF/"); + System.out.println(webAppDir); + webAppContext.setResourceBase(webAppDir.toURI().toString()); + + // Enable annotations so the server sees classes annotated with @WebServlet. + webAppContext.setConfigurations( + new Configuration[] { + new AnnotationConfiguration(), new WebInfConfiguration(), + }); + + webAppContext.setAttribute( + "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", + ".*/target/classes/|.*\\.jar"); + enableEmbeddedJspSupport(webAppContext); + + ServletHolder holderAltMapping = new ServletHolder(); + holderAltMapping.setName("index.jsp"); + holderAltMapping.setForcedPath("/index.jsp"); + webAppContext.addServlet(holderAltMapping, "/"); + + // Start the server! 🚀 + server.start(); + System.out.println("Server started!"); + + // Keep the main thread alive while the server is running. + server.join(); + } + + private static void enableEmbeddedJspSupport(ServletContextHandler servletContextHandler) + throws IOException { + // Establish Scratch directory for the servlet context (used by JSP compilation) + File tempDir = new File(System.getProperty("java.io.tmpdir")); + File scratchDir = new File(tempDir.toString(), "embedded-jetty-jsp"); + + if (!scratchDir.exists()) { + if (!scratchDir.mkdirs()) { + throw new IOException("Unable to create scratch directory: " + scratchDir); + } + } + servletContextHandler.setAttribute("javax.servlet.context.tempdir", scratchDir); + + // Set Classloader of Context to be sane (needed for JSTL) + // JSP requires a non-System classloader, this simply wraps the + // embedded System classloader in a way that makes it suitable + // for JSP to use + ClassLoader jspClassLoader = new URLClassLoader(new URL[0], Main.class.getClassLoader()); + servletContextHandler.setClassLoader(jspClassLoader); + + // Manually call JettyJasperInitializer on context startup + servletContextHandler.addBean(new JspStarter(servletContextHandler)); + + // Create / Register JSP Servlet (must be named "jsp" per spec) + ServletHolder holderJsp = new ServletHolder("jsp", JettyJspServlet.class); + holderJsp.setInitOrder(0); + holderJsp.setInitParameter("logVerbosityLevel", "DEBUG"); + holderJsp.setInitParameter("fork", "false"); + holderJsp.setInitParameter("xpoweredBy", "false"); + holderJsp.setInitParameter("compilerTargetVM", "1.8"); + holderJsp.setInitParameter("compilerSourceVM", "1.8"); + holderJsp.setInitParameter("keepgenerated", "true"); + servletContextHandler.addServlet(holderJsp, "*.jsp"); + } + + /** + * JspStarter for embedded ServletContextHandlers + * + *

This is added as a bean that is a jetty LifeCycle on the ServletContextHandler. This bean's + * doStart method will be called as the ServletContextHandler starts, and will call the + * ServletContainerInitializer for the jsp engine. + */ + public static class JspStarter extends AbstractLifeCycle + implements ServletContextHandler.ServletContainerInitializerCaller { + JettyJasperInitializer sci; + ServletContextHandler context; + + public JspStarter(ServletContextHandler context) { + this.sci = new JettyJasperInitializer(); + this.context = context; + String skip = "apache-*,ecj-*,jetty-*,asm-*,javax.servlet-*" + + "javax.annotation-*,taglibs-standard-spec-*,*.jar"; + StandardJarScanner jarScanner = new StandardJarScanner(); + StandardJarScanFilter jarScanFilter = new StandardJarScanFilter(); + jarScanFilter.setTldSkip(skip); + jarScanner.setJarScanFilter(jarScanFilter); + this.context.setAttribute("org.apache.tomcat.JarScanner", jarScanner); + } + + @Override + protected void doStart() throws Exception { + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(context.getClassLoader()); + try { + sci.onStartup(null, context.getServletContext()); + super.doStart(); + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + } +} diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java new file mode 100644 index 00000000000..0feab349ac2 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.flexible.websocket.jettynative; + +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.Future; +import java.util.logging.Logger; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; + +@WebServlet("/send") +/** Servlet that sends the message sent over POST to over a websocket connection. */ +public class SendServlet extends HttpServlet { + + private Logger logger = Logger.getLogger(SendServlet.class.getName()); + + private static final String ENDPOINT = "/echo"; + private static final String WEBSOCKET_PROTOCOL_PREFIX = "ws://"; + private static final String WEBSOCKET_HTTPS_PROTOCOL_PREFIX = "wss://"; + private static final String APPENGINE_HOST_SUFFIX = ".appspot.com"; + + // GAE_INSTANCE environment is used to detect App Engine Flexible Environment + private static final String GAE_INSTANCE_VAR = "GAE_INSTANCE"; + // GOOGLE_CLOUD_PROJECT environment variable is set to the GCP project ID on App Engine Flexible. + private static final String GOOGLE_CLOUD_PROJECT_ENV_VAR = "GOOGLE_CLOUD_PROJECT"; + // GAE_SERVICE environment variable is set to the GCP service name. + private static final String GAE_SERVICE_ENV_VAR = "GAE_SERVICE"; + + private final HttpClient httpClient; + private final WebSocketClient webSocketClient; + private final ClientSocket clientSocket; + + public SendServlet() { + this.httpClient = createHttpClient(); + this.webSocketClient = createWebSocketClient(); + this.clientSocket = new ClientSocket(); + } + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + String message = request.getParameter("message"); + try { + sendMessageOverWebSocket(message); + response.sendRedirect("/"); + } catch (Exception e) { + logger.severe("Error sending message over socket: " + e.getMessage()); + e.printStackTrace(response.getWriter()); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); + } + } + + private HttpClient createHttpClient() { + HttpClient httpClient; + if (System.getenv(GAE_INSTANCE_VAR) != null) { + // If on HTTPS, create client with SSL Context + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + httpClient = new HttpClient(sslContextFactory); + } else { + // local testing on HTTP + httpClient = new HttpClient(); + } + return httpClient; + } + + private WebSocketClient createWebSocketClient() { + return new WebSocketClient(this.httpClient); + } + + private void sendMessageOverWebSocket(String message) throws Exception { + if (!httpClient.isRunning()) { + try { + httpClient.start(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + if (!webSocketClient.isRunning()) { + try { + webSocketClient.start(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + ClientUpgradeRequest request = new ClientUpgradeRequest(); + // Attempt connection + Future future = + webSocketClient.connect(clientSocket, new URI(getWebSocketAddress()), request); + // Wait for Connect + Session session = future.get(); + // Send a message + session.getRemote().sendString(message); + // Close session + session.close(); + } + + /** + * Returns the host:port/echo address a client needs to use to communicate with the server. On App + * engine Flex environments, result will be in the form wss://project-id.appspot.com/echo + */ + public static String getWebSocketAddress() { + // Use ws://127.0.0.1:8080/echo when testing locally + String webSocketHost = "127.0.0.1:8080"; + String webSocketProtocolPrefix = WEBSOCKET_PROTOCOL_PREFIX; + + // On App Engine flexible environment, use wss://project-id.appspot.com/echo + if (System.getenv(GAE_INSTANCE_VAR) != null) { + String projectId = System.getenv(GOOGLE_CLOUD_PROJECT_ENV_VAR); + if (projectId != null) { + String serviceName = System.getenv(GAE_SERVICE_ENV_VAR); + webSocketHost = serviceName + "-dot-" + projectId + APPENGINE_HOST_SUFFIX; + } + Preconditions.checkNotNull(webSocketHost); + // Use wss:// instead of ws:// protocol when connecting over https + webSocketProtocolPrefix = WEBSOCKET_HTTPS_PROTOCOL_PREFIX; + } + return webSocketProtocolPrefix + webSocketHost + ENDPOINT; + } +} diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java new file mode 100644 index 00000000000..07cfafe0f3e --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.flexible.websocket.jettynative; + +import java.io.IOException; +import java.util.logging.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +/* + * Server-side WebSocket : echoes received message back to client. + */ +@WebSocket(maxTextMessageSize = 64 * 1024) +public class ServerSocket { + private Logger logger = Logger.getLogger(SendServlet.class.getName()); + private Session session; + + @OnWebSocketConnect + public void onWebSocketConnect(Session session) { + this.session = session; + logger.fine("Socket Connected: " + session); + } + + @OnWebSocketMessage + public void onWebSocketText(String message) { + logger.fine("Received message: " + message); + try { + // echo message back to client + this.session.getRemote().sendString(message); + } catch (IOException e) { + logger.severe("Error echoing message: " + e.getMessage()); + } + } + + @OnWebSocketClose + public void onWebSocketClose(int statusCode, String reason) { + logger.fine("Socket Closed: [" + statusCode + "] " + reason); + } + + @OnWebSocketError + public void onWebSocketError(Throwable cause) { + logger.severe("Websocket error : " + cause.getMessage()); + } +} diff --git a/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/index.jsp b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/index.jsp new file mode 100644 index 00000000000..8730c529584 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/index.jsp @@ -0,0 +1,33 @@ + + +<%@ page import="com.example.flexible.websocket.jettynative.ClientSocket" %> + + + + + Send a message + +

Publish a message

+
+ + + +
+

Last received messages

+ <%= ClientSocket.getReceivedMessages() %> + + diff --git a/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml new file mode 100644 index 00000000000..475971850a9 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml @@ -0,0 +1,24 @@ + + + + + + true + + -org.eclipse.jetty. + + diff --git a/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/js_client.jsp b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/js_client.jsp new file mode 100644 index 00000000000..ef9d7051928 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/js_client.jsp @@ -0,0 +1,85 @@ + + + +<%@ page import="com.example.flexible.websocket.jettynative.SendServlet" %> + + Google App Engine Flexible Environment - WebSocket Echo + + + +

Echo demo

+
+ + +
+ +
+

Messages:

+
    +
    + +
    +

    Status:

    +
      +
      + + + + diff --git a/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/ClientSocketTest.java b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/ClientSocketTest.java new file mode 100644 index 00000000000..6b8636852ef --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/ClientSocketTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.flexible.websocket.jettynative; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +public class ClientSocketTest { + ClientSocket socket; + + @Before + public void setUp() { + socket = new ClientSocket(); + } + + @Test + public void testOnMessage() { + assertEquals(ClientSocket.getReceivedMessages().size(), 0); + socket.onMessage("test"); + assertEquals(ClientSocket.getReceivedMessages().size(), 1); + } +} diff --git a/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/SendServletTest.java b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/SendServletTest.java new file mode 100644 index 00000000000..37916cb6a37 --- /dev/null +++ b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/SendServletTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.flexible.websocket.jettynative; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class SendServletTest { + @Test + public void testGetWebSocketAddress() { + assertTrue(SendServlet.getWebSocketAddress().contains("/echo")); + } +} diff --git a/functions/helloworld/helloworld/pom.xml b/functions/helloworld/helloworld/pom.xml index 6beba0255ca..a78a587063e 100644 --- a/functions/helloworld/helloworld/pom.xml +++ b/functions/helloworld/helloworld/pom.xml @@ -50,8 +50,8 @@ - 11 - 11 + 21 + 21 diff --git a/functions/v2/pubsub/pom.xml b/functions/v2/pubsub/pom.xml index f50876162b4..72c34790577 100644 --- a/functions/v2/pubsub/pom.xml +++ b/functions/v2/pubsub/pom.xml @@ -31,8 +31,8 @@ - 11 - 11 + 21 + 21 UTF-8 diff --git a/run/helloworld-source/README.md b/run/helloworld-source/README.md new file mode 100644 index 00000000000..0a0f4324b40 --- /dev/null +++ b/run/helloworld-source/README.md @@ -0,0 +1,16 @@ +# Cloud Run Hello World Sample + +This sample shows how to deploy a Hello World [Spring Boot](https://spring.io/projects/spring-boot) +application to Cloud Run using source deploy. + +For more details on how to work with this sample read the [Google Cloud Run Java Samples README](https://github.com/GoogleCloudPlatform/java-docs-samples/tree/main/run). + +[![Run in Google Cloud][run_img]][run_link] + +[run_img]: https://storage.googleapis.com/cloudrun/button.svg +[run_link]: https://deploy.cloud.run/?git_repo=https://github.com/GoogleCloudPlatform/java-docs-samples&dir=run/helloworld + +## Dependencies + +* **Spring Boot**: Web server framework. +* **Junit**: [development] Test running framework. diff --git a/run/helloworld-source/pom.xml b/run/helloworld-source/pom.xml new file mode 100644 index 00000000000..a14dae2cac7 --- /dev/null +++ b/run/helloworld-source/pom.xml @@ -0,0 +1,97 @@ + + + + 4.0.0 + com.example.run + helloworld-source + 0.0.1-SNAPSHOT + jar + + + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + UTF-8 + UTF-8 + 25 + 25 + 3.5.6 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + test + + + junit + junit + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + com.google.cloud.tools + jib-maven-plugin + 3.4.0 + + + gcr.io/PROJECT_ID/helloworld + + + + + + + diff --git a/run/helloworld-source/src/main/java/com/example/helloworld/HelloworldApplication.java b/run/helloworld-source/src/main/java/com/example/helloworld/HelloworldApplication.java new file mode 100644 index 00000000000..c3165c9aca3 --- /dev/null +++ b/run/helloworld-source/src/main/java/com/example/helloworld/HelloworldApplication.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START cloudrun_helloworld_service] + +package com.example.helloworld; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +public class HelloworldApplication { + + @Value("${NAME:World}") + String name; + + @RestController + class HelloworldController { + @GetMapping("/") + String hello() { + return "Hello " + name + "!"; + } + } + + public static void main(String[] args) { + SpringApplication.run(HelloworldApplication.class, args); + } +} +// [END cloudrun_helloworld_service] diff --git a/run/helloworld-source/src/main/resources/application.properties b/run/helloworld-source/src/main/resources/application.properties new file mode 100644 index 00000000000..3cebd9ca826 --- /dev/null +++ b/run/helloworld-source/src/main/resources/application.properties @@ -0,0 +1,16 @@ +# Copyright 2020 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# [START cloudrun_helloworld_properties] +server.port=${PORT:8080} +# [END cloudrun_helloworld_properties] diff --git a/run/helloworld-source/src/test/java/com/example/helloworld/HelloworldApplicationTests.java b/run/helloworld-source/src/test/java/com/example/helloworld/HelloworldApplicationTests.java new file mode 100644 index 00000000000..c35e651b6bc --- /dev/null +++ b/run/helloworld-source/src/test/java/com/example/helloworld/HelloworldApplicationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.helloworld; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +public class HelloworldApplicationTests { + + @Autowired private MockMvc mockMvc; + + @Test + public void returnsHelloWorld() throws Exception { + mockMvc + .perform(get("/")) + .andExpect(status().isOk()) + .andExpect(content().string("Hello World!")); + } +} diff --git a/run/helloworld/pom.xml b/run/helloworld/pom.xml index 70e213d033f..73c3ab340fa 100644 --- a/run/helloworld/pom.xml +++ b/run/helloworld/pom.xml @@ -41,8 +41,8 @@ limitations under the License. UTF-8 UTF-8 - 17 - 17 + 25 + 25 3.2.2