diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/WebhookEvent.java b/docs-core/src/main/java/com/sismics/docs/core/constant/WebhookEvent.java new file mode 100644 index 00000000..e28a6938 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/WebhookEvent.java @@ -0,0 +1,15 @@ +package com.sismics.docs.core.constant; + +/** + * Webhook events. + * + * @author bgamard + */ +public enum WebhookEvent { + DOCUMENT_CREATED, + DOCUMENT_UPDATED, + DOCUMENT_DELETED, + FILE_CREATED, + FILE_UPDATED, + FILE_DELETED +} 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 new file mode 100644 index 00000000..78eecefe --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java @@ -0,0 +1,119 @@ +package com.sismics.docs.core.dao; + +import com.google.common.base.Joiner; +import com.sismics.docs.core.dao.criteria.WebhookCriteria; +import com.sismics.docs.core.dao.dto.WebhookDto; +import com.sismics.docs.core.model.jpa.Webhook; +import com.sismics.docs.core.util.jpa.QueryParam; +import com.sismics.docs.core.util.jpa.QueryUtil; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.util.context.ThreadLocalContext; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.Query; +import java.sql.Timestamp; +import java.util.*; + +/** + * Webhook DAO. + * + * @author bgamard + */ +public class WebhookDao { + /** + * Returns the list of all webhooks. + * + * @param criteria Search criteria + * @param sortCriteria Sort criteria + * @return List of webhooks + */ + public List findByCriteria(WebhookCriteria criteria, SortCriteria sortCriteria) { + Map parameterMap = new HashMap<>(); + List criteriaList = new ArrayList<>(); + + StringBuilder sb = new StringBuilder("select w.WHK_ID_C as c0, w.WHK_EVENT_C as c1, w.WHK_URL_C as c2, w.WHK_CREATEDATE_D as c3 "); + sb.append(" from T_WEBHOOK w "); + + // Add search criterias + criteriaList.add("w.WHK_DELETEDATE_D is null"); + + if (!criteriaList.isEmpty()) { + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); + } + + // Perform the search + QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); + @SuppressWarnings("unchecked") + List l = QueryUtil.getNativeQuery(queryParam).getResultList(); + + // Assemble results + List webhookDtoList = new ArrayList<>(); + for (Object[] o : l) { + int i = 0; + WebhookDto webhookDto = new WebhookDto() + .setId((String) o[i++]) + .setEvent((String) o[i++]) + .setUrl((String) o[i++]) + .setCreateTimestamp(((Timestamp) o[i]).getTime()); + webhookDtoList.add(webhookDto); + } + + return webhookDtoList; + } + + /** + * Returns a webhook by ID. + * + * @param id Webhook ID + * @return Webhook + */ + public Webhook getActiveById(String id) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + Query q = em.createQuery("select w from Webhook w where w.id = :id and w.deleteDate is null"); + q.setParameter("id", id); + try { + return (Webhook) q.getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + /** + * Creates a new webhook. + * + * @param webhook Webhook + * @return New ID + */ + public String create(Webhook webhook) { + // Create the UUID + webhook.setId(UUID.randomUUID().toString()); + + // Create the webhook + EntityManager em = ThreadLocalContext.get().getEntityManager(); + webhook.setCreateDate(new Date()); + em.persist(webhook); + + return webhook.getId(); + } + + /** + * Deletes a webhook. + * + * @param webhookId Webhook ID + */ + public void delete(String webhookId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the group + Query q = em.createQuery("select w from Webhook w where w.id = :id and w.deleteDate is null"); + q.setParameter("id", webhookId); + Webhook webhookDb = (Webhook) q.getSingleResult(); + + // Delete the group + Date dateNow = new Date(); + webhookDb.setDeleteDate(dateNow); + } +} + 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 new file mode 100644 index 00000000..d93091e5 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/WebhookCriteria.java @@ -0,0 +1,9 @@ +package com.sismics.docs.core.dao.criteria; + +/** + * Webhook criteria. + * + * @author bgamard + */ +public class WebhookCriteria { +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/WebhookDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/WebhookDto.java new file mode 100644 index 00000000..5caf358f --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/WebhookDto.java @@ -0,0 +1,74 @@ +package com.sismics.docs.core.dao.dto; + +/** + * Webhook DTO. + * + * @author bgamard + */ +public class WebhookDto { + /** + * Webhook ID. + */ + private String id; + + /** + * Event. + */ + private String event; + + /** + * URL. + */ + private String url; + + /** + * Creation date. + */ + private Long createTimestamp; + + public String getId() { + return id; + } + + public WebhookDto setId(String id) { + this.id = id; + return this; + } + + public String getEvent() { + return event; + } + + public WebhookDto setEvent(String event) { + this.event = event; + return this; + } + + public String getUrl() { + return url; + } + + public WebhookDto setUrl(String url) { + this.url = url; + return this; + } + + public Long getCreateTimestamp() { + return createTimestamp; + } + + public WebhookDto setCreateTimestamp(Long createTimestamp) { + this.createTimestamp = createTimestamp; + return this; + } + + @Override + public boolean equals(Object obj) { + return id.equals(((WebhookDto) obj).getId()); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java index 5d6d3b31..9cc044c5 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java @@ -69,6 +69,7 @@ public class FileProcessingAsyncListener { * @param event File updated event */ @Subscribe + @AllowConcurrentEvents public void on(final FileUpdatedAsyncEvent event) { if (log.isInfoEnabled()) { log.info("File updated event: " + event.toString()); diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Webhook.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Webhook.java new file mode 100644 index 00000000..a1db95d2 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Webhook.java @@ -0,0 +1,108 @@ +package com.sismics.docs.core.model.jpa; + +import com.google.common.base.MoreObjects; +import com.sismics.docs.core.constant.WebhookEvent; + +import javax.persistence.*; +import java.util.Date; + +/** + * Webhook entity. + * + * @author bgamard + */ +@Entity +@Table(name = "T_WEBHOOK") +public class Webhook implements Loggable { + /** + * Webhook ID. + */ + @Id + @Column(name = "WHK_ID_C", nullable = false, length = 36) + private String id; + + /** + * Event. + */ + @Column(name = "WHK_EVENT_C", nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private WebhookEvent event; + + /** + * URL. + */ + @Column(name = "WHK_URL_C", nullable = false, length = 1024) + private String url; + + /** + * Creation date. + */ + @Column(name = "WHK_CREATEDATE_D", nullable = false) + private Date createDate; + + /** + * Deletion date. + */ + @Column(name = "WHK_DELETEDATE_D") + private Date deleteDate; + + public String getId() { + return id; + } + + public Webhook setId(String id) { + this.id = id; + return this; + } + + public WebhookEvent getEvent() { + return event; + } + + public Webhook setEvent(WebhookEvent event) { + this.event = event; + return this; + } + + public String getUrl() { + return url; + } + + public Webhook setUrl(String url) { + this.url = url; + return this; + } + + public Date getCreateDate() { + return createDate; + } + + public Webhook setCreateDate(Date createDate) { + this.createDate = createDate; + return this; + } + + @Override + public Date getDeleteDate() { + return deleteDate; + } + + public Webhook setDeleteDate(Date deleteDate) { + this.deleteDate = deleteDate; + return this; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("event", event) + .add("url", url) + .toString(); + } + + @Override + public String toMessage() { + return url; + } +} diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index 77078578..4d9e12ea 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=19 \ No newline at end of file +db.version=20 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-020-0.sql b/docs-core/src/main/resources/db/update/dbupdate-020-0.sql new file mode 100644 index 00000000..a86bd22c --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-020-0.sql @@ -0,0 +1,3 @@ +create table T_WEBHOOK ( WHK_ID_C varchar(36) not null, WHK_EVENT_C varchar(50) not null, WHK_URL_C varchar(1024) not null, WHK_CREATEDATE_D datetime not null, WHK_DELETEDATE_D datetime, primary key (WHK_ID_C) ); + +update T_CONFIG set CFG_VALUE_C = '20' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 47a648c1..1c8df450 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=19 \ No newline at end of file +db.version=20 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java index 1fd7c14d..16059c92 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java @@ -67,7 +67,7 @@ public abstract class BaseResource { */ protected boolean authenticate() { Principal principal = (Principal) request.getAttribute(SecurityFilter.PRINCIPAL_ATTRIBUTE); - if (principal != null && principal instanceof IPrincipal) { + if (principal instanceof IPrincipal) { this.principal = (IPrincipal) principal; return !this.principal.isAnonymous(); } else { @@ -93,7 +93,7 @@ public abstract class BaseResource { * @return True if the user has the base function */ boolean hasBaseFunction(BaseFunction baseFunction) { - if (principal == null || !(principal instanceof UserPrincipal)) { + if (!(principal instanceof UserPrincipal)) { return false; } Set baseFunctionSet = ((UserPrincipal) principal).getBaseFunctionSet(); 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 new file mode 100644 index 00000000..51b2dc69 --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java @@ -0,0 +1,144 @@ +package com.sismics.docs.rest.resource; + +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.model.jpa.Webhook; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.docs.rest.constant.BaseFunction; +import com.sismics.rest.exception.ForbiddenClientException; +import com.sismics.rest.util.ValidationUtil; + +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObjectBuilder; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Webhook REST resources. + * + * @author bgamard + */ +@Path("/webhook") +public class WebhookResource extends BaseResource { + /** + * Returns the list of all webhooks. + * + * @api {get} /webhook Get webhooks + * @apiName GetWebhook + * @apiWebhook Webhook + * @apiSuccess {Object[]} webhooks List of webhooks + * @apiSuccess {String} webhooks.id ID + * @apiSuccess {String} webhooks.event Event + * @apiSuccess {String} webhooks.url URL + * @apiError (client) ForbiddenError Access denied + * @apiPermission admin + * @apiVersion 1.6.0 + * + * @return Response + */ + @GET + public Response list(@QueryParam("document") String documentId) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + WebhookDao webhookDao = new WebhookDao(); + JsonArrayBuilder webhooks = Json.createArrayBuilder(); + List webhookDtoList = webhookDao.findByCriteria(new WebhookCriteria(), new SortCriteria(2, true)); + for (WebhookDto webhookDto : webhookDtoList) { + webhooks.add(Json.createObjectBuilder() + .add("id", webhookDto.getId()) + .add("event", webhookDto.getEvent()) + .add("url", webhookDto.getUrl()) + .add("create_date", webhookDto.getCreateTimestamp())); + } + + JsonObjectBuilder response = Json.createObjectBuilder() + .add("webhooks", webhooks); + return Response.ok().entity(response.build()).build(); + } + + /** + * Add a webhook. + * + * @api {put} /webhook Add a webhook + * @apiName PutWebhook + * @apiWebhook Webhook + * @apiParam {String="DOCUMENT_CREATED","DOCUMENT_UPDATED","DOCUMENT_DELETED","FILE_CREATED","FILE_UPDATED","FILE_DELETED"} event Event + * @apiParam {String} url URL + * @apiSuccess {String} status Status OK + * @apiError (client) ForbiddenError Access denied + * @apiError (client) ValidationError Validation error + * @apiPermission admin + * @apiVersion 1.6.0 + * + * @return Response + */ + @PUT + public Response add(@FormParam("event") String eventStr, + @FormParam("url") String url) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Validate input + WebhookEvent event = WebhookEvent.valueOf(ValidationUtil.validateLength(eventStr, "event", 1, 50, false)); + url = ValidationUtil.validateLength(url, "url", 1, 1024, false); + + // Create the webhook + WebhookDao webhookDao = new WebhookDao(); + webhookDao.create(new Webhook() + .setUrl(url) + .setEvent(event)); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } + + /** + * Delete a webhook. + * + * @api {delete} /webhook/:id Delete a webhook + * @apiName DeleteWebhook + * @apiWebhook Webhook + * @apiParam {String} id Webhook ID + * @apiSuccess {String} status Status OK + * @apiError (client) ForbiddenError Access denied + * @apiError (client) NotFound Webhook not found + * @apiPermission admin + * @apiVersion 1.6.0 + * + * @return Response + */ + @DELETE + @Path("{id: [a-z0-9\\-]+}") + public Response delete(@PathParam("id") String id) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Get the webhook + WebhookDao webhookDao = new WebhookDao(); + Webhook webhook = webhookDao.getActiveById(id); + if (webhook == null) { + throw new NotFoundException(); + } + + // Delete the webhook + webhookDao.delete(webhook.getId()); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } +} diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 47a648c1..1c8df450 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=19 \ No newline at end of file +db.version=20 \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index 47a648c1..1c8df450 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=19 \ No newline at end of file +db.version=20 \ No newline at end of file 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 new file mode 100644 index 00000000..f3c81222 --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java @@ -0,0 +1,68 @@ +package com.sismics.docs.rest; + +import com.sismics.util.filter.TokenBasedSecurityFilter; +import org.junit.Assert; +import org.junit.Test; + +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Form; + + +/** + * Test the webhook resource. + * + * @author bgamard + */ +public class TestWebhookResource extends BaseJerseyTest { + /** + * Test the webhook resource. + */ + @Test + public void testWebhookResource() { + // Login admin + String adminToken = clientUtil.login("admin", "admin", false); + + // Get all webhooks + JsonObject json = target().path("/webhook") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + JsonArray webhooks = json.getJsonArray("webhooks"); + Assert.assertEquals(0, webhooks.size()); + + // Create a webhook + target().path("/webhook").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("event", "DOCUMENT_CREATED") + .param("url", "https://www.sismics.com")), JsonObject.class); + + // Get all webhooks + json = target().path("/webhook") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + webhooks = json.getJsonArray("webhooks"); + Assert.assertEquals(1, webhooks.size()); + 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.assertNotNull(webhook.getJsonNumber("create_date")); + + // Delete a webhook + target().path("/webhook/" + webhookId).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .delete(JsonObject.class); + + // Get all webhooks + json = target().path("/webhook") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + webhooks = json.getJsonArray("webhooks"); + Assert.assertEquals(0, webhooks.size()); + } +} \ No newline at end of file