diff --git a/appengine-java8/pom.xml b/appengine-java8/pom.xml index 14ff7396fb2..1eef6ab237b 100644 --- a/appengine-java8/pom.xml +++ b/appengine-java8/pom.xml @@ -72,6 +72,8 @@ postgres + pubsub + requests remote-client diff --git a/appengine-java8/pubsub/README.md b/appengine-java8/pubsub/README.md new file mode 100644 index 00000000000..2852d718eab --- /dev/null +++ b/appengine-java8/pubsub/README.md @@ -0,0 +1,80 @@ +# Using Google Cloud Pub/Sub on App Engine Standard Java 8 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-java8/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 beta 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 beta pubsub subscriptions create \ + --topic \ + --push-endpoint \ + https://.appspot.com/pubsub/push?token= \ + --ack-deadline 30 +``` + +## 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=" +``` + +## 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 appengine:deploy +``` + +Direct your browser to `https://project-id.appspot.com`. diff --git a/appengine-java8/pubsub/pom.xml b/appengine-java8/pubsub/pom.xml new file mode 100644 index 00000000000..fe47c70e8f2 --- /dev/null +++ b/appengine-java8/pubsub/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.flexible + appengine-pubsub + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + + 1.8 + 1.8 + false + 1.3.1 + 9.4.4.v20170414 + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + com.google.cloud + google-cloud-pubsub + 0.24.0-beta + + + com.google.cloud + google-cloud-datastore + 1.6.0 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + ${appengine.maven.plugin} + + + + + + + + diff --git a/appengine-java8/pubsub/sample_message.json b/appengine-java8/pubsub/sample_message.json new file mode 100644 index 00000000000..bb912195ba1 --- /dev/null +++ b/appengine-java8/pubsub/sample_message.json @@ -0,0 +1 @@ +{"message":{"data":"dGVzdA==","attributes":{},"messageId":"91010751788941","publishTime":"2017-09-25T23:16:42.302Z"}} diff --git a/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/Message.java b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/Message.java new file mode 100644 index 00000000000..555d328cb38 --- /dev/null +++ b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/Message.java @@ -0,0 +1,53 @@ +/** + * 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.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-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepository.java b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepository.java new file mode 100644 index 00000000000..d68e210f560 --- /dev/null +++ b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepository.java @@ -0,0 +1,30 @@ +/** + * 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.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); +} diff --git a/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepositoryImpl.java b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepositoryImpl.java new file mode 100644 index 00000000000..8d8e5a6b18e --- /dev/null +++ b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepositoryImpl.java @@ -0,0 +1,98 @@ +/** + * 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.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); + + @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; + } + + 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-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubHome.java b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubHome.java new file mode 100644 index 00000000000..b9b551de376 --- /dev/null +++ b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubHome.java @@ -0,0 +1,47 @@ +/** + * 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.appengine.pubsub; + +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 String getReceivedMessages() { + List messageList = messageRepository.retrieve(MAX_MESSAGES); + return convertToHtmlTable(messageList); + } + + private static String convertToHtmlTable(List 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/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPublish.java b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPublish.java new file mode 100644 index 00000000000..72f15161702 --- /dev/null +++ b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPublish.java @@ -0,0 +1,65 @@ +/** + * 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.appengine.pubsub; + +import com.google.cloud.ServiceOptions; +import com.google.cloud.pubsub.v1.Publisher; +import com.google.protobuf.ByteString; +import com.google.pubsub.v1.PubsubMessage; +import com.google.pubsub.v1.TopicName; +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) { + publisher = Publisher.defaultBuilder( + TopicName.create(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()); + } + } + + private Publisher publisher; + + public PubSubPublish() { } + + PubSubPublish(Publisher publisher) { + this.publisher = publisher; + } +} diff --git a/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPush.java b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPush.java new file mode 100644 index 00000000000..225757a8744 --- /dev/null +++ b/appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/PubSubPush.java @@ -0,0 +1,78 @@ +/** + * 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.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; + +@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.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/appengine-java8/pubsub/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/pubsub/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..13cefc05511 --- /dev/null +++ b/appengine-java8/pubsub/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,9 @@ + + true + java8 + + + + + + \ No newline at end of file diff --git a/appengine-java8/pubsub/src/main/webapp/index.jsp b/appengine-java8/pubsub/src/main/webapp/index.jsp new file mode 100644 index 00000000000..fa12f02a14d --- /dev/null +++ b/appengine-java8/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

+ + + + + + + <%= PubSubHome.getReceivedMessages() %> +
IdDataPublishTime
+ +