messages) {
+ StringBuilder sb = new StringBuilder();
+ for (Message message : messages) {
+ sb.append("");
+ sb.append("" + message.getMessageId() + " ");
+ sb.append("" + message.getData() + " ");
+ sb.append("" + message.getPublishTime() + " ");
+ sb.append(" ");
+ }
+ return sb.toString();
+ }
+
+ private PubSubHome() { }
+}
diff --git a/flexible/java-8/pubsub/src/main/java/com/example/flexible/pubsub/PubSubPublish.java b/flexible/java-8/pubsub/src/main/java/com/example/flexible/pubsub/PubSubPublish.java
new file mode 100644
index 00000000000..e76b681952e
--- /dev/null
+++ b/flexible/java-8/pubsub/src/main/java/com/example/flexible/pubsub/PubSubPublish.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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.pubsub;
+
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.pubsub.v1.Publisher;
+import com.google.protobuf.ByteString;
+import com.google.pubsub.v1.ProjectTopicName;
+import com.google.pubsub.v1.PubsubMessage;
+import java.io.IOException;
+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.HttpStatus;
+
+// [START gae_flex_pubsub_publish]
+@WebServlet(name = "Publish with PubSub", value = "/pubsub/publish")
+public class PubSubPublish extends HttpServlet {
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ Publisher publisher = this.publisher;
+ try {
+ String topicId = System.getenv("PUBSUB_TOPIC");
+ // create a publisher on the topic
+ if (publisher == null) {
+ publisher = Publisher.newBuilder(
+ ProjectTopicName.of(ServiceOptions.getDefaultProjectId(), topicId))
+ .build();
+ }
+ // construct a pubsub message from the payload
+ final String payload = req.getParameter("payload");
+ PubsubMessage pubsubMessage =
+ PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8(payload)).build();
+
+ publisher.publish(pubsubMessage);
+ // redirect to home page
+ resp.sendRedirect("/");
+ } catch (Exception e) {
+ resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ }
+ // [END gae_flex_pubsub_publish]
+
+ private Publisher publisher;
+
+ public PubSubPublish() { }
+
+ PubSubPublish(Publisher publisher) {
+ this.publisher = publisher;
+ }
+}
diff --git a/flexible/java-8/pubsub/src/main/java/com/example/flexible/pubsub/PubSubPush.java b/flexible/java-8/pubsub/src/main/java/com/example/flexible/pubsub/PubSubPush.java
new file mode 100644
index 00000000000..33179e0c19d
--- /dev/null
+++ b/flexible/java-8/pubsub/src/main/java/com/example/flexible/pubsub/PubSubPush.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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.pubsub;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.util.Base64;
+import java.util.stream.Collectors;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START gae_flex_pubsub_push]
+@WebServlet(value = "/pubsub/push")
+public class PubSubPush extends HttpServlet {
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");
+ // Do not process message if request token does not match pubsubVerificationToken
+ if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0) {
+ resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ // parse message object from "message" field in the request body json
+ // decode message data from base64
+ Message message = getMessage(req);
+ try {
+ messageRepository.save(message);
+ // 200, 201, 204, 102 status codes are interpreted as success by the Pub/Sub system
+ resp.setStatus(HttpServletResponse.SC_OK);
+ } catch (Exception e) {
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+ // [END gae_flex_pubsub_push]
+
+ private Message getMessage(HttpServletRequest request) throws IOException {
+ String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));
+ JsonElement jsonRoot = jsonParser.parse(requestBody);
+ String messageStr = jsonRoot.getAsJsonObject().get("message").toString();
+ Message message = gson.fromJson(messageStr, Message.class);
+ // decode from base64
+ String decoded = decode(message.getData());
+ message.setData(decoded);
+ return message;
+ }
+
+ private String decode(String data) {
+ return new String(Base64.getDecoder().decode(data));
+ }
+
+ private final Gson gson = new Gson();
+ private final JsonParser jsonParser = new JsonParser();
+ private MessageRepository messageRepository;
+
+ PubSubPush(MessageRepository messageRepository) {
+ this.messageRepository = messageRepository;
+ }
+
+ public PubSubPush() {
+ this.messageRepository = MessageRepositoryImpl.getInstance();
+ }
+}
diff --git a/flexible/java-8/pubsub/src/main/test/com/example/flexible/pubsub/PubSubPublishTest.java b/flexible/java-8/pubsub/src/main/test/com/example/flexible/pubsub/PubSubPublishTest.java
new file mode 100644
index 00000000000..1d8820c2b2c
--- /dev/null
+++ b/flexible/java-8/pubsub/src/main/test/com/example/flexible/pubsub/PubSubPublishTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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.pubsub;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.gax.core.SettableApiFuture;
+import com.google.cloud.pubsub.v1.Publisher;
+import com.google.protobuf.ByteString;
+import com.google.pubsub.v1.PubsubMessage;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+public class PubSubPublishTest {
+
+ @Test
+ public void servletPublishesPayloadMessage() throws Exception {
+ assertNotNull(System.getenv("PUBSUB_TOPIC"));
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getParameter("payload")).thenReturn("test-message");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ Publisher publisher = mock(Publisher.class);
+ PubsubMessage message = PubsubMessage.newBuilder()
+ .setData(ByteString.copyFromUtf8("test-message")).build();
+ when(publisher.publish(eq(message))).thenReturn(SettableApiFuture.create());
+ PubSubPublish pubSubPublish = new PubSubPublish(publisher);
+ // verify content of published test message
+ pubSubPublish.doPost(request, response);
+ verify(publisher, times(1)).publish(eq(message));
+ }
+}
diff --git a/flexible/java-8/pubsub/src/main/test/com/example/flexible/pubsub/PubSubPushTest.java b/flexible/java-8/pubsub/src/main/test/com/example/flexible/pubsub/PubSubPushTest.java
new file mode 100644
index 00000000000..5b57af5b116
--- /dev/null
+++ b/flexible/java-8/pubsub/src/main/test/com/example/flexible/pubsub/PubSubPushTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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.pubsub;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.BufferedReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+/**
+ * Copyright 2017 Google Inc.
+ *
+ * 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.
+ */
+
+public class PubSubPushTest {
+
+ @Test
+ public void messageReceivedOverPushEndPointIsSaved() throws Exception {
+ MessageRepository messageRepository = mock(MessageRepository.class);
+ List messages = new ArrayList<>();
+ doAnswer((invocation) -> {
+ messages.add((Message)invocation.getArguments()[0]);
+ return null;
+ }
+ ).when(messageRepository).save(any(Message.class));
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ assertNotNull(System.getenv("PUBSUB_VERIFICATION_TOKEN"));
+ when(request.getParameter("token"))
+ .thenReturn(System.getenv("PUBSUB_VERIFICATION_TOKEN"));
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ BufferedReader reader = mock(BufferedReader.class);
+ when (request.getReader()).thenReturn(reader);
+ Stream requestBody = Stream.of(
+ "{\"message\":{\"data\":\"dGVzdA==\",\"attributes\":{},"
+ + "\"messageId\":\"91010751788941\",\"publishTime\":\"2017-04-05T23:16:42.302Z\"}}");
+ when(reader.lines()).thenReturn(requestBody);
+ PubSubPush servlet = new PubSubPush(messageRepository);
+ assertEquals(messages.size(), 0);
+ servlet.doPost(request, response);
+ assertEquals(messages.size(), 1);
+ }
+}
+
diff --git a/flexible/java-8/pubsub/src/main/webapp/index.jsp b/flexible/java-8/pubsub/src/main/webapp/index.jsp
new file mode 100644
index 00000000000..fa12f02a14d
--- /dev/null
+++ b/flexible/java-8/pubsub/src/main/webapp/index.jsp
@@ -0,0 +1,24 @@
+<%@ page import="com.example.flexible.pubsub.PubSubHome" %>
+
+
+
+
+ An example of using PubSub on App Engine Flex
+
+ Publish a message
+
+ Last received messages
+
+
+ Id
+ Data
+ PublishTime
+
+ <%= PubSubHome.getReceivedMessages() %>
+
+
+
diff --git a/flexible/java-8/sparkjava/README.md b/flexible/java-8/sparkjava/README.md
new file mode 100644
index 00000000000..692ede17b80
--- /dev/null
+++ b/flexible/java-8/sparkjava/README.md
@@ -0,0 +1,113 @@
+# SparkJava on App Engine Flexible Environment
+
+
+
+
+This app demonstrates how to use [Datastore with the Google Cloud client
+library](https://github.com/GoogleCloudPlatform/google-cloud-java/tree/main/google-cloud-datastore)
+from within an [App Engine flexible
+environment](https://cloud.google.com/appengine/docs/flexible/java/hello-world)
+project using [SparkJava](http://sparkjava.com/). The app allows you to create
+and modify a database of "users", which contains their ID, name, and email
+information.
+
+The Google Cloud client library is an idiomatic Java client for [Google Cloud
+Platform](https://cloud.google.com/) services. Read more about the library
+[here](https://github.com/GoogleCloudPlatform/google-cloud-java).
+
+Setup
+-----
+
+1. Create a Google Cloud project with the Datastore API enabled.
+ [Follow these
+ instructions](https://cloud.google.com/docs/authentication#preparation) to
+ get your project set up. If you wish to deploy this application, you will
+ also need to [enable
+ billing](https://support.google.com/cloud/?rd=2#topic=6288636).
+
+2. Set up the local development environment by [installing the Google Cloud
+ SDK](https://cloud.google.com/sdk/) and running the following commands in
+ command line: `gcloud auth application-default login` and `gcloud config set project [YOUR
+ PROJECT ID]`.
+
+3. Ensure that you have Maven installed and configured to use Java 8. See
+ installation instructions [here](https://maven.apache.org/install.html).
+
+Running locally
+---------------
+
+Run the application on your local machine by typing the following into your
+command line from the `sparkjava` directory: `mvn clean package exec:java`.
+Navigate to `localhost:8080` to view and interact with the application.
+
+Deploying
+---------
+
+If you've enabled billing (step 1 in [Setup](#Setup)), you can deploy the
+application to the web by running `mvn clean package appengine:deploy` from your command line
+(from the `sparkjava` directory).
+
+How does it work?
+-----------------
+
+You'll notice that the source code is split into three folders: `appengine`,
+`java/com/google/appengine/sparkdemo`, and `resource/public`. The `appengine`
+folder contains a `Dockerfile` and an `app.yaml`, necessary files to [configure
+the VM
+environment](https://cloud.google.com/appengine/docs/managed-vms/config). The
+`java/com/google/appengine/sparkdemo` folder contains the controller code,
+which uses the Google Cloud client library to modify the records in the Google Cloud
+Datastore. Finally, the `resource/public` folder contains the home webpage,
+which uses jQuery to send HTTP requests to create, remove, and update records.
+
+Spark runs the [`main`
+method](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/managedvms/sparkjava-demo/src/main/java/com/google/appengine/sparkdemo/Main.java)
+upon server startup. The `main` method creates the controller,
+[`UserController`](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/managedvms/sparkjava-demo/src/main/java/com/google/appengine/sparkdemo/UserController.java).
+The URIs used to send HTTP requests in the [home
+page](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/managedvms/sparkjava-demo/src/main/resources/public/index.html)
+correspond to methods in the `UserController` class. For example, the
+`index.html` code for `create` makes a `POST` request to the path `/api/users`
+with a body containing the name and email of a user to add. `UserController`
+contains the following code to process that request:
+
+```java
+post("/api/users", (req, res) -> userService.createUser(
+ req.queryParams("name"),
+ req.queryParams("email),
+), json());
+```
+This code snippet gets the name and email of the user from the POST request and
+passes it to `createUser` (in
+[`UserService.java`](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/managedvms/sparkjava-demo/src/main/java/com/google/appengine/sparkdemo/UserService.java))
+to create a database record using the Google Cloud client library. If you want
+a more in-depth tutorial on using Google Cloud client library Datastore client,
+see the [Getting
+Started](https://github.com/GoogleCloudPlatform/google-cloud-java/tree/main/google-cloud-datastore#getting-started)
+section of the client library documentation.
+
+Communication with the Google Cloud Datastore requires authentication and
+setting a project ID. When running locally, the Google Cloud client library
+automatically detects your credentials and project ID because you logged into
+the Google Cloud SDK and set your project ID. There are also many other options
+for authenticating and setting a project ID. To read more, see the
+[Authentication](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication)
+and [Specifying a Project
+ID](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id)
+sections of the client library documentation.
+
+You built and ran this application using Maven. To read more about using Maven
+with App Engine flexible environment, see the [Using Apache Maven
+documentation](https://cloud.google.com/appengine/docs/flexible/java/using-maven).
+While this particular project uses Maven, the Google Cloud client library
+packages can also be accessed using Gradle and SBT. See how to obtain the
+dependency in the [Quickstart
+section](https://github.com/GoogleCloudPlatform/google-cloud-java#quickstart)
+of the client library documentation.
+
+License
+-------
+
+Apache 2.0 - See
+[LICENSE](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/LICENSE)
+for more information.
diff --git a/flexible/java-8/sparkjava/jenkins.sh b/flexible/java-8/sparkjava/jenkins.sh
new file mode 100755
index 00000000000..3ccdfa2e240
--- /dev/null
+++ b/flexible/java-8/sparkjava/jenkins.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+# Fail on non-zero return and print command to stdout
+set -xe
+
+# Jenkins Test Script
+function runtests () {
+ curl -X GET \
+ "https://${2}-dot-${1}.appspot.com/api/users" | \
+ tee "$ERROR_OUTPUT_DIR/response.json" | \
+ grep "^\\["
+}
+
+# Jenkins provides values for GOOGLE_PROJECT_ID and GOOGLE_VERSION_ID
+
+# Test with Maven
+mvn clean package appengine:deploy \
+ -Dapp.deploy.version="${GOOGLE_VERSION_ID}" \
+ -Dapp.deploy.promote=false
+
+# End-2-End tests
+runtests "${GOOGLE_PROJECT_ID}" "${GOOGLE_VERSION_ID}"
diff --git a/flexible/java-8/sparkjava/pom.xml b/flexible/java-8/sparkjava/pom.xml
new file mode 100644
index 00000000000..1efd60d8c40
--- /dev/null
+++ b/flexible/java-8/sparkjava/pom.xml
@@ -0,0 +1,117 @@
+
+
+
+ 4.0.0
+ com.google.appengine.sparkdemo
+ spark
+ 1.0
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 1.8
+ 1.8
+
+ 2.4.3
+ ${project.build.directory}/spark-1.0-jar-with-dependencies.jar
+
+
+
+
+ com.sparkjava
+ spark-core
+ 2.9.3
+
+
+ org.slf4j
+ slf4j-simple
+ 1.8.0-beta4
+
+
+ com.google.code.gson
+ gson
+ 2.10
+
+
+ junit
+ junit
+ 4.13.2
+
+
+ com.google.cloud
+ google-cloud-datastore
+ 2.13.2
+
+
+
+
+
+ maven-assembly-plugin
+
+
+ package
+
+ single
+
+
+
+
+
+ jar-with-dependencies
+
+
+
+ com.google.appengine.sparkdemo.Main
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ com.google.appengine.sparkdemo.Main
+
+ -jar
+ ${app.stage.stagingDirectory}/spark-1.0-jar-with-dependencies.jar
+
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine.maven.plugin}
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+
+
+
+
+
diff --git a/flexible/java-8/sparkjava/src/main/appengine/app.yaml b/flexible/java-8/sparkjava/src/main/appengine/app.yaml
new file mode 100644
index 00000000000..731b06c68b7
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/main/appengine/app.yaml
@@ -0,0 +1,20 @@
+# Copyright 2023 Google Inc.
+#
+# 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:
+ jdk: openjdk8
diff --git a/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/Main.java b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/Main.java
new file mode 100644
index 00000000000..036751f80a4
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/Main.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.appengine.sparkdemo;
+
+import static spark.Spark.port;
+
+import com.google.cloud.datastore.DatastoreOptions;
+
+public class Main {
+
+ /**
+ * Starts the webapp on localhost:8080.
+ */
+ public static void main(String[] args) {
+ port(8080);
+ String kind = "DemoUser";
+ if (args != null) {
+ for (String arg : args) {
+ if (arg.startsWith("kind=")) {
+ kind = arg.substring("kind=".length());
+ }
+ }
+ }
+ UserController userController = new UserController(new UserService(
+ DatastoreOptions.getDefaultInstance().getService(), kind));
+ }
+}
diff --git a/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/ResponseError.java b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/ResponseError.java
new file mode 100644
index 00000000000..7654a80dbbc
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/ResponseError.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.appengine.sparkdemo;
+
+public class ResponseError {
+
+ private String message;
+
+ public ResponseError(String message, String... args) {
+ this.message = String.format(message, (Object) args);
+ }
+
+ public ResponseError(Exception ex) {
+ this.message = ex.getMessage();
+ }
+}
diff --git a/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/User.java b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/User.java
new file mode 100644
index 00000000000..2aa82c6c598
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/User.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.appengine.sparkdemo;
+
+import java.util.UUID;
+
+public class User {
+
+ private String id;
+ private String name;
+ private String email;
+
+ /**
+ * Construct a user given a name and email. An ID is auto-generated for the user.
+ */
+ public User(String name, String email) {
+ this(UUID.randomUUID().toString(), name, email);
+ }
+
+ /**
+ * Construct a user given an ID, name, and email.
+ */
+ public User(String id, String name, String email) {
+ this.id = id;
+ this.email = email;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/UserController.java b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/UserController.java
new file mode 100644
index 00000000000..53fee59e3ba
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/UserController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.appengine.sparkdemo;
+
+import static spark.Spark.after;
+import static spark.Spark.delete;
+import static spark.Spark.exception;
+import static spark.Spark.get;
+import static spark.Spark.post;
+import static spark.Spark.put;
+
+import com.google.gson.Gson;
+import spark.ResponseTransformer;
+import spark.Spark;
+
+public class UserController {
+
+ /**
+ * Creates a controller that maps requests to actions.
+ */
+ public UserController(final UserService userService) {
+ Spark.staticFileLocation("/public");
+
+ get("/api/users", (req, res) -> userService.getAllUsers(), json());
+
+ get("/api/users/:id", (req, res) -> userService.getUser(req.params(":id")), json());
+
+ post("/api/users",
+ (req, res) -> userService.createUser(req.queryParams("name"), req.queryParams("email")),
+ json());
+
+ put("/api/users/:id", (req, res) -> userService.updateUser(
+ req.params(":id"),
+ req.queryParams("name"),
+ req.queryParams("email")
+ ), json());
+
+ delete("/api/users/:id", (req, res) -> userService.deleteUser(req.params(":id")), json());
+
+ after((req, res) -> {
+ res.type("application/json");
+ });
+
+ exception(IllegalArgumentException.class, (error, req, res) -> {
+ res.status(400);
+ res.body(toJson(new ResponseError(error)));
+ });
+ }
+
+ private static String toJson(Object object) {
+ return new Gson().toJson(object);
+ }
+
+ private static ResponseTransformer json() {
+ return UserController::toJson;
+ }
+}
diff --git a/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/UserService.java b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/UserService.java
new file mode 100644
index 00000000000..d2661fc649c
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/main/java/com/google/appengine/sparkdemo/UserService.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.appengine.sparkdemo;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.cloud.datastore.Datastore;
+import com.google.cloud.datastore.Entity;
+import com.google.cloud.datastore.Key;
+import com.google.cloud.datastore.KeyFactory;
+import com.google.cloud.datastore.Query;
+import com.google.cloud.datastore.QueryResults;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UserService {
+
+ private final Datastore datastore;
+ private final KeyFactory keyFactory;
+ private final String kind;
+
+ /**
+ * Constructor for UserService.
+ *
+ * @param datastore service object to execute requests
+ * @param kind the kind for the Datastore entities in this demo
+ */
+ public UserService(Datastore datastore, String kind) {
+ this.datastore = datastore;
+ this.keyFactory = datastore.newKeyFactory().setKind(kind);
+ this.kind = kind;
+ }
+
+ /**
+ * Return a list of all users.
+ */
+ public List getAllUsers() {
+ Query query =
+ Query.newGqlQueryBuilder(Query.ResultType.ENTITY, "SELECT * FROM " + kind).build();
+ QueryResults results = datastore.run(query);
+ List users = new ArrayList<>();
+ while (results.hasNext()) {
+ Entity result = results.next();
+ users.add(
+ new User(result.getString("id"), result.getString("name"), result.getString("email")));
+ }
+ return users;
+ }
+
+ /**
+ * Return the user with the given id.
+ */
+ User getUser(String id) {
+ Entity entity = datastore.get(keyFactory.newKey(id));
+ return entity == null
+ ? null
+ : new User(entity.getString("id"), entity.getString("name"), entity.getString("email"));
+ }
+
+ /**
+ * Create a new user and add it to Cloud Datastore.
+ */
+ public User createUser(String name, String email) {
+ failIfInvalid(name, email);
+ User user = new User(name, email);
+ Key key = keyFactory.newKey(user.getId());
+ Entity entity = Entity.newBuilder(key)
+ .set("id", user.getId())
+ .set("name", name)
+ .set("email", email)
+ .build();
+ datastore.add(entity);
+ return user;
+ }
+
+ /**
+ * Delete a user from Cloud Datastore.
+ */
+ public String deleteUser(String id) {
+ Key key = keyFactory.newKey(id);
+ datastore.delete(key);
+ return "ok";
+ }
+
+ /**
+ * Updates a user in Cloud Datastore.
+ */
+ public User updateUser(String id, String name, String email) {
+ failIfInvalid(name, email);
+ Key key = keyFactory.newKey(id);
+ Entity entity = datastore.get(key);
+ if (entity == null) {
+ throw new IllegalArgumentException("No user with id '" + id + "' found");
+ } else {
+ entity = Entity.newBuilder(entity)
+ .set("id", id)
+ .set("name", name)
+ .set("email", email)
+ .build();
+ datastore.update(entity);
+ }
+ return new User(id, name, email);
+ }
+
+ private void failIfInvalid(String name, String email) {
+ checkArgument(name != null && !name.isEmpty(), "Parameter 'name' cannot be empty");
+ checkArgument(email != null && !email.isEmpty(), "Parameter 'email' cannot be empty");
+ }
+}
diff --git a/flexible/java-8/sparkjava/src/main/resources/public/index.html b/flexible/java-8/sparkjava/src/main/resources/public/index.html
new file mode 100644
index 00000000000..fceb22e9f35
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/main/resources/public/index.html
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
User Database
+
Using App Engine Flexible, Google Cloud Datastore, and SparkJava.
+
+
+
+
+
+
+
diff --git a/flexible/java-8/sparkjava/src/test/java/com/google/appengine/sparkdemo/UserControllerTest.java b/flexible/java-8/sparkjava/src/test/java/com/google/appengine/sparkdemo/UserControllerTest.java
new file mode 100644
index 00000000000..6700600899c
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/test/java/com/google/appengine/sparkdemo/UserControllerTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.appengine.sparkdemo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.UUID;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import spark.Spark;
+import spark.utils.IOUtils;
+
+public class UserControllerTest {
+
+ private static final String USER_NAME = "myName";
+ private static final String USER_EMAIL = "my@email.com";
+ private static String userId;
+
+ @BeforeClass
+ public static void beforeClass() {
+ Main.main(new String[] {"kind=DemoUser" + UUID.randomUUID().toString().replaceAll("-", "")});
+ Spark.awaitInitialization();
+ }
+
+ @Before
+ public void setUp() throws IOException {
+ userId = createUser(USER_NAME, USER_EMAIL).getId();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ deleteUser(userId);
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ Spark.stop();
+ }
+
+ @Test
+ public void testGetAllUsers() throws IOException {
+ User[] users = getAllUsers();
+ assertTrue(users.length <= 1);
+ }
+
+ @Test
+ public void testCreateUser() throws IOException {
+ User user = createUser(USER_NAME, USER_EMAIL);
+ assertNotNull(user.getId());
+ assertEquals(USER_NAME, user.getName());
+ assertEquals(USER_EMAIL, user.getEmail());
+ }
+
+ @Test
+ public void testCreateUserInvalidRequest() {
+ try {
+ executeRequest("POST", "/api/users?name=&email=");
+ fail("Should fail due to an invalid request.");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().startsWith("Server returned HTTP response code: 400 for URL"));
+ }
+ }
+
+ @Test
+ public void testDeleteUser() throws IOException {
+ assertNotNull(getUser(userId));
+ assertEquals("\"ok\"", deleteUser(userId));
+ assertNull(getUser(userId));
+ }
+
+ @Test
+ public void updateUser() throws IOException {
+ String newName = "myNewName";
+ String newEmail = "mynew@email.com";
+ String responseStr = executeRequest(
+ "PUT",
+ "/api/users/" + userId + "?id=" + userId + "&name=" + newName + "&email=" + newEmail);
+ User updatedUser = new Gson().fromJson(responseStr, User.class);
+ assertEquals(userId, updatedUser.getId());
+ assertEquals(newName, updatedUser.getName());
+ assertEquals(newEmail, updatedUser.getEmail());
+ }
+
+ private static String executeRequest(String method, String path) throws IOException {
+ URL url = new URL("http://localhost:8080" + path);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod(method);
+ connection.setDoOutput(true);
+ connection.connect();
+ return IOUtils.toString(connection.getInputStream());
+ }
+
+ private static User createUser(String name, String email) throws IOException {
+ return new Gson().fromJson(
+ executeRequest("POST", "/api/users?name=" + name + "&email=" + email), User.class);
+ }
+
+ private static String deleteUser(String id) throws IOException {
+ return executeRequest("DELETE", "/api/users/" + id);
+ }
+
+ private static User getUser(String id) throws IOException {
+ return new Gson().fromJson(executeRequest("GET", "/api/users/" + id), User.class);
+ }
+
+ private static User[] getAllUsers() throws IOException {
+ return new Gson().fromJson(executeRequest("GET", "/api/users"), User[].class);
+ }
+}
diff --git a/flexible/java-8/sparkjava/src/test/java/com/google/appengine/sparkdemo/UserServiceTest.java b/flexible/java-8/sparkjava/src/test/java/com/google/appengine/sparkdemo/UserServiceTest.java
new file mode 100644
index 00000000000..24c7f8677c8
--- /dev/null
+++ b/flexible/java-8/sparkjava/src/test/java/com/google/appengine/sparkdemo/UserServiceTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.appengine.sparkdemo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.datastore.Datastore;
+import com.google.cloud.datastore.DatastoreOptions;
+import com.google.cloud.datastore.Entity;
+import com.google.cloud.datastore.Key;
+import com.google.cloud.datastore.Query;
+import com.google.cloud.datastore.QueryResults;
+import com.google.cloud.datastore.StructuredQuery;
+import com.google.cloud.datastore.testing.LocalDatastoreHelper;
+import com.google.common.collect.Iterators;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.threeten.bp.Duration;
+
+public class UserServiceTest {
+
+ private static final LocalDatastoreHelper HELPER = LocalDatastoreHelper.create(1.0);
+ private static final DatastoreOptions DATASTORE_OPTIONS = HELPER.getOptions();
+ private static final Datastore DATASTORE = DATASTORE_OPTIONS.getService();
+ private static final String KIND = "DemoUser";
+ private static final UserService USER_SERVICE = new UserService(DATASTORE, KIND);
+ private static final String USER_ID = "myId";
+ private static final String USER_NAME = "myName";
+ private static final String USER_EMAIL = "my@email.com";
+ private static final User USER = new User(USER_ID, USER_NAME, USER_EMAIL);
+ private static final Key USER_KEY =
+ Key.newBuilder(DATASTORE_OPTIONS.getProjectId(), KIND, USER_ID).build();
+ private static final Entity USER_RECORD = Entity.newBuilder(USER_KEY)
+ .set("id", USER_ID)
+ .set("name", USER_NAME)
+ .set("email", USER_EMAIL)
+ .build();
+
+ @BeforeClass
+ public static void beforeClass() throws IOException, InterruptedException {
+ HELPER.start();
+ }
+
+ @Before
+ public void setUp() {
+ StructuredQuery query = Query.newKeyQueryBuilder().build();
+ QueryResults result = DATASTORE.run(query);
+ DATASTORE.delete(Iterators.toArray(result, Key.class));
+ DATASTORE.add(USER_RECORD);
+ }
+
+ @AfterClass
+ public static void afterClass() throws IOException, InterruptedException, TimeoutException {
+ HELPER.stop(Duration.ofMinutes(1));
+ }
+
+ @Test
+ public void testGetAllUsers() {
+ List allUsers = USER_SERVICE.getAllUsers();
+ assertEquals(1, allUsers.size());
+ User actualUser = allUsers.get(0);
+ assertEquals(USER.getId(), actualUser.getId());
+ assertEquals(USER.getName(), actualUser.getName());
+ assertEquals(USER.getEmail(), actualUser.getEmail());
+ }
+
+ @Test
+ public void testCreateUser() {
+ String name = "myNewName";
+ String email = "mynew@email.com";
+ User actualUser = USER_SERVICE.createUser(name, email);
+ assertEquals(name, actualUser.getName());
+ assertEquals(email, actualUser.getEmail());
+ assertNotNull(actualUser.getId());
+ try {
+ USER_SERVICE.createUser(null, email);
+ fail("Expected to fail because name is null.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parameter 'name' cannot be empty", e.getMessage());
+ }
+ try {
+ USER_SERVICE.createUser(name, null);
+ fail("Expected to fail because email is null.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parameter 'email' cannot be empty", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDeleteUser() {
+ String result = USER_SERVICE.deleteUser(USER_ID);
+ assertEquals("ok", result);
+ assertNull(DATASTORE.get(USER_KEY));
+ }
+
+ @Test
+ public void testUpdateUser() {
+ String newName = "myNewName";
+ String newEmail = "mynew@email.com";
+ User updatedUser = USER_SERVICE.updateUser(USER_ID, newName, newEmail);
+ assertEquals(USER_ID, updatedUser.getId());
+ assertEquals(newName, updatedUser.getName());
+ assertEquals(newEmail, updatedUser.getEmail());
+ try {
+ USER_SERVICE.updateUser(USER_ID, null, USER_EMAIL);
+ fail("Expected to fail because name is null.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parameter 'name' cannot be empty", e.getMessage());
+ }
+ try {
+ USER_SERVICE.updateUser(USER_ID, USER_NAME, null);
+ fail("Expected to fail because email is null.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parameter 'email' cannot be empty", e.getMessage());
+ }
+ }
+}
diff --git a/flexible/java-8/static-files/pom.xml b/flexible/java-8/static-files/pom.xml
new file mode 100644
index 00000000000..7d2639c0874
--- /dev/null
+++ b/flexible/java-8/static-files/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.flexible
+ staticfiles
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 1.8
+ 1.8
+
+ false
+
+ 2.4.3
+ 9.4.44.v20210927
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.3.2
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.3.2
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine.maven.plugin}
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty}
+
+
+
+
diff --git a/flexible/java-8/static-files/src/main/appengine/app.yaml b/flexible/java-8/static-files/src/main/appengine/app.yaml
new file mode 100644
index 00000000000..cd1d835c2d1
--- /dev/null
+++ b/flexible/java-8/static-files/src/main/appengine/app.yaml
@@ -0,0 +1,20 @@
+# Copyright 2023 Google Inc.
+#
+# 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
+
+handlers:
+- url: /.*
+ script: this field is required, but ignored
diff --git a/flexible/java-8/static-files/src/main/webapp/index.html b/flexible/java-8/static-files/src/main/webapp/index.html
new file mode 100644
index 00000000000..d1643e9a6b1
--- /dev/null
+++ b/flexible/java-8/static-files/src/main/webapp/index.html
@@ -0,0 +1,10 @@
+
+
+
+Static Files
+
+
+
+ This is a static file serving example.
+
+
diff --git a/flexible/java-8/static-files/src/main/webapp/stylesheets/styles.css b/flexible/java-8/static-files/src/main/webapp/stylesheets/styles.css
new file mode 100644
index 00000000000..573f441093f
--- /dev/null
+++ b/flexible/java-8/static-files/src/main/webapp/stylesheets/styles.css
@@ -0,0 +1,4 @@
+body {
+ font-family: Verdana, Helvetica, sans-serif;
+ background-color: #CCCCFF;
+}
diff --git a/flexible/java-8/twilio/README.md b/flexible/java-8/twilio/README.md
new file mode 100644
index 00000000000..2091089d907
--- /dev/null
+++ b/flexible/java-8/twilio/README.md
@@ -0,0 +1,40 @@
+# Java Twilio Voice and SMS Sample for Google App Engine Flexible Environment
+
+
+
+
+This sample demonstrates how to use [Twilio](https://www.twilio.com) on [Google
+App flexible environment][aeflex-docs].
+
+See the [sample application documentaion][sample-docs] for more detailed
+instructions.
+
+For more information about Twilio, see their [Java quickstart
+tutorials](https://www.twilio.com/docs/quickstart/java).
+
+[aeflex-docs]: https://cloud.google.com/appengine/docs/flexible/
+[sample-docs]: https://cloud.google.com/appengine/docs/flexible/java/using-sms-and-voice-services-via-twilio
+
+
+## Setup
+
+Before you can run or deploy the sample, you will need to do the following:
+
+1. [Create a Twilio Account](http://ahoy.twilio.com/googlecloudplatform). Google
+ App Engine customers receive a complimentary credit for SMS messages and
+ inbound messages.
+1. Create a number on twilio, and configure the voice request URL to be
+ ``https://your-app-id.appspot.com/call/receive`` and the SMS request URL to
+ be ``https://your-app-id.appspot.com/sms/receive``.
+1. Configure your Twilio settings in the environment variables section in
+ ``src/main/appengine/app.yaml``.
+
+## Running locally
+
+You can run the application locally to test the callbacks and SMS sending. You
+will need to set environment variables before starting your application:
+
+ $ export TWILIO_ACCOUNT_SID=[your-twilio-accoun-sid]
+ $ export TWILIO_AUTH_TOKEN=[your-twilio-auth-token]
+ $ export TWILIO_NUMBER=[your-twilio-number]
+ $ mvn clean jetty:run
diff --git a/flexible/java-8/twilio/pom.xml b/flexible/java-8/twilio/pom.xml
new file mode 100644
index 00000000000..e685584bb09
--- /dev/null
+++ b/flexible/java-8/twilio/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.flexible
+ twilio
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 1.8
+ 1.8
+
+ false
+
+ 2.4.3
+ 9.4.44.v20210927
+
+
+
+
+
+ com.twilio.sdk
+ twilio-java-sdk
+ 6.3.0
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.3.2
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine.maven.plugin}
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty}
+
+
+
+
diff --git a/flexible/java-8/twilio/src/main/appengine/app.yaml b/flexible/java-8/twilio/src/main/appengine/app.yaml
new file mode 100644
index 00000000000..9a1543fa5fc
--- /dev/null
+++ b/flexible/java-8/twilio/src/main/appengine/app.yaml
@@ -0,0 +1,27 @@
+# Copyright 2023 Google Inc.
+#
+# 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
+
+handlers:
+- url: /.*
+ script: this field is required, but ignored
+
+# [START gae_flex_twilio_env]
+env_variables:
+ TWILIO_ACCOUNT_SID: YOUR-TWILIO-ACCOUNT-SID
+ TWILIO_AUTH_TOKEN: YOUR-TWILIO-AUTH-TOKEN
+ TWILIO_NUMBER: YOUR-TWILIO-NUMBER
+# [END gae_flex_twilio_env]
diff --git a/flexible/java-8/twilio/src/main/java/com/example/twilio/ReceiveCallServlet.java b/flexible/java-8/twilio/src/main/java/com/example/twilio/ReceiveCallServlet.java
new file mode 100644
index 00000000000..53c8874afa9
--- /dev/null
+++ b/flexible/java-8/twilio/src/main/java/com/example/twilio/ReceiveCallServlet.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.twilio;
+
+import com.twilio.sdk.verbs.Say;
+import com.twilio.sdk.verbs.TwiMLException;
+import com.twilio.sdk.verbs.TwiMLResponse;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START gae_flex_twilio_receive_call]
+@SuppressWarnings("serial")
+@WebServlet(name = "receivecall", value = "/call/receive")
+public class ReceiveCallServlet extends HttpServlet {
+
+ @Override
+ public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException,
+ ServletException {
+ TwiMLResponse twiml = new TwiMLResponse();
+ Say say = new Say("Hello from Twilio!");
+ try {
+ twiml.append(say);
+ } catch (TwiMLException e) {
+ throw new ServletException("Twilio error", e);
+ }
+
+ resp.setContentType("application/xml");
+ resp.getWriter().print(twiml.toXML());
+ }
+}
+// [END gae_flex_twilio_receive_call]
diff --git a/flexible/java-8/twilio/src/main/java/com/example/twilio/ReceiveSmsServlet.java b/flexible/java-8/twilio/src/main/java/com/example/twilio/ReceiveSmsServlet.java
new file mode 100644
index 00000000000..65f99333bca
--- /dev/null
+++ b/flexible/java-8/twilio/src/main/java/com/example/twilio/ReceiveSmsServlet.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.twilio;
+
+import com.twilio.sdk.verbs.Message;
+import com.twilio.sdk.verbs.TwiMLException;
+import com.twilio.sdk.verbs.TwiMLResponse;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// [START gae_flex_twilio_receive_sms]
+@SuppressWarnings("serial")
+@WebServlet(name = "receivesms", value = "/sms/receive")
+public class ReceiveSmsServlet extends HttpServlet {
+
+ @Override
+ public void service(HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException {
+ String fromNumber = request.getParameter("From");
+ String body = request.getParameter("Body");
+ String message = String.format("Hello, %s, you said %s", fromNumber, body);
+
+ TwiMLResponse twiml = new TwiMLResponse();
+ Message sms = new Message(message);
+ try {
+ twiml.append(sms);
+ } catch (TwiMLException e) {
+ throw new ServletException("Twilio error", e);
+ }
+
+ response.setContentType("application/xml");
+ response.getWriter().print(twiml.toXML());
+ }
+}
+// [END gae_flex_twilio_receive_sms]
diff --git a/flexible/java-8/twilio/src/main/java/com/example/twilio/SendSmsServlet.java b/flexible/java-8/twilio/src/main/java/com/example/twilio/SendSmsServlet.java
new file mode 100644
index 00000000000..1fc2ff12418
--- /dev/null
+++ b/flexible/java-8/twilio/src/main/java/com/example/twilio/SendSmsServlet.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.twilio;
+
+import com.twilio.sdk.TwilioRestClient;
+import com.twilio.sdk.TwilioRestException;
+import com.twilio.sdk.resource.factory.MessageFactory;
+import com.twilio.sdk.resource.instance.Account;
+import com.twilio.sdk.resource.instance.Message;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+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.NameValuePair;
+import org.apache.http.message.BasicNameValuePair;
+
+// [START gae_flex_twilio_send_sms]
+@SuppressWarnings("serial")
+@WebServlet(name = "sendsms", value = "/sms/send")
+public class SendSmsServlet extends HttpServlet {
+
+ @Override
+ public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException,
+ ServletException {
+ final String twilioAccountSid = System.getenv("TWILIO_ACCOUNT_SID");
+ final String twilioAuthToken = System.getenv("TWILIO_AUTH_TOKEN");
+ final String twilioNumber = System.getenv("TWILIO_NUMBER");
+ final String toNumber = (String) req.getParameter("to");
+ if (toNumber == null) {
+ resp.getWriter()
+ .print("Please provide the number to message in the \"to\" query string parameter.");
+ return;
+ }
+ TwilioRestClient client = new TwilioRestClient(twilioAccountSid, twilioAuthToken);
+ Account account = client.getAccount();
+ MessageFactory messageFactory = account.getMessageFactory();
+ List params = new ArrayList();
+ params.add(new BasicNameValuePair("To", toNumber));
+ params.add(new BasicNameValuePair("From", twilioNumber));
+ params.add(new BasicNameValuePair("Body", "Hello from Twilio!"));
+ try {
+ Message sms = messageFactory.create(params);
+ resp.getWriter().print(sms.getBody());
+ } catch (TwilioRestException e) {
+ throw new ServletException("Twilio error", e);
+ }
+ }
+}
+// [END gae_flex_twilio_send_sms]
diff --git a/flexible/java-8/websocket-jetty/README.md b/flexible/java-8/websocket-jetty/README.md
new file mode 100644
index 00000000000..13df8d5d35f
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/README.md
@@ -0,0 +1,62 @@
+# 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
+ ```
+ gcloud init
+ ```
+- If this is your first time creating an app engine application
+ ```
+ gcloud appengine create
+ ```
+
+## Local testing
+
+Run using the [Jetty Maven plugin](http://www.eclipse.org/jetty/documentation/9.4.x/jetty-maven-plugin.html).
+```
+mvn jetty:run
+```
+You can then direct your browser to `http://localhost:8080/`
+
+To test the Javascript client, access `http://localhost:8080/js_client.jsp`
+
+## App Engine Flex Deployment
+
+#### `app.yaml` Configuration
+
+App Engine Flex deployment configuration is provided in [app.yaml](src/main/appengine/app.yaml).
+
+Set the environment variable `JETTY_MODULES_ENABLE:websocket` to enable the Jetty websocket module on the Jetty server.
+
+Manual scaling is set to a single instance as we are using an in-memory cache of messages for this sample application.
+
+For more details on configuring your `app.yaml`, please refer to [this resource](https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml).
+
+#### 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).
+
+```
+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-8/websocket-jetty/pom.xml b/flexible/java-8/websocket-jetty/pom.xml
new file mode 100644
index 00000000000..a572837edc3
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.demo
+ native-jetty-websocket-example
+ 1.0-SNAPSHOT
+ war
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 1.8
+ 1.8
+ false
+ 9.4.46.v20220331
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ org.eclipse.jetty.websocket
+ websocket-client
+ ${jetty.version}
+
+
+ org.eclipse.jetty.websocket
+ websocket-servlet
+ ${jetty.version}
+ provided
+
+
+ com.google.guava
+ guava
+ 31.1-jre
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.3.2
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.version}
+
+
+
+
diff --git a/flexible/java-8/websocket-jetty/src/main/appengine/app.yaml b/flexible/java-8/websocket-jetty/src/main/appengine/app.yaml
new file mode 100644
index 00000000000..fad44e84857
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/appengine/app.yaml
@@ -0,0 +1,33 @@
+# Copyright 2018 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
+manual_scaling:
+ instances: 1
+
+handlers:
+- url: /.*
+ script: this field is required, but ignored
+
+env_variables:
+ JETTY_MODULES_ENABLE: websocket
+
+
+# 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-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ClientSocket.java b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ClientSocket.java
new file mode 100644
index 00000000000..6514debeb98
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ClientSocket.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 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-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java
new file mode 100644
index 00000000000..66b177d8b30
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 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-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java
new file mode 100644
index 00000000000..06fd362e646
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2018 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 sslContextFactory = new SslContextFactory();
+ 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-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java
new file mode 100644
index 00000000000..ed82ca2b23a
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 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.WebSocketAdapter;
+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-8/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml b/flexible/java-8/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml
new file mode 100644
index 00000000000..b7dcc424f62
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ true
+
+ -org.eclipse.jetty.
+
+
diff --git a/flexible/java-8/websocket-jetty/src/main/webapp/index.jsp b/flexible/java-8/websocket-jetty/src/main/webapp/index.jsp
new file mode 100644
index 00000000000..1f0db29e101
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/webapp/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-8/websocket-jetty/src/main/webapp/js_client.jsp b/flexible/java-8/websocket-jetty/src/main/webapp/js_client.jsp
new file mode 100644
index 00000000000..38a2d5afe25
--- /dev/null
+++ b/flexible/java-8/websocket-jetty/src/main/webapp/js_client.jsp
@@ -0,0 +1,85 @@
+
+
+
+<%@ page import="com.example.flexible.websocket.jettynative.SendServlet" %>
+
+ Google App Engine Flexible Environment - WebSocket Echo
+
+
+
+ Echo demo
+
+
+
+
+
+
+
+
+
diff --git a/flexible/java-8/websocket-jsr356/README.md b/flexible/java-8/websocket-jsr356/README.md
new file mode 100644
index 00000000000..a7b51ed57be
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/README.md
@@ -0,0 +1,57 @@
+# 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 [JSR-356](https://www.jcp.org/en/jsr/detail?id=356) Java API for the Websocket [client](https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/javax-websocket-client-impl).
+
+## 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
+ ```
+ gcloud init
+ ```
+- If this is your first time creating an app engine application
+ ```
+ gcloud appengine create
+ ```
+
+## Local testing
+
+Run using the [Jetty Maven plugin](http://www.eclipse.org/jetty/documentation/9.4.x/jetty-maven-plugin.html).
+```
+mvn jetty:run
+```
+You can then direct your browser to `http://localhost:8080/`
+
+To test the Javascript client, access `http://localhost:8080/js_client.jsp`
+
+## App Engine Flex Deployment
+
+#### `app.yaml` Configuration
+
+App Engine Flex deployment configuration is provided in [app.yaml](src/main/appengine/app.yaml).
+
+Set the environment variable `JETTY_MODULES_ENABLE:websocket` to enable the Jetty websocket module on the Jetty server.
+
+Manual scaling is set to a single instance as we are using an in-memory cache of messages for this sample application.
+
+For more details on configuring your `app.yaml`, please refer to [this resource](https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml).
+
+#### 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).
+
+```
+ 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`
diff --git a/flexible/java-8/websocket-jsr356/pom.xml b/flexible/java-8/websocket-jsr356/pom.xml
new file mode 100644
index 00000000000..5b63f9442cd
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/pom.xml
@@ -0,0 +1,95 @@
+
+
+ 4.0.0
+ 1.0-SNAPSHOT
+ com.example.flexible
+ appengine-websocket-jsr356
+ war
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 1.8
+ 1.8
+ false
+ 9.4.46.v20220331
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+
+ org.eclipse.jetty.websocket
+ javax-websocket-client-impl
+ ${jetty.version}
+
+
+ javax
+ javaee-api
+ 8.0.1
+
+
+
+ com.google.guava
+ guava
+ 31.1-jre
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.3.2
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.4.4
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.version}
+
+
+
+
diff --git a/flexible/java-8/websocket-jsr356/src/main/appengine/app.yaml b/flexible/java-8/websocket-jsr356/src/main/appengine/app.yaml
new file mode 100644
index 00000000000..fad44e84857
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/src/main/appengine/app.yaml
@@ -0,0 +1,33 @@
+# Copyright 2018 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
+manual_scaling:
+ instances: 1
+
+handlers:
+- url: /.*
+ script: this field is required, but ignored
+
+env_variables:
+ JETTY_MODULES_ENABLE: websocket
+
+
+# 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-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java b/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java
new file mode 100644
index 00000000000..10ea0285cfd
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2018 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.jsr356;
+
+import com.google.common.util.concurrent.SettableFuture;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.ContainerProvider;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+/**
+ * Web socket client example using JSR-356 Java WebSocket API. Sends a message to the server, and
+ * stores the echoed messages received from the server.
+ */
+@ClientEndpoint
+public class ClientSocket {
+
+ private static final Logger logger = Logger.getLogger(ClientSocket.class.getName());
+
+ // 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<>();
+
+ private SettableFuture future = SettableFuture.create();
+ private Session session;
+
+ ClientSocket(URI endpointUri) {
+ try {
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ session = container.connectToServer(this, endpointUri);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @OnOpen
+ public void onOpen(Session session) {
+ future.set(true);
+ }
+
+ /**
+ * Handles message received from the server.
+ *
+ * @param message server message in String format
+ * @param session current session
+ */
+ @OnMessage
+ public void onMessage(String message, Session session) {
+ logger.fine("Received message from server : " + message);
+ messages.add(message);
+ }
+
+ boolean waitOnOpen() throws InterruptedException, ExecutionException {
+ // wait on handling onOpen
+ boolean opened = future.get();
+ logger.fine("Connected to server");
+ return opened;
+ }
+
+ @OnClose
+ public void onClose(CloseReason reason, Session session) {
+ logger.fine("Closing Web Socket: " + reason.getReasonPhrase());
+ }
+
+ void sendMessage(String str) {
+ try {
+ // Send a message to the server
+ logger.fine("Sending message : " + str);
+ session.getAsyncRemote().sendText(str);
+ } catch (Exception e) {
+ logger.severe("Error sending message : " + e.getMessage());
+ }
+ }
+
+ // Retrieve all received messages.
+ public static Collection getReceivedMessages() {
+ return Collections.unmodifiableCollection(messages);
+ }
+
+ @OnError
+ public void logErrors(Throwable t) {
+ logger.severe(t.getMessage());
+ }
+}
diff --git a/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java b/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java
new file mode 100644
index 00000000000..0ea5a04a168
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 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.jsr356;
+
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+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.http.HttpStatus;
+
+@WebServlet("/send")
+/** Servlet that converts the message sent over POST to be over websocket. */
+public class SendServlet extends HttpServlet {
+
+ private Logger logger = Logger.getLogger(SendServlet.class.getName());
+ private final String webSocketAddress = ServerSocket.getWebSocketAddress();
+ private ClientSocket clientSocket;
+
+ private void initializeWebSocket() throws Exception {
+ clientSocket = new ClientSocket(new URI(webSocketAddress));
+ clientSocket.waitOnOpen();
+ logger.info("REST service: open websocket client at " + webSocketAddress);
+ }
+
+ private void sendMessageOverWebSocket(String message) throws Exception {
+ if (clientSocket == null) {
+ try {
+ initializeWebSocket();
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+ clientSocket.sendMessage(message);
+ }
+
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ String message = request.getParameter("message");
+ Preconditions.checkNotNull(message);
+ try {
+ sendMessageOverWebSocket(message);
+ response.sendRedirect("/");
+ } catch (Exception e) {
+ e.printStackTrace(response.getWriter());
+ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
+ }
+ }
+}
diff --git a/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java b/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java
new file mode 100644
index 00000000000..ba7a74de87a
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 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.jsr356;
+
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.util.logging.Logger;
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+/**
+ * WebSocket server example using JSR-356 Java WebSocket API. Echoes back the message received over
+ * the websocket back to the client.
+ */
+@ServerEndpoint("/echo")
+public class ServerSocket {
+
+ private static final Logger logger = Logger.getLogger(ServerSocket.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";
+
+ @OnOpen
+ public void onOpen(Session session) {
+ logger.info("WebSocket Opened: " + session.getId());
+ }
+
+ /**
+ * Handle a message received from the client, and echo back to the client.
+ *
+ * @param message Message in text format
+ * @param session Current active session
+ * @throws IOException error sending message back to client
+ */
+ @OnMessage
+ public void onMessage(String message, Session session) throws IOException {
+ logger.fine("Message Received : " + message);
+ // echo message back to the client
+ session.getAsyncRemote().sendText(message);
+ }
+
+ @OnClose
+ public void onClose(CloseReason reason, Session session) {
+ logger.fine("Closing WebSocket: " + reason.getReasonPhrase());
+ }
+
+ /**
+ * 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-8/websocket-jsr356/src/main/webapp/index.jsp b/flexible/java-8/websocket-jsr356/src/main/webapp/index.jsp
new file mode 100644
index 00000000000..59de3a7c803
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/src/main/webapp/index.jsp
@@ -0,0 +1,33 @@
+
+
+<%@ page import="com.example.flexible.websocket.jsr356.ClientSocket" %>
+
+
+
+
+ Send a message
+
+ Publish a message
+
+ Message:
+
+
+
+ Last received messages
+ <%= ClientSocket.getReceivedMessages() %>
+
+
diff --git a/flexible/java-8/websocket-jsr356/src/main/webapp/js_client.jsp b/flexible/java-8/websocket-jsr356/src/main/webapp/js_client.jsp
new file mode 100644
index 00000000000..00ab0671aa5
--- /dev/null
+++ b/flexible/java-8/websocket-jsr356/src/main/webapp/js_client.jsp
@@ -0,0 +1,85 @@
+
+
+
+<%@ page import="com.example.flexible.websocket.jsr356.ServerSocket" %>
+
+ Google App Engine Flexible Environment - WebSocket Echo
+
+
+
+ Echo demo
+
+
+ Send
+
+
+
+
+
+
+
+
+