diff --git a/appengine-java21/ee8/pubsub/README.md b/appengine-java21/ee8/pubsub/README.md
new file mode 100644
index 00000000000..29a244545ee
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/README.md
@@ -0,0 +1,107 @@
+# Using Google Cloud Pub/Sub on App Engine Standard Java 21 Environment
+
+
+
+
+This sample demonstrates how to use [Google Cloud Pub/Sub][pubsub]
+from [Google App Engine standard environment][ae-docs].
+
+[pubsub]: https://cloud.google.com/pubsub/docs/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+The home page of this application provides a form to publish messages using Google/Cloud PubSub. The application
+then receives these published messages over a push subscription endpoint and then stores in Google Cloud Datastore.
+The home page provides a view of the most recent messages persisted in storage.
+
+## Clone the sample app
+
+Copy the sample apps to your local machine, and cd to the pubsub directory:
+
+```
+git clone https://github.com/GoogleCloudPlatform/java-docs-samples
+cd java-docs-samples/appengine-java21/ee8/pubsub
+```
+
+## Setup
+
+- Make sure [`gcloud`](https://cloud.google.com/sdk/docs/) is installed and initialized:
+```
+ gcloud init
+```
+- If this is the first time you are creating an App Engine project
+```
+ gcloud app create
+```
+- For local development, [set up](https://cloud.google.com/docs/authentication/getting-started) authentication
+- [Enable](https://console.cloud.google.com/launcher/details/google/pubsub.googleapis.com) Pub/Sub API
+
+- Create a topic
+```
+gcloud pubsub topics create
+```
+
+- Create a push subscription, to send messages to a Google Cloud Project URL such as https://.appspot.com/push.
+
+The verification token is used to ensure that the end point only handles requests that are sent matching the verification token.
+You can use `uuidgen` on MacOS X, Windows, and Linux to generate a unique verification token.
+
+```
+gcloud pubsub subscriptions create \
+ --topic \
+ --push-endpoint \
+ https://.appspot.com/pubsub/push?token= \
+ --ack-deadline 30
+```
+
+- Create a subscription for authenticated pushes to send messages to a Google Cloud Project URL such as https://.appspot.com/authenticated-push.
+
+The push auth service account must have Service Account Token Creator Role assigned, which can be done in the Cloud Console [IAM & admin](https://console.cloud.google.com/iam-admin/iam) UI.
+`--push-auth-token-audience` is optional. If set, remember to modify the audience field check in [PubSubAuthenticatedPush.java](src/main/java/com/example/appengine/pubsub/PubSubAuthenticatedPush.java#L48).
+
+```
+gcloud pubsub subscriptions create \
+ --topic \
+ --push-endpoint \
+ https://.appspot.com/pubsub/authenticated-push?token= \
+ --ack-deadline 30 \
+ --push-auth-service-account=[your-service-account-email] \
+ --push-auth-token-audience=example.com
+```
+
+## Run locally
+Set the following environment variables and run using shown Maven command. You can then
+direct your browser to `http://localhost:8080/`
+
+```
+export PUBSUB_TOPIC=
+export PUBSUB_VERIFICATION_TOKEN=
+mvn appengine:run
+```
+
+## Send fake subscription push messages with:
+
+```
+ curl -H "Content-Type: application/json" -i --data @sample_message.json
+ "localhost:8080/pubsub/push?token="
+```
+
+### Authenticated push notifications
+
+Simulating authenticated push requests will fail because requests need to contain a Cloud Pub/Sub-generated JWT in the "Authorization" header.
+
+```
+ curl -H "Content-Type: application/json" -i --data @sample_message.json
+ "localhost:8080/pubsub/authenticated-push?token="
+```
+
+## Deploy
+
+Update the environment variables `PUBSUB_TOPIC` and `PUBSUB_VERIFICATION_TOKEN` in
+[`appengine-web.xml`](src/main/webapp/WEB-INF/appengine-web.xml),
+then:
+
+```
+ mvn clean package appengine:deploy
+```
+
+Direct your browser to `https://project-id.appspot.com`.
diff --git a/appengine-java21/ee8/pubsub/pom.xml b/appengine-java21/ee8/pubsub/pom.xml
new file mode 100644
index 00000000000..5429e96843d
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/pom.xml
@@ -0,0 +1,109 @@
+
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-pubsub
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 1.8
+ 1.8
+ false
+
+
+
+
+
+
+
+ com.google.cloud
+ libraries-bom
+ 26.28.0
+ pom
+ import
+
+
+
+
+
+
+ com.google.cloud
+ google-cloud-pubsub
+
+
+ com.google.cloud
+ google-cloud-datastore
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ jar
+ provided
+
+
+ com.googlecode.jatl
+ jatl
+ 0.2.3
+
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.4.0
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.5.0
+
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ 9.4.53.v20231009
+
+
+
+
+
diff --git a/appengine-java21/ee8/pubsub/sample_message.json b/appengine-java21/ee8/pubsub/sample_message.json
new file mode 100644
index 00000000000..bb912195ba1
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/sample_message.json
@@ -0,0 +1 @@
+{"message":{"data":"dGVzdA==","attributes":{},"messageId":"91010751788941","publishTime":"2017-09-25T23:16:42.302Z"}}
diff --git a/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/Message.java b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/Message.java
new file mode 100644
index 00000000000..13f8d51f7f7
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/Message.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.pubsub;
+
+/**
+ * A message captures information from the Pubsub message received over the push endpoint and is
+ * persisted in storage.
+ */
+public class Message {
+ private String messageId;
+ private String publishTime;
+ private String data;
+
+ public Message(String messageId) {
+ this.messageId = messageId;
+ }
+
+ public String getMessageId() {
+ return messageId;
+ }
+
+ public void setMessageId(String messageId) {
+ this.messageId = messageId;
+ }
+
+ public String getPublishTime() {
+ return publishTime;
+ }
+
+ public void setPublishTime(String publishTime) {
+ this.publishTime = publishTime;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+}
diff --git a/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepository.java b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepository.java
new file mode 100644
index 00000000000..2246cf51353
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepository.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.pubsub;
+
+import java.util.List;
+
+public interface MessageRepository {
+
+ /** Save message to persistent storage. */
+ void save(Message message);
+
+ /**
+ * Retrieve most recent stored messages.
+ *
+ * @param limit number of messages
+ * @return list of messages
+ */
+ List retrieve(int limit);
+
+ /** Save claim to persistent storage. */
+ void saveClaim(String claim);
+
+ /**
+ * Retrieve most recent stored claims.
+ *
+ * @param limit number of messages
+ * @return list of claims
+ */
+ List retrieveClaims(int limit);
+
+ /** Save token to persistent storage. */
+ void saveToken(String token);
+
+ /**
+ * Retrieve most recent stored tokens.
+ *
+ * @param limit number of messages
+ * @return list of tokens
+ */
+ List retrieveTokens(int limit);
+}
diff --git a/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepositoryImpl.java b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepositoryImpl.java
new file mode 100644
index 00000000000..865c41c9c2f
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepositoryImpl.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2017 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.pubsub;
+
+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.KeyFactory;
+import com.google.cloud.datastore.Query;
+import com.google.cloud.datastore.QueryResults;
+import com.google.cloud.datastore.StructuredQuery;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Storage for Message objects using Cloud Datastore. */
+public class MessageRepositoryImpl implements MessageRepository {
+
+ private static MessageRepositoryImpl instance;
+
+ private String messagesKind = "messages";
+ private KeyFactory keyFactory = getDatastoreInstance().newKeyFactory().setKind(messagesKind);
+ private String claimsKind = "claims";
+ private KeyFactory claimsKindKeyFactory =
+ getDatastoreInstance().newKeyFactory().setKind(claimsKind);
+ private String tokensKind = "tokens";
+ private KeyFactory tokensKindKeyFactory =
+ getDatastoreInstance().newKeyFactory().setKind(tokensKind);
+
+ @Override
+ public void save(Message message) {
+ // Save message to "messages"
+ Datastore datastore = getDatastoreInstance();
+ Key key = datastore.allocateId(keyFactory.newKey());
+
+ Entity.Builder messageEntityBuilder =
+ Entity.newBuilder(key).set("messageId", message.getMessageId());
+
+ if (message.getData() != null) {
+ messageEntityBuilder = messageEntityBuilder.set("data", message.getData());
+ }
+
+ if (message.getPublishTime() != null) {
+ messageEntityBuilder = messageEntityBuilder.set("publishTime", message.getPublishTime());
+ }
+ datastore.put(messageEntityBuilder.build());
+ }
+
+ @Override
+ public List retrieve(int limit) {
+ // Get Message saved in Datastore
+ Datastore datastore = getDatastoreInstance();
+ Query query =
+ Query.newEntityQueryBuilder()
+ .setKind(messagesKind)
+ .setLimit(limit)
+ .addOrderBy(StructuredQuery.OrderBy.desc("publishTime"))
+ .build();
+ QueryResults results = datastore.run(query);
+
+ List messages = new ArrayList<>();
+ while (results.hasNext()) {
+ Entity entity = results.next();
+ Message message = new Message(entity.getString("messageId"));
+ String data = entity.getString("data");
+ if (data != null) {
+ message.setData(data);
+ }
+ String publishTime = entity.getString("publishTime");
+ if (publishTime != null) {
+ message.setPublishTime(publishTime);
+ }
+ messages.add(message);
+ }
+ return messages;
+ }
+
+ @Override
+ public void saveClaim(String claim) {
+ // Save message to "messages"
+ Datastore datastore = getDatastoreInstance();
+ Key key = datastore.allocateId(claimsKindKeyFactory.newKey());
+
+ Entity.Builder claimEntityBuilder = Entity.newBuilder(key).set("claim", claim);
+
+ datastore.put(claimEntityBuilder.build());
+ }
+
+ @Override
+ public List retrieveClaims(int limit) {
+ // Get claim saved in Datastore
+ Datastore datastore = getDatastoreInstance();
+ Query query = Query.newEntityQueryBuilder().setKind(claimsKind).setLimit(limit).build();
+ QueryResults results = datastore.run(query);
+
+ List claims = new ArrayList<>();
+ while (results.hasNext()) {
+ Entity entity = results.next();
+ String claim = entity.getString("claim");
+ if (claim != null) {
+ claims.add(claim);
+ }
+ }
+ return claims;
+ }
+
+ @Override
+ public void saveToken(String token) {
+ // Save message to "messages"
+ Datastore datastore = getDatastoreInstance();
+ Key key = datastore.allocateId(tokensKindKeyFactory.newKey());
+
+ Entity.Builder tokenEntityBuilder = Entity.newBuilder(key).set("token", token);
+
+ datastore.put(tokenEntityBuilder.build());
+ }
+
+ @Override
+ public List retrieveTokens(int limit) {
+ // Get token saved in Datastore
+ Datastore datastore = getDatastoreInstance();
+ Query query = Query.newEntityQueryBuilder().setKind(tokensKind).setLimit(limit).build();
+ QueryResults results = datastore.run(query);
+
+ List tokens = new ArrayList<>();
+ while (results.hasNext()) {
+ Entity entity = results.next();
+ String token = entity.getString("token");
+ if (token != null) {
+ tokens.add(token);
+ }
+ }
+ return tokens;
+ }
+
+ private Datastore getDatastoreInstance() {
+ return DatastoreOptions.getDefaultInstance().getService();
+ }
+
+ private MessageRepositoryImpl() {}
+
+ // retrieve a singleton instance
+ public static synchronized MessageRepositoryImpl getInstance() {
+ if (instance == null) {
+ instance = new MessageRepositoryImpl();
+ }
+ return instance;
+ }
+}
diff --git a/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubAuthenticatedPush.java b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubAuthenticatedPush.java
new file mode 100644
index 00000000000..491f873c6e3
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubAuthenticatedPush.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.pubsub;
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+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.Collections;
+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_java21_standard_pubsub_auth_push]
+@WebServlet(value = "/pubsub/authenticated-push")
+public class PubSubAuthenticatedPush extends HttpServlet {
+ private final String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");
+ private final MessageRepository messageRepository;
+ private final GoogleIdTokenVerifier verifier =
+ new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory())
+ /**
+ * Please change example.com to match with value you are providing while creating
+ * subscription as provided in @see README.
+ */
+ .setAudience(Collections.singletonList("example.com"))
+ .build();
+ private final Gson gson = new Gson();
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+
+ // Verify that the request originates from the application.
+ if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0) {
+ resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
+ String authorizationHeader = req.getHeader("Authorization");
+ if (authorizationHeader == null
+ || authorizationHeader.isEmpty()
+ || authorizationHeader.split(" ").length != 2) {
+ resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ String authorization = authorizationHeader.split(" ")[1];
+
+ try {
+ // Verify and decode the JWT.
+ // Note: For high volume push requests, it would save some network overhead
+ // if you verify the tokens offline by decoding them using Google's Public
+ // Cert; caching already seen tokens works best when a large volume of
+ // messsages have prompted a single push server to handle them, in which
+ // case they would all share the same token for a limited time window.
+ GoogleIdToken idToken = verifier.verify(authorization);
+
+ GoogleIdToken.Payload payload = idToken.getPayload();
+ // IMPORTANT: you should validate claim details not covered by signature
+ // and audience verification above, including:
+ // - Ensure that `payload.getEmail()` is equal to the expected service
+ // account set up in the push subscription settings.
+ // - Ensure that `payload.getEmailVerified()` is set to true.
+
+ messageRepository.saveToken(authorization);
+ messageRepository.saveClaim(payload.toPrettyString());
+ // parse message object from "message" field in the request body json
+ // decode message data from base64
+ Message message = getMessage(req);
+ messageRepository.save(message);
+ // 200, 201, 204, 102 status codes are interpreted as success by the Pub/Sub system
+ resp.setStatus(102);
+ super.doPost(req, resp);
+ } catch (Exception e) {
+ resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ private Message getMessage(HttpServletRequest request) throws IOException {
+ String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));
+ JsonElement jsonRoot = JsonParser.parseString(requestBody).getAsJsonObject();
+ 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));
+ }
+
+ PubSubAuthenticatedPush(MessageRepository messageRepository) {
+ this.messageRepository = messageRepository;
+ }
+
+ public PubSubAuthenticatedPush() {
+ this(MessageRepositoryImpl.getInstance());
+ }
+}
+// [END gae_java21_standard_pubsub_auth_push]
diff --git a/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubHome.java b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubHome.java
new file mode 100644
index 00000000000..68483cecf0b
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubHome.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.pubsub;
+
+import com.googlecode.jatl.Html;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.List;
+
+public class PubSubHome {
+
+ private static MessageRepository messageRepository = MessageRepositoryImpl.getInstance();
+ private static int MAX_MESSAGES = 10;
+
+ /**
+ * Retrieve received messages in html.
+ *
+ * @return html representation of messages (one per row)
+ */
+ public static List getReceivedMessages() {
+ List messageList = messageRepository.retrieve(MAX_MESSAGES);
+ return messageList;
+ }
+
+ /**
+ * Retrieve received claims in html.
+ *
+ * @return html representation of claims (one per row)
+ */
+ public static List getReceivedClaims() {
+ List claimList = messageRepository.retrieveClaims(MAX_MESSAGES);
+ return claimList;
+ }
+
+ /**
+ * Retrieve received tokens in html.
+ *
+ * @return html representation of tokens (one per row)
+ */
+ public static List getReceivedTokens() {
+ List tokenList = messageRepository.retrieveTokens(MAX_MESSAGES);
+ return tokenList;
+ }
+
+ public static String convertToHtml() {
+ Writer writer = new StringWriter(1024);
+ new Html(writer) {
+ {
+ html();
+ head();
+ meta().httpEquiv("refresh").content("10").end();
+ end();
+ body();
+ h3().text("Publish a message").end();
+ form().action("pubsub/publish").method("POST");
+ label().text("Message:").end();
+ input().id("payload").type("input").name("payload").end();
+ input().id("submit").type("submit").value("Send").end();
+ end();
+ h3().text("Last received tokens").end();
+ table().border("1").cellpadding("10");
+ tr();
+ th().text("Tokens").end();
+ end();
+ markupString(getReceivedTokens());
+ h3().text("Last received claims").end();
+ table().border("1").cellpadding("10");
+ tr();
+ th().text("Claims").end();
+ end();
+ markupString(getReceivedClaims());
+ h3().text("Last received messages").end();
+ table().border("1").cellpadding("10");
+ tr();
+ th().text("Id").end();
+ th().text("Data").end();
+ th().text("PublishTime").end();
+ end();
+ markupMessage(getReceivedMessages());
+ endAll();
+ done();
+ }
+
+ Html markupString(List strings) {
+ for (String string : strings) {
+ tr();
+ th().text(string).end();
+ end();
+ }
+ return end();
+ }
+
+ Html markupMessage(List messages) {
+ for (Message message : messages) {
+ tr();
+ th().text(message.getMessageId()).end();
+ th().text(message.getData()).end();
+ th().text(message.getPublishTime()).end();
+ end();
+ }
+ return end();
+ }
+ };
+ return ((StringWriter) writer).getBuffer().toString();
+ }
+
+ private PubSubHome() {}
+}
diff --git a/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPublish.java b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPublish.java
new file mode 100644
index 00000000000..fecb0c3b806
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPublish.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.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;
+
+@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) {
+ ProjectTopicName topicName =
+ ProjectTopicName.newBuilder()
+ .setProject(ServiceOptions.getDefaultProjectId())
+ .setTopic(topicId)
+ .build();
+ publisher = Publisher.newBuilder(topicName).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());
+ }
+ }
+
+ private Publisher publisher;
+
+ public PubSubPublish() {}
+
+ PubSubPublish(Publisher publisher) {
+ this.publisher = publisher;
+ }
+}
diff --git a/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPush.java b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPush.java
new file mode 100644
index 00000000000..e699293fa44
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPush.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.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_java21_standard_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(102);
+ } catch (Exception e) {
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ private Message getMessage(HttpServletRequest request) throws IOException {
+ String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));
+ JsonElement jsonRoot = JsonParser.parseString(requestBody).getAsJsonObject();
+ 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 MessageRepository messageRepository;
+
+ PubSubPush(MessageRepository messageRepository) {
+ this.messageRepository = messageRepository;
+ }
+
+ public PubSubPush() {
+ this.messageRepository = MessageRepositoryImpl.getInstance();
+ }
+}
+// [END gae_java21_standard_pubsub_push]
diff --git a/appengine-java21/ee8/pubsub/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java21/ee8/pubsub/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..0094de59f9b
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,12 @@
+
+ java21
+ true
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/appengine-java21/ee8/pubsub/src/main/webapp/index.jsp b/appengine-java21/ee8/pubsub/src/main/webapp/index.jsp
new file mode 100644
index 00000000000..7d358582809
--- /dev/null
+++ b/appengine-java21/ee8/pubsub/src/main/webapp/index.jsp
@@ -0,0 +1,2 @@
+<%@ page import="com.example.appengine.pubsub.PubSubHome" %>
+<%= PubSubHome.convertToHtml() %>