From dc5606acf0db174ee174ad4a4b03b110fb04445f Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Tue, 5 Nov 2019 22:34:04 -0800 Subject: [PATCH 01/11] Add support for global/project level webhooks --- pom.xml | 19 +++- .../com/sourcegraph/webhook/Dispatcher.java | 66 +++++++++++ .../sourcegraph/webhook/EventSerializer.java | 103 ++++++++++++++++++ .../sourcegraph/webhook/WebhookListener.java | 74 +++++++++++++ .../sourcegraph/webhook/WebhookRouter.java | 46 ++++++++ .../webhook/registry/EventEntity.java | 13 +++ .../sourcegraph/webhook/registry/Webhook.java | 19 ++++ .../webhook/registry/WebhookEntity.java | 15 +++ .../webhook/registry/WebhookRegistry.java | 94 ++++++++++++++++ src/main/resources/atlassian-plugin.xml | 10 +- 10 files changed, 455 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/sourcegraph/webhook/Dispatcher.java create mode 100644 src/main/java/com/sourcegraph/webhook/EventSerializer.java create mode 100644 src/main/java/com/sourcegraph/webhook/WebhookListener.java create mode 100644 src/main/java/com/sourcegraph/webhook/WebhookRouter.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/EventEntity.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/Webhook.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java diff --git a/pom.xml b/pom.xml index 37d2e3f..290f69b 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,11 @@ + + javax.inject + javax.inject + 1 + com.atlassian.bitbucket.server bitbucket-api @@ -62,6 +67,12 @@ provided 2.0 + + com.atlassian.activeobjects + activeobjects-plugin + 1.2.3 + provided + com.atlassian.plugin atlassian-spring-scanner-annotation @@ -90,7 +101,11 @@ sal-api provided - + + com.atlassian.bitbucket.server + bitbucket-spi + provided + @@ -152,7 +167,7 @@ atlassian-spring-scanner-external-jar - false + true diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java new file mode 100644 index 0000000..c0db30e --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -0,0 +1,66 @@ +package com.sourcegraph.webhook; + +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.sal.api.net.Request; +import com.atlassian.sal.api.net.RequestFactory; +import com.atlassian.sal.api.net.Response; +import com.atlassian.sal.api.net.ResponseException; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ExecutorService; + +@Component +public class Dispatcher { + private static final Logger log = LoggerFactory.getLogger(Dispatcher.class); + @ComponentImport + private static ExecutorService executor; + @ComponentImport + private static RequestFactory requestFactory; + + @Autowired + public Dispatcher(ExecutorService executor, RequestFactory requestFactory) { + Dispatcher.executor = executor; + Dispatcher.requestFactory = requestFactory; + } + + public static void dispatch(String external, JsonElement payload) { + // System.out.println(external + " " + new Gson().toJson(payload)); + + executor.submit(() -> { + Request request = requestFactory.createRequest(Request.MethodType.POST, external); + request.setRequestBody(new Gson().toJson(payload)); + + int retries = 5; + while (true) { + Response response = null; + try { + response = (Response) request.executeAndReturn((resp) -> resp); + } catch (ResponseException e) { + log.debug("Dispatching webhook data to URL: [" + external + "] failed with error:\n" + e); + } + retries--; + + if (response != null && response.isSuccessful()) { + break; + } + + if (retries == 0) { + log.debug("Dispatching webhook data to URL: [" + external + "] failed after 5 attempts.."); + break; + } + + try { + Thread.sleep(10 * 1000); + } catch (InterruptedException e) { + log.debug("Dispatching webhook data to URL: [" + external + "] was interrupted."); + break; + } + } + }); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/EventSerializer.java b/src/main/java/com/sourcegraph/webhook/EventSerializer.java new file mode 100644 index 0000000..4881207 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/EventSerializer.java @@ -0,0 +1,103 @@ +package com.sourcegraph.webhook; + +import com.atlassian.bitbucket.event.ApplicationEvent; +import com.atlassian.bitbucket.event.pull.*; +import com.atlassian.bitbucket.json.JsonRenderer; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.google.gson.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +@Component +public class EventSerializer { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + private static Map, Adapter> adapters = new HashMap<>(); + private static JsonRenderer renderer; + + private JsonObject payload; + + private static JsonElement render(Object o) { + String raw = renderer.render(o, new HashMap<>()); + return raw == null ? null : new JsonParser().parse(raw); + } + + private static Adapter PullRequestUpdatedEventAdapter = (element, event) -> { + element.addProperty("previousTitle", event.getPreviousTitle()); + element.addProperty("previousDescription", event.getPreviousDescription()); + element.add("previousTarget", render(event.getPreviousToBranch())); + }; + + private static Adapter PullRequestReviewersUpdatedEventAdapter = (element, event) -> { + element.add("removedReviewers", render(event.getRemovedReviewers())); + element.add("addedReviewers", render(event.getAddedReviewers())); + }; + + private static Adapter PullRequestParticipantStatusUpdatedEventAdapter = (element, event) -> { + element.add("participant", render(event.getParticipant())); + element.addProperty("previousStatus", event.getPreviousStatus().name()); + }; + + private static Adapter PullRequestCommentEventAdapter = (element, event) -> { + element.add("comment", render(event.getComment())); + if (event.getParent() != null) { + element.addProperty("commentParentId", event.getParent().getId()); + } + }; + + private static Adapter PullRequestCommentEditedEventAdapter = (element, event) -> { + PullRequestCommentEventAdapter.apply(element, event); + element.addProperty("previousComment", event.getPreviousText()); + }; + + static { + adapters.put(PullRequestUpdatedEvent.class, PullRequestUpdatedEventAdapter); + adapters.put(PullRequestReviewersUpdatedEvent.class, PullRequestReviewersUpdatedEventAdapter); + adapters.put(PullRequestParticipantApprovedEvent.class, PullRequestParticipantStatusUpdatedEventAdapter); + adapters.put(PullRequestParticipantUnapprovedEvent.class, PullRequestParticipantStatusUpdatedEventAdapter); + adapters.put(PullRequestParticipantReviewedEvent.class, PullRequestParticipantStatusUpdatedEventAdapter); + adapters.put(PullRequestCommentAddedEvent.class, PullRequestCommentEventAdapter); + adapters.put(PullRequestCommentDeletedEvent.class, PullRequestCommentEventAdapter); + adapters.put(PullRequestCommentEditedEvent.class, PullRequestCommentEditedEventAdapter); + } + + @Autowired + public EventSerializer(@ComponentImport JsonRenderer renderer) { + EventSerializer.renderer = renderer; + } + + public EventSerializer(String name) { + payload = new JsonObject(); + payload.addProperty("eventKey", name); + } + + public JsonElement serialize(ApplicationEvent event) { + buildApplicationEvent(event); + if (event instanceof PullRequestEvent) { + buildPullRequestEvent((PullRequestEvent) event); + } + + Adapter adapter = adapters.get(event.getClass()); + if (adapter == null) { + return payload; + } + adapter.apply(payload, event); + return payload; + } + + private void buildApplicationEvent(ApplicationEvent event) { + payload.addProperty("date", DATE_FORMAT.format(event.getDate())); + payload.add("actor", render(event.getUser())); + } + + private void buildPullRequestEvent(PullRequestEvent event) { + payload.add("pullRequest", render(event.getPullRequest())); + } + + private interface Adapter { + void apply(JsonObject element, T event); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/WebhookListener.java b/src/main/java/com/sourcegraph/webhook/WebhookListener.java new file mode 100644 index 0000000..7e835c9 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/WebhookListener.java @@ -0,0 +1,74 @@ +package com.sourcegraph.webhook; + +import com.atlassian.bitbucket.event.ApplicationEvent; +import com.atlassian.bitbucket.event.backup.BackupStartedEvent; +import com.atlassian.bitbucket.event.permission.PermissionEvent; +import com.atlassian.bitbucket.event.pull.*; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scm.event.ScmEvent; +import com.atlassian.event.api.AsynchronousPreferred; +import com.atlassian.event.api.EventListener; +import com.google.common.base.Joiner; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.sourcegraph.webhook.registry.Webhook; +import com.sourcegraph.webhook.registry.WebhookRegistry; + +import javax.inject.Named; +import java.util.*; + +@AsynchronousPreferred +@Named("WebhookListener") +public class WebhookListener { + private static Map, List> triggers = new HashMap<>(); + + private static void register(Class type, String key) { + String[] split = key.split(":"); + List pieces = new ArrayList<>(); + for (int i = 0, index = 0; i < split.length; i++) { + index += split[i].length(); + pieces.add(key.substring(0, index)); + index++; + } + triggers.put(type, pieces); + } + + static { + register(PullRequestOpenedEvent.class, "pr:opened"); + register(PullRequestUpdatedEvent.class, "pr:modified"); + register(PullRequestReviewersUpdatedEvent.class, "pr:reviewer:updated"); + register(PullRequestParticipantApprovedEvent.class, "pr:reviewer:approved"); + register(PullRequestParticipantUnapprovedEvent.class, "pr:reviewer:unapproved"); + register(PullRequestParticipantReviewedEvent.class, "pr:reviewer:needs_work"); + register(PullRequestMergedEvent.class, "pr:merged"); + register(PullRequestDeclinedEvent.class, "pr:declined"); + register(PullRequestDeletedEvent.class, "pr:deleted"); + register(PullRequestCommentAddedEvent.class, "pr:comment:added"); + register(PullRequestCommentEditedEvent.class, "pr:comment:edited"); + register(PullRequestCommentDeletedEvent.class, "pr:comment:deleted"); + } + + @EventListener + public void onPullRequestEvent(PullRequestEvent event) { + handle(event, event.getPullRequest().getToRef().getRepository()); + } + + private void handle(ApplicationEvent event, Repository repository) { + System.out.println(event.getClass()); + List keys = triggers.get(event.getClass()); + if (keys == null || keys.isEmpty()) { + return; + } + + List hooks = WebhookRegistry.getWebhooks(keys, repository); + if (hooks.isEmpty()) { + return; + } + + String key = keys.get(keys.size() - 1); + EventSerializer serializer = new EventSerializer(key); + JsonElement payload = serializer.serialize(event); + hooks.forEach(hook -> Dispatcher.dispatch(hook.external, payload)); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java new file mode 100644 index 0000000..a56e234 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -0,0 +1,46 @@ +package com.sourcegraph.webhook; + +import com.google.gson.Gson; +import com.sourcegraph.webhook.registry.Webhook; +import com.sourcegraph.webhook.registry.WebhookRegistry; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +@Path("/webhook") +public class WebhookRouter { + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response get() { + List hooks = WebhookRegistry.getWebhooks(); + String resp = new Gson().toJson(hooks); + return Response.ok(resp).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response put( + @FormParam("scope") String scope, + @FormParam("identifier") String identifier, + @FormParam("events") String events, + @FormParam("external") String external) { + // TODO - Form Data Validation + Webhook hook = new Webhook(0, scope, identifier, new HashSet<>(), external); + Collections.addAll(hook.events, events.split(",")); + System.out.println(hook.events.size()); + System.out.println("REGISTERING: " + new Gson().toJson(hook)); + WebhookRegistry.register(hook); + return Response.ok().build(); + } + + @DELETE + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response delete(@FormParam("id") int id) { + WebhookRegistry.deregister(id); + return Response.noContent().build(); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java new file mode 100644 index 0000000..dfd9d06 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java @@ -0,0 +1,13 @@ +package com.sourcegraph.webhook.registry; + +import net.java.ao.Entity; + +public interface EventEntity extends Entity { + String getEvent(); + + void setEvent(String event); + + WebhookEntity getWebhook(); + + void setWebhook(WebhookEntity hook); +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java new file mode 100644 index 0000000..623e07c --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java @@ -0,0 +1,19 @@ +package com.sourcegraph.webhook.registry; + +import java.util.Set; + +public class Webhook { + public int id; + public String scope; + public String identifier; + public Set events; + public String external; + + public Webhook(int id, String scope, String identifier, Set events, String external) { + this.id = id; + this.scope = scope; + this.identifier = identifier; + this.events = events; + this.external = external; + } +} \ No newline at end of file diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java new file mode 100644 index 0000000..e5b9f64 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java @@ -0,0 +1,15 @@ +package com.sourcegraph.webhook.registry; + +import net.java.ao.Entity; +import net.java.ao.OneToMany; + +public interface WebhookEntity extends Entity { + String getScope(); + + String getIdentifier(); + + @OneToMany + EventEntity[] getEvents(); + + String getExternal(); +} \ No newline at end of file diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java new file mode 100644 index 0000000..e69a29e --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -0,0 +1,94 @@ +package com.sourcegraph.webhook.registry; + +import com.atlassian.activeobjects.external.ActiveObjects; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; +import net.java.ao.Query; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +public class WebhookRegistry { + @ComponentImport + private static ActiveObjects activeObjects; + + @Autowired + public WebhookRegistry(ActiveObjects ao) { + WebhookRegistry.activeObjects = ao; + } + + public static List getWebhooks() { + WebhookEntity[] entities = activeObjects.find(WebhookEntity.class); + return getWebhooksFromEntities(entities); + } + + public static List getWebhooks(List keys, Repository repository) { + String params = Joiner.on(", ").join(Collections.nCopies(keys.size(), "?")); + Iterable args = Iterables.concat(keys, Arrays.asList( + repository.getName(), + repository.getProject().getName() + )); + + String where = "event.EVENT in (" + params + ") " + + "AND (webhook.SCOPE = \'global\' " + + "OR (webhook.SCOPE = \'project\' AND webhook.IDENTIFIER = ?) " + + "OR (webhook.SCOPE = \'repository\' AND webhook.IDENTIFIER = ?))"; + + Query query = Query.select() + .alias(WebhookEntity.class, "webhook") + .alias(EventEntity.class, "event") + .join(EventEntity.class, "event.WEBHOOK_ID = webhook.ID") + .where(where, Iterables.toArray(args, Object.class)); + + WebhookEntity[] hooks = activeObjects.find(WebhookEntity.class, query); + return getWebhooksFromEntities(hooks); + } + + private static List getWebhooksFromEntities(WebhookEntity[] entities) { + List hooks = new ArrayList<>(entities.length); + for (WebhookEntity ent : entities) { + Set events = new HashSet<>(); + for (EventEntity ev : ent.getEvents()) { + events.add(ev.getEvent()); + } + + hooks.add(new Webhook(ent.getID(), ent.getScope(), ent.getIdentifier(), events, ent.getExternal())); + } + return hooks; + } + + public static void register(Webhook hook) { + activeObjects.executeInTransaction(() -> { + Map params = new HashMap<>(); + params.put("SCOPE", hook.scope); + params.put("IDENTIFIER", hook.identifier); + params.put("EXTERNAL", hook.external); + + WebhookEntity hookEntity = activeObjects.create(WebhookEntity.class, params); + hookEntity.save(); + + for (String event : hook.events) { + EventEntity eventEntity = activeObjects.create(EventEntity.class); + eventEntity.setEvent(event); + eventEntity.setWebhook(hookEntity); + eventEntity.save(); + } + + return hookEntity; + }); + } + + public static void deregister(int id) { + activeObjects.executeInTransaction(() -> { + activeObjects.deleteWithSQL(EventEntity.class, "WEBHOOK_ID = ?", id); + activeObjects.deleteWithSQL(WebhookEntity.class, "ID = ?", id); + return null; + }); + } +} diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index facf0e4..55bfc31 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -7,6 +7,12 @@ images/pluginLogo.png + + + com.sourcegraph.webhook.registry.WebhookEntity + com.sourcegraph.webhook.registry.EventEntity + + com.atlassian.auiplugin:ajs @@ -33,8 +39,8 @@ /plugins/servlet/sourcegraph + - Provides REST resources for the Sourcegraph admin UI. + Provides REST resources for Sourcegraph. - From d62b653783c3a028c437fe22707d3c2b383a25a1 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Thu, 7 Nov 2019 23:25:22 -0800 Subject: [PATCH 02/11] Check for invalid scope, repository, and project --- .../sourcegraph/webhook/WebhookListener.java | 11 ++-- .../sourcegraph/webhook/WebhookRouter.java | 9 ++- .../webhook/registry/WebhookEntity.java | 2 +- .../webhook/registry/WebhookException.java | 8 +++ .../webhook/registry/WebhookRegistry.java | 63 ++++++++++++++++--- 5 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/sourcegraph/webhook/registry/WebhookException.java diff --git a/src/main/java/com/sourcegraph/webhook/WebhookListener.java b/src/main/java/com/sourcegraph/webhook/WebhookListener.java index 7e835c9..606ac42 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookListener.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookListener.java @@ -1,22 +1,19 @@ package com.sourcegraph.webhook; import com.atlassian.bitbucket.event.ApplicationEvent; -import com.atlassian.bitbucket.event.backup.BackupStartedEvent; -import com.atlassian.bitbucket.event.permission.PermissionEvent; import com.atlassian.bitbucket.event.pull.*; import com.atlassian.bitbucket.repository.Repository; -import com.atlassian.bitbucket.scm.event.ScmEvent; import com.atlassian.event.api.AsynchronousPreferred; import com.atlassian.event.api.EventListener; -import com.google.common.base.Joiner; -import com.google.gson.Gson; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.sourcegraph.webhook.registry.Webhook; import com.sourcegraph.webhook.registry.WebhookRegistry; import javax.inject.Named; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @AsynchronousPreferred @Named("WebhookListener") diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java index a56e234..e1b4553 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.sourcegraph.webhook.registry.Webhook; +import com.sourcegraph.webhook.registry.WebhookException; import com.sourcegraph.webhook.registry.WebhookRegistry; import javax.ws.rs.*; @@ -33,7 +34,13 @@ public Response put( Collections.addAll(hook.events, events.split(",")); System.out.println(hook.events.size()); System.out.println("REGISTERING: " + new Gson().toJson(hook)); - WebhookRegistry.register(hook); + + try { + WebhookRegistry.register(hook); + } catch (WebhookException e) { + return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build(); + } + return Response.ok().build(); } diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java index e5b9f64..896d9b5 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java @@ -6,7 +6,7 @@ public interface WebhookEntity extends Entity { String getScope(); - String getIdentifier(); + int getIdentifier(); @OneToMany EventEntity[] getEvents(); diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java new file mode 100644 index 0000000..0aa07b7 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java @@ -0,0 +1,8 @@ +package com.sourcegraph.webhook.registry; + +public class WebhookException extends Exception { + + public WebhookException(String message) { + super(message); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java index e69a29e..251efb9 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -1,10 +1,11 @@ package com.sourcegraph.webhook.registry; import com.atlassian.activeobjects.external.ActiveObjects; +import com.atlassian.bitbucket.project.Project; +import com.atlassian.bitbucket.project.ProjectService; import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; -import com.google.common.base.Function; -import com.google.common.base.Functions; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import net.java.ao.Query; @@ -17,10 +18,16 @@ public class WebhookRegistry { @ComponentImport private static ActiveObjects activeObjects; + @ComponentImport + private static ProjectService projects; + @ComponentImport + private static RepositoryService repositories; @Autowired - public WebhookRegistry(ActiveObjects ao) { + public WebhookRegistry(ActiveObjects ao, ProjectService projects, RepositoryService repositories) { WebhookRegistry.activeObjects = ao; + WebhookRegistry.projects = projects; + WebhookRegistry.repositories = repositories; } public static List getWebhooks() { @@ -30,9 +37,9 @@ public static List getWebhooks() { public static List getWebhooks(List keys, Repository repository) { String params = Joiner.on(", ").join(Collections.nCopies(keys.size(), "?")); - Iterable args = Iterables.concat(keys, Arrays.asList( - repository.getName(), - repository.getProject().getName() + Iterable args = Iterables.concat(keys, Arrays.asList( + repository.getId(), + repository.getProject().getId() )); String where = "event.EVENT in (" + params + ") " @@ -58,16 +65,19 @@ private static List getWebhooksFromEntities(WebhookEntity[] entities) { events.add(ev.getEvent()); } - hooks.add(new Webhook(ent.getID(), ent.getScope(), ent.getIdentifier(), events, ent.getExternal())); + String name = resolveName(ent.getScope(), ent.getIdentifier()); + hooks.add(new Webhook(ent.getID(), ent.getScope(), name, events, ent.getExternal())); } return hooks; } - public static void register(Webhook hook) { + public static void register(Webhook hook) throws WebhookException { + int identifier = resolveID(hook.scope, hook.identifier); + activeObjects.executeInTransaction(() -> { Map params = new HashMap<>(); params.put("SCOPE", hook.scope); - params.put("IDENTIFIER", hook.identifier); + params.put("IDENTIFIER", identifier); params.put("EXTERNAL", hook.external); WebhookEntity hookEntity = activeObjects.create(WebhookEntity.class, params); @@ -91,4 +101,39 @@ public static void deregister(int id) { return null; }); } + + private static int resolveID(String scope, String name) throws WebhookException { + switch (scope) { + case "repository": + String[] split = name.split("/"); + Repository repository = repositories.getBySlug(split[0], split[1]); + if (repository == null) { + throw new WebhookException("No such repository: " + name); + } + return repository.getId(); + case "project": + Project project = projects.getByName(name); + if (project == null) { + throw new WebhookException("No such project: " + name); + } + return project.getId(); + case "global": + return 0; + default: + throw new WebhookException("Invalid scope: " + scope); + } + } + + private static String resolveName(String scope, int id) { + switch (scope) { + case "repository": + Repository repository = repositories.getById(id); + return repository == null ? "" : repository.getName(); + case "project": + Project project = projects.getById(id); + return project == null ? "" : project.getName(); + default: + return ""; + } + } } From 289a9b47ec37837109bb9fe13adcf91600bc7051 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Fri, 8 Nov 2019 16:37:59 -0800 Subject: [PATCH 03/11] Improve exception handling --- .../com/sourcegraph/webhook/Dispatcher.java | 5 +- .../sourcegraph/webhook/WebhookRouter.java | 9 +--- .../webhook/registry/WebhookException.java | 47 ++++++++++++++++++- .../webhook/registry/WebhookRegistry.java | 9 ++-- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index c0db30e..cf56d13 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -17,6 +17,7 @@ @Component public class Dispatcher { private static final Logger log = LoggerFactory.getLogger(Dispatcher.class); + private static final int RETRY_DELAY = 10 * 1000; @ComponentImport private static ExecutorService executor; @ComponentImport @@ -50,12 +51,12 @@ public static void dispatch(String external, JsonElement payload) { } if (retries == 0) { - log.debug("Dispatching webhook data to URL: [" + external + "] failed after 5 attempts.."); + log.warn("Dispatching webhook data to URL: [" + external + "] failed after 5 attempts.."); break; } try { - Thread.sleep(10 * 1000); + Thread.sleep(RETRY_DELAY); } catch (InterruptedException e) { log.debug("Dispatching webhook data to URL: [" + external + "] was interrupted."); break; diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java index e1b4553..c34f370 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -28,19 +28,14 @@ public Response put( @FormParam("scope") String scope, @FormParam("identifier") String identifier, @FormParam("events") String events, - @FormParam("external") String external) { + @FormParam("external") String external) throws WebhookException { // TODO - Form Data Validation Webhook hook = new Webhook(0, scope, identifier, new HashSet<>(), external); Collections.addAll(hook.events, events.split(",")); System.out.println(hook.events.size()); System.out.println("REGISTERING: " + new Gson().toJson(hook)); - try { - WebhookRegistry.register(hook); - } catch (WebhookException e) { - return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build(); - } - + WebhookRegistry.register(hook); return Response.ok().build(); } diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java index 0aa07b7..895e8ed 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java @@ -1,8 +1,51 @@ package com.sourcegraph.webhook.registry; -public class WebhookException extends Exception { +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; - public WebhookException(String message) { +@Provider +public class WebhookException extends Exception implements ExceptionMapper { + private Response.StatusType status; + + public WebhookException() { + } + + public WebhookException(Response.StatusType status, String message) { super(message); + this.status = status; + } + + public Response.StatusType getStatus() { + return status; + } + + @Override + public Response toResponse(WebhookException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); + } + + public enum Status implements Response.StatusType { + UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"); + + private final int code; + private final String reason; + + Status(int status, String reason) { + this.code = status; + this.reason = reason; + } + + public int getStatusCode() { + return this.code; + } + + public Response.Status.Family getFamily() { + return null; + } + + public String getReasonPhrase() { + return this.reason; + } } } diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java index 251efb9..6624ec9 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -12,6 +12,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; import java.util.*; @Component @@ -59,6 +61,7 @@ public static List getWebhooks(List keys, Repository repository private static List getWebhooksFromEntities(WebhookEntity[] entities) { List hooks = new ArrayList<>(entities.length); + for (WebhookEntity ent : entities) { Set events = new HashSet<>(); for (EventEntity ev : ent.getEvents()) { @@ -108,19 +111,19 @@ private static int resolveID(String scope, String name) throws WebhookException String[] split = name.split("/"); Repository repository = repositories.getBySlug(split[0], split[1]); if (repository == null) { - throw new WebhookException("No such repository: " + name); + throw new WebhookException(Response.Status.NOT_FOUND, "No such repository: " + name); } return repository.getId(); case "project": Project project = projects.getByName(name); if (project == null) { - throw new WebhookException("No such project: " + name); + throw new WebhookException(Response.Status.NOT_FOUND, "No such project: " + name); } return project.getId(); case "global": return 0; default: - throw new WebhookException("Invalid scope: " + scope); + throw new WebhookException(WebhookException.Status.UNPROCESSABLE_ENTITY, "Invalid scope: " + scope); } } From a76c23b7206ce43751dae66eb1b28fd3edf3686e Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Mon, 11 Nov 2019 09:40:41 -0800 Subject: [PATCH 04/11] HMAC signing + JSON endpoint --- .../com/sourcegraph/webhook/Dispatcher.java | 46 +++++++++++++++---- .../sourcegraph/webhook/EventSerializer.java | 6 +-- .../sourcegraph/webhook/WebhookListener.java | 17 +++---- .../sourcegraph/webhook/WebhookRouter.java | 39 +++++++++++----- .../sourcegraph/webhook/registry/Webhook.java | 19 ++++++-- .../webhook/registry/WebhookEntity.java | 6 ++- .../webhook/registry/WebhookRegistry.java | 7 +-- 7 files changed, 100 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index cf56d13..3106797 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -5,19 +5,30 @@ import com.atlassian.sal.api.net.RequestFactory; import com.atlassian.sal.api.net.Response; import com.atlassian.sal.api.net.ResponseException; +import com.google.common.hash.Hashing; import com.google.gson.Gson; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.sourcegraph.webhook.registry.Webhook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.util.concurrent.ExecutorService; @Component public class Dispatcher { private static final Logger log = LoggerFactory.getLogger(Dispatcher.class); private static final int RETRY_DELAY = 10 * 1000; + private static final int MAX_RETRIES = 5; @ComponentImport private static ExecutorService executor; @ComponentImport @@ -29,39 +40,54 @@ public Dispatcher(ExecutorService executor, RequestFactory requestFactory) { Dispatcher.requestFactory = requestFactory; } - public static void dispatch(String external, JsonElement payload) { + public static String sign(String secret, String data) throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance("HmacSHA1"); + SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1"); + mac.init(secretKeySpec); + return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8))); + } + + public static void dispatch(Webhook hook, JsonObject payload) { // System.out.println(external + " " + new Gson().toJson(payload)); executor.submit(() -> { - Request request = requestFactory.createRequest(Request.MethodType.POST, external); - request.setRequestBody(new Gson().toJson(payload)); + Request request = requestFactory.createRequest(Request.MethodType.POST, hook.endpoint); + + String json = new Gson().toJson(payload); + request.setRequestBody(json); - int retries = 5; - while (true) { + try { + request.setHeader("X-Signature", sign(hook.secret, json)); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + log.error(e.toString()); + } + + for (int retries = 0; retries < MAX_RETRIES; retries++) { Response response = null; try { response = (Response) request.executeAndReturn((resp) -> resp); } catch (ResponseException e) { - log.debug("Dispatching webhook data to URL: [" + external + "] failed with error:\n" + e); + log.debug("Dispatching webhook data to URL: [" + hook.endpoint + "] failed with error:\n" + e); } - retries--; if (response != null && response.isSuccessful()) { + log.debug("Dispatching webhook data to URL: [" + hook.endpoint + "] succeeded."); break; } - if (retries == 0) { - log.warn("Dispatching webhook data to URL: [" + external + "] failed after 5 attempts.."); + if (retries == MAX_RETRIES - 1) { + log.warn("Dispatching webhook data to URL: [" + hook.endpoint + "] failed after " + MAX_RETRIES + " attempts.."); break; } try { Thread.sleep(RETRY_DELAY); } catch (InterruptedException e) { - log.debug("Dispatching webhook data to URL: [" + external + "] was interrupted."); + log.debug("Dispatching webhook data to URL: [" + hook.endpoint + "] was interrupted."); break; } } }); + } } diff --git a/src/main/java/com/sourcegraph/webhook/EventSerializer.java b/src/main/java/com/sourcegraph/webhook/EventSerializer.java index 4881207..4146e03 100644 --- a/src/main/java/com/sourcegraph/webhook/EventSerializer.java +++ b/src/main/java/com/sourcegraph/webhook/EventSerializer.java @@ -14,7 +14,7 @@ @Component public class EventSerializer { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + private static final SimpleDateFormat RFC3339 = new SimpleDateFormat("yyyy-MM-dd'T'h:m:ssZZZZZ"); private static Map, Adapter> adapters = new HashMap<>(); private static JsonRenderer renderer; @@ -74,7 +74,7 @@ public EventSerializer(String name) { payload.addProperty("eventKey", name); } - public JsonElement serialize(ApplicationEvent event) { + public JsonObject serialize(ApplicationEvent event) { buildApplicationEvent(event); if (event instanceof PullRequestEvent) { buildPullRequestEvent((PullRequestEvent) event); @@ -89,7 +89,7 @@ public JsonElement serialize(ApplicationEvent event) { } private void buildApplicationEvent(ApplicationEvent event) { - payload.addProperty("date", DATE_FORMAT.format(event.getDate())); + payload.addProperty("date", RFC3339.format(event.getDate())); payload.add("actor", render(event.getUser())); } diff --git a/src/main/java/com/sourcegraph/webhook/WebhookListener.java b/src/main/java/com/sourcegraph/webhook/WebhookListener.java index 606ac42..e852e1f 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookListener.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookListener.java @@ -5,7 +5,7 @@ import com.atlassian.bitbucket.repository.Repository; import com.atlassian.event.api.AsynchronousPreferred; import com.atlassian.event.api.EventListener; -import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.sourcegraph.webhook.registry.Webhook; import com.sourcegraph.webhook.registry.WebhookRegistry; @@ -21,14 +21,16 @@ public class WebhookListener { private static Map, List> triggers = new HashMap<>(); private static void register(Class type, String key) { + // Enumerate all prefixes (or super/parent events) of event key. + // "pr:comment:added" -> ["pr", "pr:comment", "pr:comment:added"] String[] split = key.split(":"); - List pieces = new ArrayList<>(); + List prefixes = new ArrayList<>(); for (int i = 0, index = 0; i < split.length; i++) { index += split[i].length(); - pieces.add(key.substring(0, index)); + prefixes.add(key.substring(0, index)); index++; } - triggers.put(type, pieces); + triggers.put(type, prefixes); } static { @@ -38,7 +40,7 @@ private static void register(Class type, String key) { register(PullRequestParticipantApprovedEvent.class, "pr:reviewer:approved"); register(PullRequestParticipantUnapprovedEvent.class, "pr:reviewer:unapproved"); register(PullRequestParticipantReviewedEvent.class, "pr:reviewer:needs_work"); - register(PullRequestMergedEvent.class, "pr:merged"); + register(PullRequestMergeActivityEvent.class, "pr:merged"); register(PullRequestDeclinedEvent.class, "pr:declined"); register(PullRequestDeletedEvent.class, "pr:deleted"); register(PullRequestCommentAddedEvent.class, "pr:comment:added"); @@ -52,7 +54,6 @@ public void onPullRequestEvent(PullRequestEvent event) { } private void handle(ApplicationEvent event, Repository repository) { - System.out.println(event.getClass()); List keys = triggers.get(event.getClass()); if (keys == null || keys.isEmpty()) { return; @@ -65,7 +66,7 @@ private void handle(ApplicationEvent event, Repository repository) { String key = keys.get(keys.size() - 1); EventSerializer serializer = new EventSerializer(key); - JsonElement payload = serializer.serialize(event); - hooks.forEach(hook -> Dispatcher.dispatch(hook.external, payload)); + JsonObject payload = serializer.serialize(event); + hooks.forEach(hook -> Dispatcher.dispatch(hook, payload)); } } diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java index c34f370..9618059 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -1,19 +1,31 @@ package com.sourcegraph.webhook; import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonReader; import com.sourcegraph.webhook.registry.Webhook; import com.sourcegraph.webhook.registry.WebhookException; import com.sourcegraph.webhook.registry.WebhookRegistry; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.*; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.io.BufferedReader; +import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.List; @Path("/webhook") public class WebhookRouter { + private static final Logger log = LoggerFactory.getLogger(WebhookRouter.class); + @GET @Produces(MediaType.APPLICATION_JSON) public Response get() { @@ -23,19 +35,22 @@ public Response get() { } @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response put( - @FormParam("scope") String scope, - @FormParam("identifier") String identifier, - @FormParam("events") String events, - @FormParam("external") String external) throws WebhookException { - // TODO - Form Data Validation - Webhook hook = new Webhook(0, scope, identifier, new HashSet<>(), external); - Collections.addAll(hook.events, events.split(",")); - System.out.println(hook.events.size()); - System.out.println("REGISTERING: " + new Gson().toJson(hook)); + @Consumes(MediaType.APPLICATION_JSON) + public Response put(String raw) throws WebhookException { + try { + Gson gson = new Gson(); + Webhook hook = gson.fromJson(raw, Webhook.class); + if (hook == null) { + throw new WebhookException(Response.Status.BAD_REQUEST, "Invalid JSON"); + } + log.info("Registering webhook: " + raw); + WebhookRegistry.register(hook); + } catch (JsonIOException e) { + throw new WebhookException(Response.Status.INTERNAL_SERVER_ERROR, ""); + } catch (JsonSyntaxException e) { + throw new WebhookException(Response.Status.BAD_REQUEST, "Invalid JSON"); + } - WebhookRegistry.register(hook); return Response.ok().build(); } diff --git a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java index 623e07c..47ea166 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java +++ b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java @@ -1,19 +1,32 @@ package com.sourcegraph.webhook.registry; +import com.google.gson.annotations.Expose; + import java.util.Set; public class Webhook { + @Expose public int id; + @Expose + public String name; + @Expose public String scope; + @Expose public String identifier; + @Expose public Set events; - public String external; + @Expose + public String endpoint; + @Expose(serialize = false) + public String secret; - public Webhook(int id, String scope, String identifier, Set events, String external) { + public Webhook(int id, String name, String scope, String identifier, Set events, String endpoint, String secret) { this.id = id; + this.name = name; this.scope = scope; this.identifier = identifier; this.events = events; - this.external = external; + this.endpoint = endpoint; + this.secret = secret; } } \ No newline at end of file diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java index 896d9b5..a3e152e 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java @@ -4,6 +4,8 @@ import net.java.ao.OneToMany; public interface WebhookEntity extends Entity { + String getName(); + String getScope(); int getIdentifier(); @@ -11,5 +13,7 @@ public interface WebhookEntity extends Entity { @OneToMany EventEntity[] getEvents(); - String getExternal(); + String getEndpoint(); + + String getSecret(); } \ No newline at end of file diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java index 6624ec9..bff0a53 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -12,7 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.Response; import java.util.*; @@ -69,7 +68,7 @@ private static List getWebhooksFromEntities(WebhookEntity[] entities) { } String name = resolveName(ent.getScope(), ent.getIdentifier()); - hooks.add(new Webhook(ent.getID(), ent.getScope(), name, events, ent.getExternal())); + hooks.add(new Webhook(ent.getID(), ent.getName(), ent.getScope(), name, events, ent.getEndpoint(), ent.getSecret())); } return hooks; } @@ -80,8 +79,10 @@ public static void register(Webhook hook) throws WebhookException { activeObjects.executeInTransaction(() -> { Map params = new HashMap<>(); params.put("SCOPE", hook.scope); + params.put("NAME", hook.name); params.put("IDENTIFIER", identifier); - params.put("EXTERNAL", hook.external); + params.put("ENDPOINT", hook.endpoint); + params.put("SECRET", hook.secret); WebhookEntity hookEntity = activeObjects.create(WebhookEntity.class, params); hookEntity.save(); From ec7f52b5371cfb5035e1d31469fdaf7684311687 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Mon, 11 Nov 2019 14:09:03 -0800 Subject: [PATCH 05/11] Update src/main/java/com/sourcegraph/webhook/Dispatcher.java Co-Authored-By: Nick Snyder --- src/main/java/com/sourcegraph/webhook/Dispatcher.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index 3106797..906f9ec 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -48,7 +48,6 @@ public static String sign(String secret, String data) throws NoSuchAlgorithmExce } public static void dispatch(Webhook hook, JsonObject payload) { - // System.out.println(external + " " + new Gson().toJson(payload)); executor.submit(() -> { Request request = requestFactory.createRequest(Request.MethodType.POST, hook.endpoint); From cd01a27d20b241a33ee66af051d2f9545d5d8341 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Tue, 12 Nov 2019 22:05:12 -0800 Subject: [PATCH 06/11] Address comments --- .../com/sourcegraph/webhook/Dispatcher.java | 60 +++++++++++-------- .../sourcegraph/webhook/EventSerializer.java | 28 ++++++--- .../sourcegraph/webhook/WebhookListener.java | 7 +-- .../sourcegraph/webhook/WebhookRouter.java | 1 + .../webhook/registry/EventEntity.java | 1 + .../sourcegraph/webhook/registry/Webhook.java | 1 + .../webhook/registry/WebhookEntity.java | 1 + .../webhook/registry/WebhookException.java | 1 + .../webhook/registry/WebhookRegistry.java | 1 + 9 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index 906f9ec..2d050af 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -10,6 +10,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.sourcegraph.webhook.registry.Webhook; +import org.omg.CORBA.DynAnyPackage.Invalid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -28,7 +29,7 @@ public class Dispatcher { private static final Logger log = LoggerFactory.getLogger(Dispatcher.class); private static final int RETRY_DELAY = 10 * 1000; - private static final int MAX_RETRIES = 5; + private static final int MAX_ATTEMPTS = 5; @ComponentImport private static ExecutorService executor; @ComponentImport @@ -40,53 +41,64 @@ public Dispatcher(ExecutorService executor, RequestFactory requestFactory) { Dispatcher.requestFactory = requestFactory; } - public static String sign(String secret, String data) throws NoSuchAlgorithmException, InvalidKeyException { + private static String sign(String secret, String data) throws NoSuchAlgorithmException, InvalidKeyException { Mac mac = Mac.getInstance("HmacSHA1"); SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1"); mac.init(secretKeySpec); return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8))); } - public static void dispatch(Webhook hook, JsonObject payload) { + private static Request createRequest(Webhook hook, EventSerializer serializer) { + Request request = requestFactory.createRequest(Request.MethodType.POST, hook.endpoint); + request.setHeader("X-Event-Key", serializer.getName()); + JsonObject payload = serializer.serialize(); + String json = new Gson().toJson(payload); + request.setRequestBody(json); - executor.submit(() -> { - Request request = requestFactory.createRequest(Request.MethodType.POST, hook.endpoint); + try { + request.setHeader("X-Signature", sign(hook.secret, json)); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + log.error(e.toString()); + return null; + } - String json = new Gson().toJson(payload); - request.setRequestBody(json); + return request; + } - try { - request.setHeader("X-Signature", sign(hook.secret, json)); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - log.error(e.toString()); + public static void dispatch(Webhook hook, EventSerializer serializer) { + executor.submit(() -> { + Request request = createRequest(hook, serializer); + if (request == null) { + return; } - for (int retries = 0; retries < MAX_RETRIES; retries++) { - Response response = null; + int attempt = 0; + while (true) { + request.setHeader("X-Attempt-Number", String.valueOf(attempt)); try { - response = (Response) request.executeAndReturn((resp) -> resp); + Response response = (Response) request.executeAndReturn((resp) -> resp); + if (response.isSuccessful()) { + log.debug("Dispatching webhook (" + serializer.getName() + ") data to URL: [" + hook.endpoint + "] succeeded."); + break; + } } catch (ResponseException e) { - log.debug("Dispatching webhook data to URL: [" + hook.endpoint + "] failed with error:\n" + e); - } - - if (response != null && response.isSuccessful()) { - log.debug("Dispatching webhook data to URL: [" + hook.endpoint + "] succeeded."); - break; + log.debug("Dispatching webhook data (" + serializer.getName() + ") to URL: [" + hook.endpoint + "] failed with error:\n" + e); } + attempt++; - if (retries == MAX_RETRIES - 1) { - log.warn("Dispatching webhook data to URL: [" + hook.endpoint + "] failed after " + MAX_RETRIES + " attempts.."); + if (attempt == MAX_ATTEMPTS) { + log.warn("Dispatching webhook data (" + serializer.getName() + ") to URL: [" + hook.endpoint + "] failed after " + attempt + " attempts.."); break; } try { Thread.sleep(RETRY_DELAY); } catch (InterruptedException e) { - log.debug("Dispatching webhook data to URL: [" + hook.endpoint + "] was interrupted."); + log.debug("Dispatching webhook data (" + serializer.getName() + ") to URL: [" + hook.endpoint + "] was interrupted."); break; } } }); - } + } diff --git a/src/main/java/com/sourcegraph/webhook/EventSerializer.java b/src/main/java/com/sourcegraph/webhook/EventSerializer.java index 4146e03..1f70699 100644 --- a/src/main/java/com/sourcegraph/webhook/EventSerializer.java +++ b/src/main/java/com/sourcegraph/webhook/EventSerializer.java @@ -18,10 +18,13 @@ public class EventSerializer { private static Map, Adapter> adapters = new HashMap<>(); private static JsonRenderer renderer; + private ApplicationEvent event; + private String name; private JsonObject payload; private static JsonElement render(Object o) { - String raw = renderer.render(o, new HashMap<>()); + HashMap options = new HashMap<>(); + String raw = renderer.render(o, options); return raw == null ? null : new JsonParser().parse(raw); } @@ -69,25 +72,33 @@ public EventSerializer(@ComponentImport JsonRenderer renderer) { EventSerializer.renderer = renderer; } - public EventSerializer(String name) { + public EventSerializer(String name, ApplicationEvent event) { payload = new JsonObject(); - payload.addProperty("eventKey", name); + this.name = name; + this.event = event; } - public JsonObject serialize(ApplicationEvent event) { - buildApplicationEvent(event); + public JsonObject serialize() { + buildApplicationEvent(this.event); if (event instanceof PullRequestEvent) { buildPullRequestEvent((PullRequestEvent) event); } Adapter adapter = adapters.get(event.getClass()); - if (adapter == null) { - return payload; + if (adapter != null) { + adapter.apply(payload, event); } - adapter.apply(payload, event); return payload; } + public String getName() { + return this.name; + } + + public ApplicationEvent getEvent() { + return this.event; + } + private void buildApplicationEvent(ApplicationEvent event) { payload.addProperty("date", RFC3339.format(event.getDate())); payload.add("actor", render(event.getUser())); @@ -100,4 +111,5 @@ private void buildPullRequestEvent(PullRequestEvent event) { private interface Adapter { void apply(JsonObject element, T event); } + } diff --git a/src/main/java/com/sourcegraph/webhook/WebhookListener.java b/src/main/java/com/sourcegraph/webhook/WebhookListener.java index e852e1f..5035872 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookListener.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookListener.java @@ -5,7 +5,6 @@ import com.atlassian.bitbucket.repository.Repository; import com.atlassian.event.api.AsynchronousPreferred; import com.atlassian.event.api.EventListener; -import com.google.gson.JsonObject; import com.sourcegraph.webhook.registry.Webhook; import com.sourcegraph.webhook.registry.WebhookRegistry; @@ -65,8 +64,8 @@ private void handle(ApplicationEvent event, Repository repository) { } String key = keys.get(keys.size() - 1); - EventSerializer serializer = new EventSerializer(key); - JsonObject payload = serializer.serialize(event); - hooks.forEach(hook -> Dispatcher.dispatch(hook, payload)); + EventSerializer serializer = new EventSerializer(key, event); + hooks.forEach(hook -> Dispatcher.dispatch(hook, serializer)); } + } diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java index 9618059..f2c5840 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -60,4 +60,5 @@ public Response delete(@FormParam("id") int id) { WebhookRegistry.deregister(id); return Response.noContent().build(); } + } diff --git a/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java index dfd9d06..68af437 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java +++ b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java @@ -10,4 +10,5 @@ public interface EventEntity extends Entity { WebhookEntity getWebhook(); void setWebhook(WebhookEntity hook); + } diff --git a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java index 47ea166..064175f 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java +++ b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java @@ -29,4 +29,5 @@ public Webhook(int id, String name, String scope, String identifier, Set this.endpoint = endpoint; this.secret = secret; } + } \ No newline at end of file diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java index a3e152e..5645b48 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java @@ -16,4 +16,5 @@ public interface WebhookEntity extends Entity { String getEndpoint(); String getSecret(); + } \ No newline at end of file diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java index 895e8ed..452bff6 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java @@ -48,4 +48,5 @@ public String getReasonPhrase() { return this.reason; } } + } diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java index bff0a53..206fa9f 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -140,4 +140,5 @@ private static String resolveName(String scope, int id) { return ""; } } + } From 34e3762f780a483b2bc413d0c3332c93fa88bca8 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Tue, 12 Nov 2019 22:18:13 -0800 Subject: [PATCH 07/11] Fix ending newlines --- src/main/java/com/sourcegraph/webhook/Dispatcher.java | 1 - src/main/java/com/sourcegraph/webhook/EventSerializer.java | 1 - src/main/java/com/sourcegraph/webhook/WebhookListener.java | 1 - src/main/java/com/sourcegraph/webhook/WebhookRouter.java | 1 - .../java/com/sourcegraph/webhook/registry/EventEntity.java | 1 - src/main/java/com/sourcegraph/webhook/registry/Webhook.java | 3 +-- .../java/com/sourcegraph/webhook/registry/WebhookEntity.java | 3 +-- .../com/sourcegraph/webhook/registry/WebhookException.java | 1 - .../java/com/sourcegraph/webhook/registry/WebhookRegistry.java | 2 +- 9 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index 2d050af..deb3637 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -100,5 +100,4 @@ public static void dispatch(Webhook hook, EventSerializer serializer) { } }); } - } diff --git a/src/main/java/com/sourcegraph/webhook/EventSerializer.java b/src/main/java/com/sourcegraph/webhook/EventSerializer.java index 1f70699..71463de 100644 --- a/src/main/java/com/sourcegraph/webhook/EventSerializer.java +++ b/src/main/java/com/sourcegraph/webhook/EventSerializer.java @@ -111,5 +111,4 @@ private void buildPullRequestEvent(PullRequestEvent event) { private interface Adapter { void apply(JsonObject element, T event); } - } diff --git a/src/main/java/com/sourcegraph/webhook/WebhookListener.java b/src/main/java/com/sourcegraph/webhook/WebhookListener.java index 5035872..f04a091 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookListener.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookListener.java @@ -67,5 +67,4 @@ private void handle(ApplicationEvent event, Repository repository) { EventSerializer serializer = new EventSerializer(key, event); hooks.forEach(hook -> Dispatcher.dispatch(hook, serializer)); } - } diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java index f2c5840..9618059 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -60,5 +60,4 @@ public Response delete(@FormParam("id") int id) { WebhookRegistry.deregister(id); return Response.noContent().build(); } - } diff --git a/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java index 68af437..dfd9d06 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java +++ b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java @@ -10,5 +10,4 @@ public interface EventEntity extends Entity { WebhookEntity getWebhook(); void setWebhook(WebhookEntity hook); - } diff --git a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java index 064175f..9a70035 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java +++ b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java @@ -29,5 +29,4 @@ public Webhook(int id, String name, String scope, String identifier, Set this.endpoint = endpoint; this.secret = secret; } - -} \ No newline at end of file +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java index 5645b48..35cc63d 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java @@ -16,5 +16,4 @@ public interface WebhookEntity extends Entity { String getEndpoint(); String getSecret(); - -} \ No newline at end of file +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java index 452bff6..895e8ed 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java @@ -48,5 +48,4 @@ public String getReasonPhrase() { return this.reason; } } - } diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java index 206fa9f..5e23e88 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -140,5 +140,5 @@ private static String resolveName(String scope, int id) { return ""; } } - } + From 67bf8c3c0b294eb421130c551740328f9b7cedcd Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Tue, 12 Nov 2019 22:38:02 -0800 Subject: [PATCH 08/11] Clean imports --- src/main/java/com/sourcegraph/webhook/Dispatcher.java | 4 ---- src/main/java/com/sourcegraph/webhook/WebhookRouter.java | 8 -------- 2 files changed, 12 deletions(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index deb3637..a22f1e6 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -5,12 +5,9 @@ import com.atlassian.sal.api.net.RequestFactory; import com.atlassian.sal.api.net.Response; import com.atlassian.sal.api.net.ResponseException; -import com.google.common.hash.Hashing; import com.google.gson.Gson; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.sourcegraph.webhook.registry.Webhook; -import org.omg.CORBA.DynAnyPackage.Invalid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -18,7 +15,6 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java index 9618059..e43d49b 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -3,23 +3,15 @@ import com.google.gson.Gson; import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; -import com.google.gson.stream.JsonReader; import com.sourcegraph.webhook.registry.Webhook; import com.sourcegraph.webhook.registry.WebhookException; import com.sourcegraph.webhook.registry.WebhookRegistry; -import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.http.HttpServletRequest; import javax.ws.rs.*; -import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.io.BufferedReader; -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; import java.util.List; @Path("/webhook") From e419d040a32b0eef5ae38933032f21dcbe34b950 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Wed, 13 Nov 2019 04:18:38 -0800 Subject: [PATCH 09/11] Add ReopenedEvent --- src/main/java/com/sourcegraph/webhook/WebhookListener.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/sourcegraph/webhook/WebhookListener.java b/src/main/java/com/sourcegraph/webhook/WebhookListener.java index f04a091..5507db9 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookListener.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookListener.java @@ -33,6 +33,7 @@ private static void register(Class type, String key) { } static { + register(PullRequestReopenedEvent.class, "pr:opened"); register(PullRequestOpenedEvent.class, "pr:opened"); register(PullRequestUpdatedEvent.class, "pr:modified"); register(PullRequestReviewersUpdatedEvent.class, "pr:reviewer:updated"); From 99b191d84efc49bf227f06e81421b0472e343188 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Wed, 13 Nov 2019 05:08:58 -0800 Subject: [PATCH 10/11] Support webhook delete by name + change header signature name --- .../java/com/sourcegraph/webhook/Dispatcher.java | 4 +++- .../java/com/sourcegraph/webhook/WebhookRouter.java | 9 +++++++-- .../webhook/registry/WebhookRegistry.java | 13 +++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index a22f1e6..4b8b84c 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -47,12 +47,14 @@ private static String sign(String secret, String data) throws NoSuchAlgorithmExc private static Request createRequest(Webhook hook, EventSerializer serializer) { Request request = requestFactory.createRequest(Request.MethodType.POST, hook.endpoint); request.setHeader("X-Event-Key", serializer.getName()); + request.setHeader("X-Hook-ID", String.valueOf(hook.id)); + request.setHeader("X-Hook-Name", hook.name); JsonObject payload = serializer.serialize(); String json = new Gson().toJson(payload); request.setRequestBody(json); try { - request.setHeader("X-Signature", sign(hook.secret, json)); + request.setHeader("X-Hub-Signature", sign(hook.secret, json)); } catch (InvalidKeyException | NoSuchAlgorithmException e) { log.error(e.toString()); return null; diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java index e43d49b..0c631d4 100644 --- a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -48,8 +48,13 @@ public Response put(String raw) throws WebhookException { @DELETE @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response delete(@FormParam("id") int id) { - WebhookRegistry.deregister(id); + public Response delete(@FormParam("id") int id, @FormParam("name") String name) { + if (name != null) { + WebhookRegistry.deregister(name); + } else { + WebhookRegistry.deregister(id); + } + return Response.noContent().build(); } } diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java index 5e23e88..4de29f4 100644 --- a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -106,6 +106,19 @@ public static void deregister(int id) { }); } + public static void deregister(String name) { + activeObjects.executeInTransaction(() -> { + WebhookEntity[] hooks = activeObjects.find(WebhookEntity.class, "NAME = ?", name); + for (WebhookEntity hook : hooks) { + for (EventEntity event : hook.getEvents()) { + activeObjects.delete(event); + } + activeObjects.delete(hook); + } + return null; + }); + } + private static int resolveID(String scope, String name) throws WebhookException { switch (scope) { case "repository": From a109cc7a8dce8b00d89d74f1ec3de733085d1ae1 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Wed, 13 Nov 2019 05:34:27 -0800 Subject: [PATCH 11/11] Exclude secret from being serialized --- src/main/java/com/sourcegraph/webhook/Dispatcher.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java index 4b8b84c..86577ba 100644 --- a/src/main/java/com/sourcegraph/webhook/Dispatcher.java +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -6,6 +6,7 @@ import com.atlassian.sal.api.net.Response; import com.atlassian.sal.api.net.ResponseException; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.sourcegraph.webhook.registry.Webhook; import org.slf4j.Logger; @@ -49,8 +50,12 @@ private static Request createRequest(Webhook hook, EventSerializer serializer) { request.setHeader("X-Event-Key", serializer.getName()); request.setHeader("X-Hook-ID", String.valueOf(hook.id)); request.setHeader("X-Hook-Name", hook.name); + JsonObject payload = serializer.serialize(); - String json = new Gson().toJson(payload); + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + String json = gson.toJson(payload); request.setRequestBody(json); try {