From 884239bc261f8efccea472a8ad1ab4973d371c76 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 16 Oct 2018 22:49:29 +0200 Subject: [PATCH] #243: call webhooks --- docs-core/pom.xml | 5 + .../com/sismics/docs/core/dao/WebhookDao.java | 4 + .../core/dao/criteria/WebhookCriteria.java | 15 +++ .../listener/async/WebhookAsyncListener.java | 103 ++++++++++++++++++ .../docs/core/model/context/AppContext.java | 1 + .../docs/rest/resource/WebhookResource.java | 1 + .../docs/rest/TestWebhookResource.java | 24 +++- .../resource/ThirdPartyWebhookResource.java | 34 ++++++ pom.xml | 7 ++ 9 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java create mode 100644 docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java diff --git a/docs-core/pom.xml b/docs-core/pom.xml index 64fbea77..28071930 100644 --- a/docs-core/pom.xml +++ b/docs-core/pom.xml @@ -116,6 +116,11 @@ com.sun.mail javax.mail + + + com.squareup.okhttp3 + okhttp + diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java index 78eecefe..cfd16ccb 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java @@ -36,6 +36,10 @@ public class WebhookDao { sb.append(" from T_WEBHOOK w "); // Add search criterias + if (criteria.getEvent() != null) { + criteriaList.add("w.WHK_EVENT_C = :event"); + parameterMap.put("event", criteria.getEvent().name()); + } criteriaList.add("w.WHK_DELETEDATE_D is null"); if (!criteriaList.isEmpty()) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/WebhookCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/WebhookCriteria.java index d93091e5..a7675e79 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/WebhookCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/WebhookCriteria.java @@ -1,9 +1,24 @@ package com.sismics.docs.core.dao.criteria; +import com.sismics.docs.core.constant.WebhookEvent; + /** * Webhook criteria. * * @author bgamard */ public class WebhookCriteria { + /** + * Webhook event. + */ + private WebhookEvent event; + + public WebhookEvent getEvent() { + return event; + } + + public WebhookCriteria setEvent(WebhookEvent event) { + this.event = event; + return this; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java new file mode 100644 index 00000000..b95f5360 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java @@ -0,0 +1,103 @@ +package com.sismics.docs.core.listener.async; + +import com.google.common.collect.Lists; +import com.google.common.eventbus.AllowConcurrentEvents; +import com.google.common.eventbus.Subscribe; +import com.sismics.docs.core.constant.WebhookEvent; +import com.sismics.docs.core.dao.WebhookDao; +import com.sismics.docs.core.dao.criteria.WebhookCriteria; +import com.sismics.docs.core.dao.dto.WebhookDto; +import com.sismics.docs.core.event.*; +import com.sismics.docs.core.util.TransactionUtil; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; + +/** + * Listener for triggering webhooks. + * + * @author bgamard + */ +public class WebhookAsyncListener { + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(WebhookAsyncListener.class); + + /** + * OkHttp client. + */ + private static final OkHttpClient client = new OkHttpClient(); + public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + @Subscribe + @AllowConcurrentEvents + public void on(final DocumentCreatedAsyncEvent event) { + triggerWebhook(WebhookEvent.DOCUMENT_CREATED, event.getDocument().getId()); + } + + @Subscribe + @AllowConcurrentEvents + public void on(final DocumentUpdatedAsyncEvent event) { + triggerWebhook(WebhookEvent.DOCUMENT_UPDATED, event.getDocumentId()); + } + + @Subscribe + @AllowConcurrentEvents + public void on(final DocumentDeletedAsyncEvent event) { + triggerWebhook(WebhookEvent.DOCUMENT_UPDATED, event.getDocumentId()); + } + + @Subscribe + @AllowConcurrentEvents + public void on(final FileCreatedAsyncEvent event) { + triggerWebhook(WebhookEvent.FILE_CREATED, event.getFile().getId()); + } + + @Subscribe + @AllowConcurrentEvents + public void on(final FileUpdatedAsyncEvent event) { + triggerWebhook(WebhookEvent.FILE_UPDATED, event.getFile().getId()); + } + + @Subscribe + @AllowConcurrentEvents + public void on(final FileDeletedAsyncEvent event) { + triggerWebhook(WebhookEvent.FILE_DELETED, event.getFile().getId()); + } + + /** + * Trigger the webhooks for the specified event. + * + * @param event Event + * @param id ID + */ + private void triggerWebhook(WebhookEvent event, String id) { + List webhookUrlList = Lists.newArrayList(); + + TransactionUtil.handle(() -> { + WebhookDao webhookDao = new WebhookDao(); + List webhookDtoList = webhookDao.findByCriteria(new WebhookCriteria().setEvent(event), null); + for (WebhookDto webhookDto : webhookDtoList) { + webhookUrlList.add(webhookDto.getUrl()); + } + }); + + RequestBody body = RequestBody.create(JSON, "{\"event\": \"" + event.name() + "\", \"id\": \"" + id + "\"}"); + + for (String webhookUrl : webhookUrlList) { + Request request = new Request.Builder() + .url(webhookUrl) + .post(body) + .build(); + try (Response response = client.newCall(request).execute()) { + log.info("Successfully called the webhook at: " + webhookUrl + " - " + response.code()); + } catch (IOException e) { + log.error("Error calling the webhook at: " + webhookUrl, e); + } + } + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java index 1193530c..57ef11bc 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java @@ -145,6 +145,7 @@ public class AppContext { asyncEventBus.register(new RebuildIndexAsyncListener()); asyncEventBus.register(new AclCreatedAsyncListener()); asyncEventBus.register(new AclDeletedAsyncListener()); + asyncEventBus.register(new WebhookAsyncListener()); mailEventBus = newAsyncEventBus(); mailEventBus.register(new PasswordLostAsyncListener()); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java index 51b2dc69..97f74251 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java @@ -67,6 +67,7 @@ public class WebhookResource extends BaseResource { * Add a webhook. * * @api {put} /webhook Add a webhook + * @apiDescription Each time the specified event is raised, the webhook URL will be POST-ed with the following JSON payload: {"event": "Event name", "id": "ID of the document or file"} * @apiName PutWebhook * @apiWebhook Webhook * @apiParam {String="DOCUMENT_CREATED","DOCUMENT_UPDATED","DOCUMENT_DELETED","FILE_CREATED","FILE_UPDATED","FILE_DELETED"} event Event diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java index f3c81222..08a34a34 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java @@ -1,5 +1,6 @@ package com.sismics.docs.rest; +import com.sismics.docs.rest.resource.ThirdPartyWebhookResource; import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; @@ -8,6 +9,7 @@ import javax.json.JsonArray; import javax.json.JsonObject; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; +import java.util.Date; /** @@ -23,6 +25,10 @@ public class TestWebhookResource extends BaseJerseyTest { public void testWebhookResource() { // Login admin String adminToken = clientUtil.login("admin", "admin", false); + + // Login webhook1 + clientUtil.createUser("webhook1"); + String webhook1Token = clientUtil.login("webhook1"); // Get all webhooks JsonObject json = target().path("/webhook") @@ -37,7 +43,21 @@ public class TestWebhookResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() .param("event", "DOCUMENT_CREATED") - .param("url", "https://www.sismics.com")), JsonObject.class); + .param("url", "http://localhost:" + getPort() + "/docs/thirdpartywebhook")), JsonObject.class); + + // Create a document + json = target().path("/document").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, webhook1Token) + .put(Entity.form(new Form() + .param("title", "Webhook document 1") + .param("language", "eng") + .param("create_date", Long.toString(new Date().getTime()))), JsonObject.class); + String document1Id = json.getString("id"); + + // Check the webhook payload + JsonObject payload = ThirdPartyWebhookResource.getLastPayload(); + Assert.assertEquals("DOCUMENT_CREATED", payload.getString("event")); + Assert.assertEquals(document1Id, payload.getString("id")); // Get all webhooks json = target().path("/webhook") @@ -49,7 +69,7 @@ public class TestWebhookResource extends BaseJerseyTest { JsonObject webhook = webhooks.getJsonObject(0); String webhookId = webhook.getString("id"); Assert.assertEquals("DOCUMENT_CREATED", webhook.getString("event")); - Assert.assertEquals("https://www.sismics.com", webhook.getString("url")); + Assert.assertEquals("http://localhost:" + getPort() + "/docs/thirdpartywebhook", webhook.getString("url")); Assert.assertNotNull(webhook.getJsonNumber("create_date")); // Delete a webhook diff --git a/docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java b/docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java new file mode 100644 index 00000000..b3082b00 --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java @@ -0,0 +1,34 @@ +package com.sismics.docs.rest.resource; + +import javax.json.JsonObject; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +/** + * Webhook REST resources. + * + * @author bgamard + */ +@Path("/thirdpartywebhook") +public class ThirdPartyWebhookResource extends BaseResource { + /** + * Last payload received. + */ + private static JsonObject lastPayload; + + /** + * Add a webhook. + * + * @return Response + */ + @POST + public Response webhook(JsonObject request) { + lastPayload = request; + return Response.ok().build(); + } + + public static JsonObject getLastPayload() { + return lastPayload; + } +} diff --git a/pom.xml b/pom.xml index b346cfee..3525f792 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ 1.5.7 1.5.6 1.11.2 + 3.11.0 9.3.11.v20160721 9.3.11.v20160721 @@ -224,6 +225,12 @@ ${org.jsoup.jsoup.version} + + com.squareup.okhttp3 + okhttp + ${com.squareup.okhttp3.okhttp.version} + + log4j log4j