package com.sap.jam.webhooks.sample; import java.io.IOException; import java.io.Reader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; 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 javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.InvocationCallback; import javax.ws.rs.client.ResponseProcessingException; import javax.ws.rs.core.MediaType; import org.apache.http.HttpStatus; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; /** * This is a simple servlet demonstrating how webhooks calls sent by SAP Jam should be handled. */ @WebServlet("/") public class WebhooksServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final String JAM_BASE_URL = "https://developer.sapjam.com"; // Change this if your Jam instance is not in Developer private static final String JAM_OAUTH_TOKEN = "<YOUR JAM_OAUTH_TOKEN>"; private static final String JAM_WEBHOOK_VERIFICATION_TOKEN = "<YOUR JAM_WEBHOOK_VERIFICATION_TOKEN>"; private final Client httpClient = ClientBuilder.newClient(); private final ExecutorService eventHandlingPool = Executors.newCachedThreadPool(); /** * @see HttpServlet#HttpServlet() */ public WebhooksServlet() { super(); } /** * This is the POST end point that expects to receive webhook callbacks from Jam. Upon receiving any webhook callback, * clients must verify that the verification token within the request payload exists and is correct. After verification, * the client must echo the challenge string contained within the payload as the response to the request. * * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ @Override protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { System.out.println("Received POST call"); final JSONObject callbackPayload = parseJSONRequest(request.getReader()); final String verificationToken = (String)callbackPayload.get("@sapjam.hub.verificationToken"); final String challengeString = (String)callbackPayload.get("@sapjam.hub.challenge"); final JSONArray events = (JSONArray)callbackPayload.get("value"); // Verify the identity of the server if (verificationToken.equals(JAM_WEBHOOK_VERIFICATION_TOKEN)) { handleEvents(events); response.getWriter().println(challengeString); // Echo the challenge string as a response } else { response.sendError(HttpStatus.SC_FORBIDDEN); } } /** * To make sure the webhook client responds to Jam webhook calls within a couple seconds, we encourage all event handling to be done * asynchronously. In Java, this can be done using with {@link ForkJoinPool} or {@link Executors} (used here). * * @param events */ private void handleEvents(final JSONArray events) { if (events == null) { return; } for (final Object event : events) { eventHandlingPool.execute(new Runnable() { @Override public void run() { replyToEvent((JSONObject)event); } }); } } /** * For this sample application, we want our webhooks client to post a reply to each document or comment that triggered a * webhook call. Since different types of event entities have different APIs for posting comments, we will have check the * type of the associated entity for each event to make the appropriate API call. * * See our webhooks documentation for the possible event types and entity types that could be sent by a * webhook call. * * @param eventObject */ private void replyToEvent(final JSONObject eventObject) { final String eventEntityId = (String)eventObject.get("Id"); final String entityType = (String)eventObject.get("@sapjam.event.entityType"); System.out.printf("Processing event type: %s", entityType); switch (entityType) { case "ContentItem": final String contentItemType = (String)eventObject.get("ContentItemType"); postToContentItem(eventEntityId, contentItemType); break; case "FeedEntry": postToFeed(eventEntityId); break; case "Comment": final String feedEntryId = (String)((JSONObject)eventObject.get("ParentFeedEntry")).get("Id"); postToFeed(feedEntryId); break; default: break; } } /** * See @see <a href= * "https://developer.sapjam.com/ODataDocs/ui#!/Content/post_ContentItems_Id_Id_ContentItemType_ContentItemType_FeedEntries"> * SAP Jam OData Documentation for reference</a> * * @param contentItemId * @param contentItemType */ private void postToContentItem(final String contentItemId, final String contentItemType) { final String oDataPath = String.format("ContentItems(Id='%s', ContentItemType='%s')/FeedEntries", contentItemId, contentItemType); postToOData(oDataPath, "{\"Text\": \"I've received a webhook call!\"}"); } /** * See @see * <a href="https://developer.sapjam.com/ODataDocs/ui#!/Feed/get_FeedEntries_id_Replies">SAP Jam OData Documentation for * reference</a> * * @param feedEntryId */ private void postToFeed(final String feedEntryId) { final String oDataPath = String.format("FeedEntries('%s')/Replies", feedEntryId); postToOData(oDataPath, "{\"Text\": \"I've received a webhook call!\"}"); } /** * This method encapsulates a basic way of making most POST calls to the Jam OData API under the JSON format. * * @param oDataPath API end point to call * @param payload a JSON request body */ private void postToOData(final String oDataPath, final String payload) { System.out.printf("Making Jam OData POST call to %s with payload: %n%s", oDataPath, payload); httpClient .target(JAM_BASE_URL) .path("/api/v1/OData/" + oDataPath) .queryParam("$format", "json") .request(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + JAM_OAUTH_TOKEN) .header("Content-Type", MediaType.APPLICATION_JSON) .header("Accept", MediaType.APPLICATION_JSON) .async() .post(Entity.json(payload), new InvocationCallback<String>() { @Override public void completed(final String response) { System.out.println("Received response: " + response); } @Override public void failed(final Throwable throwable) { final ResponseProcessingException exception = (ResponseProcessingException)throwable; final String responseString = exception.getResponse().readEntity(String.class); System.out.println("Received error response: " + responseString); throwable.printStackTrace(); } }); } /** * Converts a text input stream into a JSON object. * * @param reader * @return a parsed JSONObject. If parsing fails, an empty JSON Object is returned. */ private JSONObject parseJSONRequest(final Reader reader) { final JSONParser jsonParser = new JSONParser(); JSONObject parsedPayload = new JSONObject(); try { parsedPayload = (JSONObject)jsonParser.parse(reader); } catch (ParseException | IOException e) { e.printStackTrace(); } return parsedPayload; } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { response.getWriter().println("Please add this endpoint as the callback URL in a Push Notification Subscription on SAP Jam."); } @Override public void destroy() { httpClient.close(); eventHandlingPool.shutdown(); } }