diff --git a/zanata-model/src/main/java/org/zanata/model/type/WebhookType.java b/zanata-model/src/main/java/org/zanata/model/type/WebhookType.java index 5480e43196..37a1a2b4d8 100644 --- a/zanata-model/src/main/java/org/zanata/model/type/WebhookType.java +++ b/zanata-model/src/main/java/org/zanata/model/type/WebhookType.java @@ -1,11 +1,23 @@ package org.zanata.model.type; +import lombok.Getter; + /** * Type of Webhook event. See {@link org.zanata.model.WebHook} for usage. * * @author Alex Eng aeng@redhat.com */ public enum WebhookType { - DocumentMilestoneEvent, - DocumentStatsEvent; + DocumentMilestoneEvent("Translation milestone"), + DocumentStatsEvent("Translation update"), + VersionChangedEvent("Project version"), + ProjectMaintainerChangedEvent("Maintainers update"), + SourceDocumentChangedEvent("Source document"); + + @Getter + private String displayName; + + WebhookType(String displayName) { + this.displayName = displayName; + } } diff --git a/zanata-war/src/main/java/org/zanata/action/ProjectHome.java b/zanata-war/src/main/java/org/zanata/action/ProjectHome.java index 404253c56b..016a1fc7ca 100644 --- a/zanata-war/src/main/java/org/zanata/action/ProjectHome.java +++ b/zanata-war/src/main/java/org/zanata/action/ProjectHome.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.ResourceBundle; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.enterprise.inject.Any; @@ -41,6 +42,7 @@ import javax.persistence.EntityManager; import javax.persistence.EntityNotFoundException; +import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; @@ -221,6 +223,8 @@ public Map getSelectedEnabledLocales() { @Setter private Boolean selectedCheckbox = Boolean.TRUE; + private Map> webhookUrlMapToType = null; + private List disabledLocales; public ProjectHome() { @@ -1070,56 +1074,126 @@ public List getValidationList() { @Getter public class WebhookTypeItem { + private String value; private String name; private String description; - public WebhookTypeItem(WebhookType type, String desc) { - this.name = type.name(); + public WebhookTypeItem(WebhookType webhookType, String desc) { + this.value = webhookType.name(); + this.name = webhookType.getDisplayName(); this.description = desc; } } - public List getWebhookTypes() { - List results = Lists.newArrayList(); - results.add(new WebhookTypeItem(WebhookType.DocumentMilestoneEvent, - msgs.get("jsf.webhookType.DocumentMilestoneEvent.desc"))); - results.add(new WebhookTypeItem(WebhookType.DocumentStatsEvent, - msgs.get("jsf.webhookType.DocumentStatsEvent.desc"))); + public List getAvailableWebhookTypes() { + WebhookTypeItem docMilestone = + new WebhookTypeItem(WebhookType.DocumentMilestoneEvent, + msgs.get( + "jsf.webhookType.DocumentMilestoneEvent.desc")); + + WebhookTypeItem stats = + new WebhookTypeItem(WebhookType.DocumentStatsEvent, + msgs.get("jsf.webhookType.DocumentStatsEvent.desc")); + + WebhookTypeItem version = + new WebhookTypeItem(WebhookType.VersionChangedEvent, + msgs.get("jsf.webhookType.VersionChangedEvent.desc")); + + WebhookTypeItem maintainer = + new WebhookTypeItem(WebhookType.ProjectMaintainerChangedEvent, + msgs.get("jsf.webhookType.ProjectMaintainerChangedEvent.desc")); + + WebhookTypeItem srcDoc = + new WebhookTypeItem(WebhookType.SourceDocumentChangedEvent, + msgs.get("jsf.webhookType.SourceDocumentChangedEvent.desc")); + + return Lists + .newArrayList(docMilestone, stats, version, maintainer, srcDoc); + } + + @Getter + public class WebhookItem { + // removed : and / from url for use in html + private String id; + private String url; + private List types; + + public WebhookItem(String url, List types) { + this.id = url.replaceAll(":", "").replaceAll("/", ""); + this.url = url; + this.types = types; + } + } + + public List getWebhooks() { + if (webhookUrlMapToType == null) { + webhookUrlMapToType = Maps.newHashMap(); + for (WebHook webHook : getInstance().getWebHooks()) { + List webHookTypes = webhookUrlMapToType.get(webHook.getUrl()); + if (webHookTypes == null) { + webHookTypes = Lists.newArrayList(); + } + webHookTypes.add(webHook.getWebhookType()); + webhookUrlMapToType.put(webHook.getUrl(), webHookTypes); + } + } + List results = Lists.newArrayList(); + results.addAll(webhookUrlMapToType + .entrySet().stream() + .map(entry -> new WebhookItem(entry.getKey(), Lists.transform( + entry.getValue(), new Function() { + @Override + public String apply(WebhookType type) { + return type.getDisplayName(); + } + }))).collect(Collectors.toList())); return results; } @Transactional - public void addWebHook(String url, String secret, String strType) { + public void addWebHook(String url, String secret, String strTypes) { identity.checkPermission(getInstance(), "update"); - WebhookType type = WebhookType.valueOf(strType); - if (isValidUrl(url, type)) { + List types = getTypes(strTypes); + if (isValidUrl(url, types)) { secret = StringUtils.isBlank(secret) ? null : secret; - WebHook webHook = + List newTypes = Lists.newArrayList(); + for(WebhookType type: types) { + WebHook webHook = new WebHook(this.getInstance(), url, type, secret); - getInstance().getWebHooks().add(webHook); + getInstance().getWebHooks().add(webHook); + newTypes.add(webHook.getWebhookType()); + } update(); + if (webhookUrlMapToType.containsKey(url)) { + webhookUrlMapToType.get(url).addAll(newTypes); + } else { + webhookUrlMapToType.put(url, newTypes); + } facesMessages.addGlobal( - msgs.format("jsf.project.AddNewWebhook", webHook.getUrl())); + msgs.format("jsf.project.AddNewWebhook", url)); } } @Transactional - public void removeWebHook(Long webhookId) { + public void removeWebHook(String url) { identity.checkPermission(getInstance(), "update"); - WebHook webHook = webHookDAO.findById(webhookId); - if (webHook != null) { - getInstance().getWebHooks().remove(webHook); - webHookDAO.makeTransient(webHook); + List webHooks = webHookDAO.findByUrl(url); + if (webHooks != null && !webHooks.isEmpty()) { + for(WebHook webHook: webHooks) { + getInstance().getWebHooks().remove(webHook); + webHookDAO.makeTransient(webHook); + } + webhookUrlMapToType.remove(url); facesMessages.addGlobal( - msgs.format("jsf.project.RemoveWebhook", webHook.getUrl())); + msgs.format("jsf.project.RemoveWebhook", url)); } } - public void testWebhook(String url, String secret, String strType) { + public void testWebhook(String url, String secret, String strTypes) { identity.checkPermission(getInstance(), "update"); - WebhookType type = WebhookType.valueOf(strType); - if (isValidUrl(url, type)) { + List types = getTypes(strTypes); + if (isValidUrl(url, types)) { TestEvent event = new TestEvent(identity.getAccountUsername(), getSlug()); WebHooksPublisher @@ -1127,10 +1201,22 @@ public void testWebhook(String url, String secret, String strType) { } } + private final Function convertToWebHookType = new Function() { + @Override + public WebhookType apply(String input) { + return WebhookType.valueOf(input); + } + }; + + private List getTypes(String strTypes) { + return Lists.transform(Lists.newArrayList(strTypes.split(",")), + convertToWebHookType); + } + /** * Check if url is valid and there is no duplication of url+type */ - private boolean isValidUrl(String url, WebhookType type) { + private boolean isValidUrl(String url, List types) { if (!UrlUtil.isValidUrl(url)) { facesMessages.addGlobal(SEVERITY_ERROR, msgs.format("jsf.project.InvalidUrl", url)); @@ -1138,9 +1224,11 @@ private boolean isValidUrl(String url, WebhookType type) { } for(WebHook webHook: getInstance().getWebHooks()) { if (StringUtils.equalsIgnoreCase(webHook.getUrl(), url) - && type.equals(webHook.getWebhookType())) { + && types.contains(webHook.getWebhookType())) { facesMessages.addGlobal(SEVERITY_ERROR, - msgs.get("jsf.project.DuplicateUrl")); + msgs.format("jsf.project.DuplicateUrl", + webHook.getUrl(), + webHook.getWebhookType().getDisplayName())); return false; } } diff --git a/zanata-war/src/main/java/org/zanata/action/VersionHome.java b/zanata-war/src/main/java/org/zanata/action/VersionHome.java index 20ff335cfb..218dc9d8c2 100644 --- a/zanata-war/src/main/java/org/zanata/action/VersionHome.java +++ b/zanata-war/src/main/java/org/zanata/action/VersionHome.java @@ -28,6 +28,7 @@ import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; @@ -61,6 +62,8 @@ import org.zanata.model.HLocale; import org.zanata.model.HProject; import org.zanata.model.HProjectIteration; +import org.zanata.model.WebHook; +import org.zanata.model.type.WebhookType; import org.zanata.model.validator.SlugValidator; import org.zanata.seam.scope.ConversationScopeMessages; import org.zanata.security.ZanataIdentity; @@ -68,9 +71,11 @@ import org.zanata.service.SlugEntityService; import org.zanata.service.ValidationService; import org.zanata.service.impl.LocaleServiceImpl; +import org.zanata.service.impl.WebHooksPublisher; import org.zanata.ui.faces.FacesMessages; import org.zanata.util.ComparatorUtil; import org.zanata.util.UrlUtil; +import org.zanata.webhook.events.VersionChangedEvent; import org.zanata.webtrans.shared.model.ValidationAction; import org.zanata.webtrans.shared.model.ValidationId; import org.zanata.webtrans.shared.validation.ValidationFactory; @@ -82,9 +87,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import java.util.stream.Collectors; @Named("versionHome") @ViewScoped @@ -429,10 +436,11 @@ public void setSlug(String slug) { @Override @Transactional public String persist() { - if (!validateSlug(getInputSlugValue(), "slug")) { + String slug = getInputSlugValue(); + if (!validateSlug(slug, "slug")) { return null; } - getInstance().setSlug(getInputSlugValue()); + getInstance().setSlug(slug); updateProjectType(); HProject project = getProject(); @@ -450,7 +458,32 @@ public String persist() { getInstance().getCustomizedValidations().putAll( project.getCustomizedValidations()); - return super.persist(); + String result = super.persist(); + processWebhookNewVersion(slug, VersionChangedEvent.ChangeType.CREATED, + getInstance().getCreationDate()); + return result; + } + + private void processWebhookNewVersion(String versionSlug, + VersionChangedEvent.ChangeType changeType, Date date) { + List versionWebhooks = + getProject().getWebHooks().stream().filter( + webHook -> webHook.getWebhookType() + .equals(WebhookType.VersionChangedEvent)) + .collect(Collectors.toList()); + + if (versionWebhooks.isEmpty()) { + return; + } + + VersionChangedEvent event = + new VersionChangedEvent(getProjectSlug(), + versionSlug, changeType, date); + for (WebHook webhook : versionWebhooks) { + WebHooksPublisher + .publish(webhook.getUrl(), event, + Optional.fromNullable(webhook.getSecret())); + } } @Override @@ -559,7 +592,10 @@ public void updateStatus(char initial) { @Transactional public void deleteSelf() { + String slug = getInstance().getSlug(); updateStatus('O'); + processWebhookNewVersion(slug, VersionChangedEvent.ChangeType.DELETED, + getInstance().getLastChanged()); } @Transactional diff --git a/zanata-war/src/main/java/org/zanata/dao/WebHookDAO.java b/zanata-war/src/main/java/org/zanata/dao/WebHookDAO.java index ab0e5659a7..ee2b616ccf 100644 --- a/zanata-war/src/main/java/org/zanata/dao/WebHookDAO.java +++ b/zanata-war/src/main/java/org/zanata/dao/WebHookDAO.java @@ -1,11 +1,14 @@ package org.zanata.dao; +import org.hibernate.Query; import org.hibernate.Session; import javax.enterprise.context.RequestScoped; import javax.inject.Named; import org.zanata.model.WebHook; +import java.util.List; + @Named("webHookDAO") @RequestScoped public class WebHookDAO extends AbstractDAOImpl { @@ -17,4 +20,12 @@ public WebHookDAO() { public WebHookDAO(Session session) { super(WebHook.class, session); } + + public List findByUrl(String url) { + Query q = getSession().createQuery("from WebHook where url =:url") + .setParameter("url", url) + .setCacheable(true) + .setComment("WebHookDAO.findByUrl"); + return q.list(); + } } diff --git a/zanata-war/src/main/java/org/zanata/webhook/events/VersionChangedEvent.java b/zanata-war/src/main/java/org/zanata/webhook/events/VersionChangedEvent.java new file mode 100644 index 0000000000..2d8acfb7b4 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/webhook/events/VersionChangedEvent.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016, Red Hat, Inc. and individual contributors as indicated by the + * @author tags. See the copyright.txt file in the distribution for a full + * listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this software; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF + * site: http://www.fsf.org. + */ +package org.zanata.webhook.events; + +import java.util.Date; +import java.util.Map; + +import org.codehaus.jackson.annotate.JsonPropertyOrder; +import org.zanata.common.ContentState; +import org.zanata.common.LocaleId; +import org.zanata.events.WebhookEventType; +import org.zanata.model.type.WebhookType; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * + * Event for when a version is created/removed in a project + * + * @author Alex Eng aeng@redhat.com + */ +@Getter +@Setter +@AllArgsConstructor +@JsonPropertyOrder({"project", "version", "changeType", "date"}) +@EqualsAndHashCode +public class VersionChangedEvent extends WebhookEventType { + + private static final String EVENT_TYPE = + WebhookType.VersionChangedEvent.name(); + + public static enum ChangeType { + CREATED, + DELETED + } + + /** + * Target project slug. + * {@link org.zanata.model.HProject#slug} + */ + private final String project; + + /** + * Target project version slug. + * {@link org.zanata.model.HProjectIteration#slug} + */ + private final String version; + + /** + * Change type + */ + private final ChangeType changeType; + + /** + * Timestamp + */ + private final Date date; + + @Override + public String getType() { + return EVENT_TYPE; + } +} diff --git a/zanata-war/src/main/resources/messages.properties b/zanata-war/src/main/resources/messages.properties index 09a1bbd8a3..f86ffcfa08 100644 --- a/zanata-war/src/main/resources/messages.properties +++ b/zanata-war/src/main/resources/messages.properties @@ -318,17 +318,21 @@ jsf.project.LanguageUpdateFromGlobal=Updated languages from global settings. jsf.project.AboutPageUpdated=About page updated. jsf.project.AboutPageUpdateFailed=There was a problem while updating the about page. jsf.project.AddWebhook=Add webhook +jsf.project.NewWebhook=New webhook jsf.project.RemoveWebhook=Webhook {0} removed. jsf.project.AddNewWebhook=Webhook {0} added. jsf.project.PayloadURL=Payload URL jsf.project.WebhookType.label=Type jsf.project.InvalidUrl=Invalid URL: {0} -jsf.project.DuplicateUrl=Same URL and type is already in the list. +jsf.project.DuplicateUrl=Same URL and type is already in the list. {0}, {1} jsf.webhook.response.state={0}% {1} jsf.webhook.test.label=Test webhook jsf.webhook.test.tooltip=Fire a test event -jsf.webhookType.DocumentMilestoneEvent.desc=Trigger when a document is 100% translated or approved -jsf.webhookType.DocumentStatsEvent.desc=Trigger when translations are updated (singly or in a batch) +jsf.webhookType.DocumentMilestoneEvent.desc=A document is 100% translated or approved +jsf.webhookType.DocumentStatsEvent.desc=Translations are updated (singly or in a batch) +jsf.webhookType.VersionChangedEvent.desc=Project version is created or removed +jsf.webhookType.ProjectMaintainerChangedEvent.desc=Project maintainer is added or removed +jsf.webhookType.SourceDocumentChangedEvent.desc=Source document is added or removed #------ [home] > Projects > [project-id] ------ jsf.ReadOnlyVersions=Read-only versions diff --git a/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml b/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml new file mode 100644 index 0000000000..422fb931f2 --- /dev/null +++ b/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml @@ -0,0 +1,146 @@ + + + +

+ #{msgs['jsf.project.WebHooks']} + + + + + +

+ + +
+

#{msgs['jsf.project.NewWebhook']}

+
+ + +
+
+ + +
+ +
+ + +
+ + #{webhookType.description} + +
+
+ +
+ +
+ + + +
+
+ + + +
    + +
  • + #{webhook.url} + #{webhook.types} + + +
  • +
    +
+
+
+
+ +
+ +
diff --git a/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml b/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml index 9dcdd40206..8412a270b3 100644 --- a/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml +++ b/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml @@ -21,28 +21,28 @@ + action="#{projectHome.testWebhook(url, secret, types)}"> - + - - + + - +
  • -

    - #{msgs['jsf.project.WebHooks']} - - - - - -

    - - - - -
      - -
    • - #{webhook.url} - #{webhook.webhookType.name()} - -
    • -
      -
    -
    -
    -
    -
    -

    - #{msgs['jsf.project.AddWebhook']} -

    -
    - - -
    -
    - - -
    -
    - - -
    - -
    - - - -
    -
    -
    +
  • diff --git a/zanata-war/src/main/webapp/WEB-INF/layout/project/webhook-form.xhtml b/zanata-war/src/main/webapp/WEB-INF/layout/project/webhook-form.xhtml new file mode 100644 index 0000000000..29ee4b720e --- /dev/null +++ b/zanata-war/src/main/webapp/WEB-INF/layout/project/webhook-form.xhtml @@ -0,0 +1,37 @@ + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + + #{webhookType.description} + +
    +
    + +
    +
    +