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 + + +Open in Cloud Shell + +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() %>