diff --git a/docs/images/project-webhooks-edit.png b/docs/images/project-webhooks-edit.png new file mode 100644 index 0000000000..58079065cc Binary files /dev/null and b/docs/images/project-webhooks-edit.png differ diff --git a/docs/images/project-webhooks-new.png b/docs/images/project-webhooks-new.png new file mode 100644 index 0000000000..43b358f655 Binary files /dev/null and b/docs/images/project-webhooks-new.png differ diff --git a/docs/images/project-webhooks-settings.png b/docs/images/project-webhooks-settings.png index 24f8425567..4701730e5d 100644 Binary files a/docs/images/project-webhooks-settings.png and b/docs/images/project-webhooks-settings.png differ diff --git a/docs/user-guide/projects/project-settings.md b/docs/user-guide/projects/project-settings.md index d1f81a5df6..dbfa2b02c7 100644 --- a/docs/user-guide/projects/project-settings.md +++ b/docs/user-guide/projects/project-settings.md @@ -146,29 +146,57 @@ The access restriction feature is intended for use with special roles that can b
Project Webhooks Settings tab
-The Webhooks feature is HTTP callbacks which are triggered when a document in a language has reached a certain milestone. -Currently, webhooks events will be triggered when - -- A document has reached 100% Translated -- A document has reached 100% Approved (by reviewer) - +The Webhooks feature is HTTP callbacks which are triggered when a selected event happens. When an event occurs, Zanata will make a HTTP POST to the provided payload URL in the project. +Types of events available: -Example webhook response: - +#### Translation Milestone +Trigger when document has reached 100% Translated or Approved +``` +{ + "type": "org.zanata.events.DocumentMilestoneEvent", + "milestone": "100% Translated", + "locale": "de", + "docId": "zanata-war/src/main/resources/messages", + "version": "master", + "project": "zanata-server", + "editorDocumentUrl": "https://translate.zanata.org/zanata/webtrans/Application.xhtml?project=zanata-server&iteration=master&localeId=de&locale=en#view:doc;doc:zanata-war/src/main/resources/messages" +} +``` + +#### Translation update +Trigger when translation is updated ``` { - "eventType": "org.zanata.events.DocumentMilestoneEvent", - "milestone": "100% Translated", - "locale": "de", - "docId": "zanata-war/src/main/resources/messages", - "version": "master", - "project": "zanata-server", - "editorDocumentUrl": "https://translate.zanata.org/zanata/webtrans/Application.xhtml?project=zanata-server&iteration=master&localeId=de&locale=en#view:doc;doc:zanata-war/src/main/resources/messages" + "username":"aeng", + "project":"Zanata", + "version":"master", + "docId":"doc1id", + "locale":"zh-CN", + "wordDeltasByState":{"New":-16,"Translated":16}, + "type":"DocumentStatsEvent" } ``` -If a secret key is provided for that payload URL, Zanata will sign the webhook request with HTTP header `X-Zanata-Webhook`. +#### Project version +Trigger when a version is added to removed from a project +``` +{"project":"zanata","version":"new-version","changeType":"CREATE","type":"VersionChangedEvent"} +``` + +#### Project maintainer +Trigger when a project maintainer is added or removed from project +``` +{"project":"zanata","username":"aeng","changeType":"REMOVE","role":"Maintainer","type":"ProjectMaintainerChangedEvent"} +``` + +#### Document +Trigger when a source document is added or removed from project version +``` +{"project":"zanata","version":"new-version"","changeType":"ADD","type":"SourceDocumentChangedEvent"} +``` + +If a secret key is provided for that payload URL, Zanata will sign the webhook request with HTTP header `X-Zanata-Webhook`. The header is a double hash of `HMAC-SHA1` in base64 digest. The double hash is generated from the full request body and the payload URL as provided. Here is some sample pseudocode for checking the validity of a request: @@ -181,15 +209,24 @@ boolean verifyRequest(request, secret, callbackURL) { } ``` +### Adding a new webhook +
+![Project Webhooks New](/images/project-webhooks-new.png) +
+1. Click on 'New webhook' button. +2. Enter a valid URL and secret key (optional) into the provided text input. +3. Select webhook types for this URL. +4. Click on 'Add webhook' button to add the URL. -### Adding a webhook -1. Enter a valid URL and secret key (optional) into the provided text input. -2. Click on 'Add webhook' button to add the URL. -3. If secret key is provided, Zanata will include cryptographic signature in HTTP header `X-Zanata-Webhook`. - -### Remove a webhook -- Click on the `X` sign on right side of the webhook to remove the entry. +### Update a webhook +
+![Project Webhooks Edit](/images/project-webhooks-edit.png) +
+1. Click on 'Edit' button on the right of the webhook entry +2. Update any value in the form +3. Click on 'Update' to save the changes +4. To remove webhook, click on button 'Delete' ------------ diff --git a/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectWebHooksTab.java b/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectWebHooksTab.java index 5cdfe01d45..5a0badba97 100644 --- a/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectWebHooksTab.java +++ b/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectWebHooksTab.java @@ -24,6 +24,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.zanata.page.projects.ProjectBasePage; @@ -42,18 +43,33 @@ public class ProjectWebHooksTab extends ProjectBasePage { private By webHooksForm = By.id("settings-webhooks-form"); - private By saveWebhookButton = By.id("add-webhook-button"); - private By urlInputField = By.id("payloadUrlInput"); - private By secretInputField = By.id("secretInput"); + private By newWebHooksForm = By.id("newWebhook"); + private By deleteBtn = By.name("deleteWebhookBtn"); + private By editBtn = By.name("editBtn"); + + private final JavascriptExecutor jsExecutor = + (JavascriptExecutor) getDriver(); public ProjectWebHooksTab(WebDriver driver) { super(driver); } - public ProjectWebHooksTab enterUrl(String url, String key) { - enterText(readyElement(urlInputField), url); - enterText(readyElement(secretInputField), key); - readyElement(saveWebhookButton).click(); + public WebElement getParentElement(WebElement child) { + return (WebElement) jsExecutor + .executeScript("return arguments[0].parentNode;", child); + } + + public ProjectWebHooksTab enterUrl(String url, String key, List types) { + enterText(getUrlInputField(newWebHooksForm), url); + enterText(getSecretInputField(newWebHooksForm), key); + + for (String type : types) { + WebElement checkbox = readyElement(newWebHooksForm) + .findElement(By.cssSelector("input[value=" + type + "]")); + getParentElement(checkbox).click(); + } + + getSaveWebhookButton(newWebHooksForm).click(); return new ProjectWebHooksTab(getDriver()); } @@ -65,7 +81,14 @@ public List getWebHooks() { return list.stream().map(element -> new WebhookItem( element.findElement(By.name("url")).getText(), - element.findElement(By.name("type")).getText())) + getSelectedTypes(element))) + .collect(Collectors.toList()); + } + + private List getSelectedTypes(WebElement parentForm) { + return parentForm.findElement(By.name("types")) + .findElements(By.cssSelector("input[checked=checked]")).stream() + .map(input -> input.getAttribute("value")) .collect(Collectors.toList()); } @@ -85,7 +108,12 @@ public ProjectWebHooksTab clickRemoveOn(String url) { boolean clicked = false; for (WebElement listItem : listItems) { if (listItem.getText().contains(url)) { - listItem.findElement(By.tagName("button")).click(); + listItem.findElement(editBtn).click(); + WebElement deleteButton = listItem.findElement(deleteBtn); + if (!deleteButton.isDisplayed()) { + listItem.findElement(editBtn).click(); + } + deleteButton.click(); clicked = true; break; } @@ -109,6 +137,18 @@ private List getWebhookList() { @AllArgsConstructor public class WebhookItem { private String url; - private String type; + private List types; + } + + private WebElement getSaveWebhookButton(By parentId) { + return readyElement(parentId).findElement(By.name("addWebhookBtn")); + } + + private WebElement getUrlInputField(By parentId) { + return readyElement(parentId).findElement(By.name("payloadUrlInput")); + } + + private WebElement getSecretInputField(By parentId) { + return readyElement(parentId).findElement(By.name("secretInput")); } } diff --git a/functional-test/src/test/java/org/zanata/feature/project/EditWebHooksTest.java b/functional-test/src/test/java/org/zanata/feature/project/EditWebHooksTest.java index 3f55befca9..da49826e7e 100644 --- a/functional-test/src/test/java/org/zanata/feature/project/EditWebHooksTest.java +++ b/functional-test/src/test/java/org/zanata/feature/project/EditWebHooksTest.java @@ -20,6 +20,7 @@ */ package org.zanata.feature.project; +import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -31,6 +32,8 @@ import org.zanata.workflow.LoginWorkFlow; import org.zanata.workflow.ProjectWorkFlow; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; /** * @author Damian Jansen djansen@redhat.com @@ -52,11 +55,12 @@ public void before() { public void addWebHook() throws Exception { String testUrl = "http://www.example.com"; String key = "secret_key"; + List types = Lists.newArrayList("DocumentMilestoneEvent"); ProjectWebHooksTab projectWebHooksTab = new ProjectWorkFlow() .goToProjectByName("about fedora") .gotoSettingsTab() .gotoSettingsWebHooksTab() - .enterUrl(testUrl, key); + .enterUrl(testUrl, key, types); assertThat(projectWebHooksTab.getWebHooks()) .extracting("url") @@ -70,11 +74,12 @@ public void addWebHook() throws Exception { public void removeWebHook() throws Exception { String testUrl = "http://www.example.com"; String key = "secret_key"; + List types = Lists.newArrayList("DocumentMilestoneEvent"); ProjectWebHooksTab projectWebHooksTab = new ProjectWorkFlow() .goToProjectByName("about fedora") .gotoSettingsTab() .gotoSettingsWebHooksTab() - .enterUrl(testUrl, key) + .enterUrl(testUrl, key, types) .expectWebHooksContains(testUrl) .clickRemoveOn(testUrl); diff --git a/zanata-model/src/main/java/org/zanata/model/HProject.java b/zanata-model/src/main/java/org/zanata/model/HProject.java index 387dd16286..ea4aaeb340 100644 --- a/zanata-model/src/main/java/org/zanata/model/HProject.java +++ b/zanata-model/src/main/java/org/zanata/model/HProject.java @@ -153,7 +153,8 @@ public class HProject extends SlugEntityBase implements Serializable, @Column(name = "alias", nullable = false) private Map localeAliases = Maps.newHashMap(); - @OneToMany(mappedBy = "project", cascade = CascadeType.ALL) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "project", + orphanRemoval = true) private List webHooks = Lists.newArrayList(); @Enumerated(EnumType.STRING) @@ -221,7 +222,7 @@ public void addIteration(HProjectIteration iteration) { * @see {@link #getMaintainers} */ public void addMaintainer(HPerson maintainer) { - getMembers().add(asMember(maintainer, Maintainer)); + getMembers().add(new HProjectMember(this, maintainer, Maintainer)); } /** @@ -236,87 +237,8 @@ public void removeMaintainer(HPerson maintainer) { // there is only one maintainer then removal of any other person would // do nothing anyway. if (getMaintainers().size() > 1) { - getMembers().remove(asMember(maintainer, Maintainer)); - } - } - - /** - * Update all security settings for a person. - * - * The HPerson and HLocale entities in memberships must be attached to avoid - * persistence problems with Hibernate. - */ - public void updateProjectPermissions(PersonProjectMemberships memberships) { - HPerson person = memberships.getPerson(); - - boolean wasMaintainer = getMaintainers().contains(memberships.getPerson()); - boolean isLastMaintainer = wasMaintainer && getMaintainers().size() <= 1; - // business rule: every project must have at least one maintainer - boolean isMaintainer = isLastMaintainer || memberships.isMaintainer(); - - ensureMembership(isMaintainer, asMember(person, Maintainer)); - - // business rule: if someone is a Maintainer, they must also be a TranslationMaintainer - boolean isTranslationMaintainer = memberships.isMaintainer() || - memberships.isTranslationMaintainer(); - - ensureMembership(isTranslationMaintainer, asMember(person, TranslationMaintainer)); - } - - public void updateLocalePermissions(PersonProjectMemberships memberships) { - HPerson person = memberships.getPerson(); - - for (PersonProjectMemberships.LocaleRoles localeRoles - : memberships.getLocaleRoles()) { - HLocale locale = localeRoles.getLocale(); - ensureMembership(localeRoles.isTranslator(), asMember(locale, person, Translator)); - ensureMembership(localeRoles.isReviewer(), asMember(locale, person, Reviewer)); - ensureMembership(localeRoles.isCoordinator(), asMember(locale, person, Coordinator)); - ensureMembership(localeRoles.isGlossarist(), asMember(locale, person, Glossarist)); - } - } - - /** - * Get a person as a member object in this project for a role. - */ - private HProjectMember asMember(HPerson person, ProjectRole role) { - return new HProjectMember(this, person, role); - } - - /** - * Get a person as a member object in this project for a locale-specific role. - */ - private HProjectLocaleMember asMember(HLocale locale, HPerson person, LocaleRole role) { - return new HProjectLocaleMember(this, locale, person, role); - } - - /** - * Ensure the given membership is present or absent. - */ - private void ensureMembership(boolean shouldBePresent, HProjectMember membership) { - final Set members = getMembers(); - final boolean isPresent = members.contains(membership); - if (isPresent != shouldBePresent) { - if (shouldBePresent) { - members.add(membership); - } else { - members.remove(membership); - } - } - } - - /** - * Ensure the given locale membership is present or absent. - */ - private void ensureMembership(boolean shouldBePresent, HProjectLocaleMember membership) { - final Set members = getLocaleMembers(); - final boolean isPresent = members.contains(membership); - if (isPresent != shouldBePresent) { - if (shouldBePresent) { - members.add(membership); - } else { - members.remove(membership); - } + getMembers() + .remove(new HProjectMember(this, maintainer, Maintainer)); } } diff --git a/zanata-model/src/main/java/org/zanata/model/WebHook.java b/zanata-model/src/main/java/org/zanata/model/WebHook.java index 74bfcd5cf7..486c2b72b5 100644 --- a/zanata-model/src/main/java/org/zanata/model/WebHook.java +++ b/zanata-model/src/main/java/org/zanata/model/WebHook.java @@ -22,26 +22,32 @@ package org.zanata.model; import java.io.Serializable; +import java.util.Set; +import javax.persistence.CascadeType; import javax.persistence.Column; +import javax.persistence.ElementCollection; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Transient; +import javax.persistence.UniqueConstraint; import javax.validation.constraints.Size; +import com.google.common.collect.Sets; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; -import org.hibernate.annotations.TypeDefs; import org.zanata.model.type.WebhookType; -import org.zanata.model.type.WebhookTypeType; import org.zanata.model.validator.Url; /** @@ -51,9 +57,7 @@ @Getter @Setter(AccessLevel.PRIVATE) @NoArgsConstructor -@TypeDefs({ - @TypeDef(name = "webhookType", typeClass = WebhookTypeType.class) -}) +@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"url", "projectId"})) public class WebHook implements Serializable { private Long id; @@ -61,9 +65,10 @@ public class WebHook implements Serializable { private HProject project; @Url + @Size(max = 255) private String url; - private WebhookType webhookType; + private Set types = Sets.newHashSet(); /** * Secret key used to generate webhook header in hmac-sha1 encryption. @@ -72,10 +77,11 @@ public class WebHook implements Serializable { @Column(nullable = true) private String secret; - public WebHook(HProject project, String url, WebhookType webhookType, String secret) { + public WebHook(HProject project, String url, Set types, + String secret) { this.project = project; this.url = url; - this.webhookType = webhookType; + this.types = types; this.secret = secret; } @@ -85,15 +91,44 @@ public Long getId() { return id; } - @ManyToOne + @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST, + CascadeType.REFRESH }) @JoinColumn(name = "projectId", nullable = false) public HProject getProject() { return project; } - @Type(type = "webhookType") - public WebhookType getWebhookType() { - return webhookType; + @ElementCollection + @Enumerated(EnumType.STRING) + @JoinTable(name = "WebHook_WebHookType", + joinColumns = @JoinColumn(name = "webhookId")) + @Column(name = "type", nullable = false) + public Set getTypes() { + return types; } + /** + * This will replace all properties with given ones. + * + * @param url - new url + * @param newTypes - new types + * @param secret - new secret key + */ + @Transient + public void update(String url, Set newTypes, String secret) { + this.url = url; + this.secret = secret; + + /** + * Copy all newTypes into currentTypes and remove those that are not + * in the newTypes + */ + this.types.addAll(newTypes); + Set currentTypes = Sets.newHashSet(this.types); + for (WebhookType type: currentTypes) { + if (!newTypes.contains(type)) { + this.types.remove(type); + } + } + } } 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..910b7d1ec8 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,25 @@ package org.zanata.model.type; +import lombok.Getter; + +import java.io.Serializable; + /** * Type of Webhook event. See {@link org.zanata.model.WebHook} for usage. * * @author Alex Eng aeng@redhat.com */ -public enum WebhookType { - DocumentMilestoneEvent, - DocumentStatsEvent; +public enum WebhookType implements Serializable { + DocumentMilestoneEvent("Translation milestone"), + DocumentStatsEvent("Translation update"), + VersionChangedEvent("Project version"), + ProjectMaintainerChangedEvent("Project maintainer update"), + SourceDocumentChangedEvent("Document"); + + @Getter + private String displayName; + + WebhookType(String displayName) { + this.displayName = displayName; + } } diff --git a/zanata-model/src/main/java/org/zanata/model/type/WebhookTypeType.java b/zanata-model/src/main/java/org/zanata/model/type/WebhookTypeType.java deleted file mode 100644 index 9c5ba5b701..0000000000 --- a/zanata-model/src/main/java/org/zanata/model/type/WebhookTypeType.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2015, 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.model.type; - -import org.hibernate.MappingException; -import org.hibernate.dialect.Dialect; -import org.hibernate.type.AbstractSingleColumnStandardBasicType; -import org.hibernate.type.DiscriminatorType; -import org.hibernate.type.StringType; - -/** - * @author Alex Eng aeng@redhat.com - */ -public class WebhookTypeType extends AbstractSingleColumnStandardBasicType - implements DiscriminatorType { - - public WebhookTypeType() { - super(StringType.INSTANCE.getSqlTypeDescriptor(), - WebhookTypeTypeDescriptor.INSTANCE); - } - - @Override - public String toString(WebhookType value) { - return value.name(); - } - - @Override - public String getName() { - return "webhookType"; - } - - @Override - public String objectToSQLString(WebhookType value, Dialect dialect) - throws Exception { - return "\'" + toString(value) + "\'"; - } - - public WebhookType stringToObject(String xml) throws Exception { - if (xml.length() < 1) { - throw new MappingException( - "multiple or zero characters found parsing string"); - } - return WebhookType.valueOf(xml); - } - - public WebhookType fromStringValue(String xml) { - return WebhookType.valueOf(xml); - } -} diff --git a/zanata-model/src/main/java/org/zanata/model/type/WebhookTypeTypeDescriptor.java b/zanata-model/src/main/java/org/zanata/model/type/WebhookTypeTypeDescriptor.java deleted file mode 100644 index df86daaa05..0000000000 --- a/zanata-model/src/main/java/org/zanata/model/type/WebhookTypeTypeDescriptor.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2015, 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.model.type; - -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.AbstractTypeDescriptor; - -/** - * @author Alex Eng aeng@redhat.com - */ -public class WebhookTypeTypeDescriptor extends - AbstractTypeDescriptor { - - public static final WebhookTypeTypeDescriptor INSTANCE = - new WebhookTypeTypeDescriptor(); - - public WebhookTypeTypeDescriptor() { - super(WebhookType.class); - } - - @Override - public WebhookType fromString(String string) { - if (string == null) { - return null; - } else { - return WebhookType.valueOf(string); - } - } - - @Override - public String toString(WebhookType value) { - return value.name(); - } - - @Override - public X unwrap(WebhookType value, Class type, - WrapperOptions options) { - if (value == null) { - return null; - } - if (String.class.isAssignableFrom(type)) { - return (X) String.valueOf(value.name()); - } - throw unknownUnwrap(type); - } - - @Override - public WebhookType wrap(X value, WrapperOptions options) { - if (value == null) { - return null; - } - if (String.class.isInstance(value)) { - return WebhookType.valueOf((String) value); - } - throw unknownWrap(value.getClass()); - } -} diff --git a/zanata-model/src/main/java/org/zanata/util/ZanataEntities.java b/zanata-model/src/main/java/org/zanata/util/ZanataEntities.java index 0a536c491e..9ecfa891bd 100644 --- a/zanata-model/src/main/java/org/zanata/util/ZanataEntities.java +++ b/zanata-model/src/main/java/org/zanata/util/ZanataEntities.java @@ -23,6 +23,7 @@ import java.util.List; import org.zanata.model.Activity; +import org.zanata.model.Glossary; import org.zanata.model.HAccount; import org.zanata.model.HAccountActivationKey; import org.zanata.model.HAccountResetPasswordKey; @@ -76,7 +77,7 @@ public static List entitiesForRemoval() { TransMemory.class); builder.add(Activity.class); // glossary - builder.add(HGlossaryTerm.class, HGlossaryEntry.class); + builder.add(HGlossaryTerm.class, HGlossaryEntry.class, Glossary.class); // text flows and targets builder.add(HPoTargetHeader.class, HTextFlowTargetHistory.class, HTextFlowTargetReviewComment.class, 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..5f199a35a9 100644 --- a/zanata-war/src/main/java/org/zanata/action/ProjectHome.java +++ b/zanata-war/src/main/java/org/zanata/action/ProjectHome.java @@ -29,6 +29,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; @@ -41,7 +42,6 @@ import javax.persistence.EntityManager; import javax.persistence.EntityNotFoundException; -import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; @@ -77,9 +77,10 @@ import org.zanata.security.ZanataIdentity; import org.zanata.security.annotations.Authenticated; import org.zanata.service.LocaleService; +import org.zanata.service.ProjectService; import org.zanata.service.SlugEntityService; import org.zanata.service.ValidationService; -import org.zanata.service.impl.WebHooksPublisher; +import org.zanata.service.impl.WebhookServiceImpl; import org.zanata.ui.AbstractListFilter; import org.zanata.ui.InMemoryListFilter; import org.zanata.ui.autocomplete.MaintainerAutocomplete; @@ -87,7 +88,7 @@ import org.zanata.util.CommonMarkRenderer; import org.zanata.util.ComparatorUtil; import org.zanata.util.UrlUtil; -import org.zanata.webhook.events.TestEvent; +import org.zanata.webhook.events.ProjectMaintainerChangedEvent; import org.zanata.webtrans.shared.model.ValidationAction; import org.zanata.webtrans.shared.model.ValidationId; import org.zanata.webtrans.shared.validation.ValidationFactory; @@ -97,6 +98,8 @@ import static javax.faces.application.FacesMessage.SEVERITY_ERROR; import static javax.faces.application.FacesMessage.SEVERITY_INFO; +import static org.zanata.service.impl.WebhookServiceImpl.getTypesFromString; +import static org.zanata.model.ProjectRole.Maintainer; @Named("projectHome") @Slf4j @@ -174,6 +177,12 @@ public class ProjectHome extends SlugHome implements @Inject private CopyTransOptionsModel copyTransOptionsModel; + @Inject + private WebhookServiceImpl webhookServiceImpl; + + @Inject + private ProjectService projectServiceImpl; + @Inject private UrlUtil urlUtil; @@ -848,6 +857,12 @@ public String persist() { validationAction.getState().name()); } retValue = super.persist(); + + webhookServiceImpl.processWebhookMaintainerChanged( + getInstance().getSlug(), + creator.getAccount().getUsername(), Maintainer, + getInstance().getWebHooks(), + ProjectMaintainerChangedEvent.ChangeType.ADD); } return retValue; } @@ -878,10 +893,13 @@ public String removeMaintainer(HPerson person) { getInstance().removeMaintainer(person); maintainerFilter.reset(); update(); - facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.format("jsf.project.MaintainerRemoved", person.getName())); + webhookServiceImpl.processWebhookMaintainerChanged(getSlug(), + person.getAccount().getUsername(), Maintainer, + getInstance().getWebHooks(), + ProjectMaintainerChangedEvent.ChangeType.REMOVE); if (person.equals(authenticatedAccount.getPerson())) { urlUtil.redirectToInternal(urlUtil.projectUrl(getSlug())); } @@ -1068,82 +1086,93 @@ public List getValidationList() { return sortedList; } - @Getter - public class WebhookTypeItem { - private String name; - private String description; - - public WebhookTypeItem(WebhookType type, String desc) { - this.name = type.name(); - 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"))); - 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)) { - secret = StringUtils.isBlank(secret) ? null : secret; - WebHook webHook = - new WebHook(this.getInstance(), url, type, secret); - getInstance().getWebHooks().add(webHook); - update(); + Set types = getTypesFromString(strTypes); + if(types.isEmpty()) { + facesMessages.addGlobal(msgs.get("jsf.project.webhookType.empty")); + return; + } + if (!isValidUrl(url)) { + return; + } + if (projectServiceImpl.isDuplicateWebhookUrl(getInstance(), url)) { + facesMessages.addGlobal(SEVERITY_ERROR, + msgs.format("jsf.project.DuplicateUrl", url)); + return; + } + boolean isAdded = projectServiceImpl.addWebhook(getInstance(), url, + secret, types); + if (isAdded) { 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 id) { identity.checkPermission(getInstance(), "update"); - WebHook webHook = webHookDAO.findById(webhookId); + WebHook webHook = webHookDAO.findById(new Long(id)); + String url = webHook.getUrl(); if (webHook != null) { getInstance().getWebHooks().remove(webHook); webHookDAO.makeTransient(webHook); facesMessages.addGlobal( - msgs.format("jsf.project.RemoveWebhook", webHook.getUrl())); + msgs.format("jsf.project.RemoveWebhook", url)); + } + } + + @Transactional + public void updateWebhook(String id, String url, String secret, + String strTypes) { + identity.checkPermission(getInstance(), "update"); + Set types = getTypesFromString(strTypes); + if(types.isEmpty()) { + facesMessages.addGlobal(msgs.get("jsf.project.webhookType.empty")); + return; + } + if (!isValidUrl(url)) { + return; + } + Long webhookId = new Long(id); + if (projectServiceImpl.isDuplicateWebhookUrl(getInstance(), url, + webhookId)) { + facesMessages.addGlobal(SEVERITY_ERROR, + msgs.format("jsf.project.DuplicateUrl", url)); + return; + } + boolean updated = projectServiceImpl.updateWebhook(getInstance(), + webhookId, url, secret, types); + if (updated) { + facesMessages.addGlobal( + msgs.format("jsf.project.UpdateWebhook", url)); } } - public void testWebhook(String url, String secret, String strType) { + public void testWebhook(String url, String secret) { identity.checkPermission(getInstance(), "update"); - WebhookType type = WebhookType.valueOf(strType); - if (isValidUrl(url, type)) { - TestEvent event = - new TestEvent(identity.getAccountUsername(), getSlug()); - WebHooksPublisher - .publish(url, event, Optional.fromNullable(secret)); + if (projectServiceImpl.isDuplicateWebhookUrl(getInstance(), url)) { + facesMessages.addGlobal(SEVERITY_ERROR, + msgs.format("jsf.project.DuplicateUrl", url)); + return; } + if (!isValidUrl(url)) { + return; + } + webhookServiceImpl.processTestEvent(identity.getAccountUsername(), + getSlug(), url, secret); } /** * Check if url is valid and there is no duplication of url+type */ - private boolean isValidUrl(String url, WebhookType type) { - if (!UrlUtil.isValidUrl(url)) { + private boolean isValidUrl(String url) { + if (!webhookServiceImpl.isValidUrl(url)) { facesMessages.addGlobal(SEVERITY_ERROR, - msgs.format("jsf.project.InvalidUrl", url)); + msgs.format("jsf.project.InvalidUrl", url)); return false; } - for(WebHook webHook: getInstance().getWebHooks()) { - if (StringUtils.equalsIgnoreCase(webHook.getUrl(), url) - && type.equals(webHook.getWebhookType())) { - facesMessages.addGlobal(SEVERITY_ERROR, - msgs.get("jsf.project.DuplicateUrl")); - return false; - } - } return true; } @@ -1203,6 +1232,9 @@ public static class ProjectMaintainersAutocomplete extends MaintainerAutocomplet @Inject private ProjectHome projectHome; + @Inject + private WebhookServiceImpl webhookServiceImpl; + @Inject private Messages msgs; @@ -1246,6 +1278,12 @@ public void onSelectItemAction() { facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.format("jsf.project.MaintainerAdded", maintainer.getName())); + + webhookServiceImpl.processWebhookMaintainerChanged( + getInstance().getSlug(), + maintainer.getAccount().getUsername(), Maintainer, + getInstance().getWebHooks(), + ProjectMaintainerChangedEvent.ChangeType.ADD); } } diff --git a/zanata-war/src/main/java/org/zanata/action/ProjectPermissionDialog.java b/zanata-war/src/main/java/org/zanata/action/ProjectPermissionDialog.java index ad093d0004..4397bede40 100644 --- a/zanata-war/src/main/java/org/zanata/action/ProjectPermissionDialog.java +++ b/zanata-war/src/main/java/org/zanata/action/ProjectPermissionDialog.java @@ -21,6 +21,8 @@ package org.zanata.action; +import static org.zanata.webhook.events.ProjectMaintainerChangedEvent.ChangeType; + import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; @@ -49,9 +51,13 @@ import org.zanata.model.LocaleRole; import org.zanata.model.PersonProjectMemberships; import org.zanata.model.ProjectRole; +import org.zanata.model.WebHook; import org.zanata.security.ZanataIdentity; import org.zanata.service.LocaleService; +import org.zanata.service.ProjectService; import org.zanata.service.impl.LocaleServiceImpl; +import org.zanata.service.impl.ProjectServiceImpl; +import org.zanata.service.impl.WebhookServiceImpl; import org.zanata.ui.AbstractAutocomplete; import org.zanata.ui.faces.FacesMessages; import org.zanata.util.ServiceLocator; @@ -60,8 +66,6 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.util.Map; - /* * Backing bean for project permissions dialog. @@ -74,7 +78,7 @@ @Transactional @Slf4j public class ProjectPermissionDialog extends AbstractAutocomplete - implements Serializable { + implements Serializable { @Inject private FacesMessages facesMessages; @@ -88,22 +92,24 @@ public class ProjectPermissionDialog extends AbstractAutocomplete @Inject private LocaleService localeServiceImpl; + @Inject + private WebhookServiceImpl webhookServiceImpl; + @Inject private PersonDAO personDAO; @Inject private ProjectDAO projectDAO; + @Inject + private ProjectService projectServiceImpl; + @Getter private PersonProjectMemberships data; @Getter private HProject project; - private ListMultimap personRoles; - - private Map> personLocaleRoles; - public void setData(HProject project, HPerson person) { this.project = project; setPerson(person); @@ -125,8 +131,10 @@ private void setPerson(HPerson person) { } } - final ListMultimap localeRoles = ArrayListMultimap.create(); - for (HProjectLocaleMember membership : getProject().getLocaleMembers()) { + final ListMultimap localeRoles = + ArrayListMultimap.create(); + for (HProjectLocaleMember membership : getProject() + .getLocaleMembers()) { if (membership.getPerson().equals(person)) { localeRoles.put(membership.getLocale(), membership.getRole()); } @@ -135,9 +143,10 @@ private void setPerson(HPerson person) { data = new PersonProjectMemberships(person, projectRoles, localeRoles); LocaleService localeServiceImpl = ServiceLocator.instance().getInstance( - LocaleServiceImpl.class); + LocaleServiceImpl.class); List locales = - localeServiceImpl.getSupportedLanguageByProject(getProject().getSlug()); + localeServiceImpl + .getSupportedLanguageByProject(getProject().getSlug()); data.ensureLocalesPresent(locales); } @@ -158,7 +167,7 @@ public boolean lastMaintainerSelected() { project = projectDAO.findById(getProject().getId()); return project.getMaintainers().size() <= 1 - && project.getMaintainers().contains(data.getPerson()); + && project.getMaintainers().contains(data.getPerson()); } /** @@ -175,13 +184,15 @@ public boolean anySelected() { * Update role membership for a project-specific role based on the current * checkbox value. * - * @param role the role to assign or remove for the current person + * @param role the role to assign or remove for the current person * @param checked the current checkbox value */ public void bindProjectRole(String role, boolean checked) { - if(StringUtils.equalsIgnoreCase(role, ProjectRole.TranslationMaintainer.name())) { + if (StringUtils + .equalsIgnoreCase(role, ProjectRole.TranslationMaintainer.name())) { data.setTranslationMaintainer(checked); - } else if(StringUtils.equalsIgnoreCase(role, ProjectRole.Maintainer.name())) { + } else if (StringUtils + .equalsIgnoreCase(role, ProjectRole.Maintainer.name())) { data.setMaintainer(checked); } } @@ -192,31 +203,37 @@ public void bindProjectRole(String role, boolean checked) { * * @param localeRole represents both the locale and the role * format: {localeId}:{role}. e.g en-US:Reviewer - * @param checked the current checkbox value + * @param checked the current checkbox value */ public void bindTranslationRole(String localeRole, boolean checked) { String[] localeRoleList = StringUtils.split(localeRole, ':'); - final HLocale hLocale = localeServiceImpl.getByLocaleId(localeRoleList[0]); + final HLocale hLocale = + localeServiceImpl.getByLocaleId(localeRoleList[0]); String role = localeRoleList[1]; final Optional - matchingLocaleRoles = Iterables - .tryFind(data.getLocaleRoles(), localeEqualsPredicate(hLocale)); + matchingLocaleRoles = Iterables + .tryFind(data.getLocaleRoles(), localeEqualsPredicate(hLocale)); if (matchingLocaleRoles.isPresent()) { - PersonProjectMemberships.LocaleRoles localeRoles = matchingLocaleRoles.get(); - if (StringUtils.equalsIgnoreCase(role, LocaleRole.Translator.name())) { + PersonProjectMemberships.LocaleRoles localeRoles = + matchingLocaleRoles.get(); + if (StringUtils + .equalsIgnoreCase(role, LocaleRole.Translator.name())) { localeRoles.setTranslator(checked); - } else if (StringUtils.equalsIgnoreCase(role, LocaleRole.Reviewer.name())) { + } else if (StringUtils + .equalsIgnoreCase(role, LocaleRole.Reviewer.name())) { localeRoles.setReviewer(checked); - } else if (StringUtils.equalsIgnoreCase(role, LocaleRole.Coordinator.name())) { + } else if (StringUtils + .equalsIgnoreCase(role, LocaleRole.Coordinator.name())) { localeRoles.setCoordinator(checked); } else if (StringUtils.equalsIgnoreCase(role, LocaleRole.Glossarist.name())) { localeRoles.setGlossarist(checked); } } else { // No LocaleRoles for the given locale, so create a new one. - List roleList = Lists.newArrayList(LocaleRole.valueOf(role)); + List roleList = + Lists.newArrayList(LocaleRole.valueOf(role)); data.addLocaleRoles(hLocale, roleList); } } @@ -225,7 +242,7 @@ public void bindTranslationRole(String localeRole, boolean checked) { * Get a predicate that checks if a LocaleRoles.getLocale() is the given locale. */ private Predicate localeEqualsPredicate( - final HLocale hLocale) { + final HLocale hLocale) { return new Predicate() { @Override public boolean apply(PersonProjectMemberships.LocaleRoles input) { @@ -250,40 +267,60 @@ public void saveSelections() { // so they are all attached before any persistence is attempted. HPerson person = personDAO.findById(data.getPerson().getId()); data.setPerson(person); - for (PersonProjectMemberships.LocaleRoles roles : data.getLocaleRoles()) { + for (PersonProjectMemberships.LocaleRoles roles : data + .getLocaleRoles()) { roles.setLocale(localeServiceImpl - .getByLocaleId(roles.getLocale().getLocaleId())); + .getByLocaleId(roles.getLocale().getLocaleId())); } - final boolean canManageMembers = identity.hasPermission(project, "manage-members"); - final boolean canManageTransMembers = identity.hasPermission(project, "manage-translation-members"); - final boolean canChangeAnyMembers = canManageMembers || canManageTransMembers; + final boolean canManageMembers = + identity.hasPermission(project, "manage-members"); + final boolean canManageTransMembers = + identity.hasPermission(project, "manage-translation-members"); + final boolean canChangeAnyMembers = + canManageMembers || canManageTransMembers; + List updatedRoles = null; if (canManageMembers) { // generate a warning if trying to remove last maintainer // business rule in project will prevent actual removal if (!data.isMaintainer() - && project.getMaintainers().size() <= 1 - && project.getMaintainers().contains(data.getPerson())) { + && project.getMaintainers().size() <= 1 + && project.getMaintainers().contains(data.getPerson())) { facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, - msgs.get("jsf.project.NeedAtLeastOneMaintainer")); + msgs.get("jsf.project.NeedAtLeastOneMaintainer")); } - - project.updateProjectPermissions(data); + updatedRoles = + projectServiceImpl.updateProjectPermissions(project, data); } if (canManageTransMembers) { - project.updateLocalePermissions(data); + projectServiceImpl.updateLocalePermissions(project, data); } if (canChangeAnyMembers) { projectDAO.makePersistent(project); + if (!updatedRoles.isEmpty()) { + List webHooks = project.getWebHooks(); + for (ProjectServiceImpl.UpdatedRole updatedRole : updatedRoles) { + ChangeType changeType = updatedRole.isAdded() + ? ChangeType.ADD : ChangeType.REMOVE; + + webhookServiceImpl + .processWebhookMaintainerChanged(project.getSlug(), + updatedRole.getUsername(), + updatedRole.getRole(), + webHooks, changeType); + } + } } else { - throw new AuthorizationException("You are not authorized to manage permissions for this project."); + throw new AuthorizationException( + "You are not authorized to manage permissions for this project."); } } - @Override public List suggest() { + @Override + public List suggest() { return getPersonDAO().findAllContainingName(getQuery()); } @@ -291,7 +328,8 @@ private PersonDAO getPersonDAO() { return ServiceLocator.instance().getInstance(PersonDAO.class); } - @Override public void onSelectItemAction() { + @Override + public void onSelectItemAction() { String selected = getSelectedItem(); HPerson selectedPerson = getPersonDAO().findByUsername(selected); 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..445b261bd3 100644 --- a/zanata-war/src/main/java/org/zanata/action/VersionHome.java +++ b/zanata-war/src/main/java/org/zanata/action/VersionHome.java @@ -68,9 +68,11 @@ import org.zanata.service.SlugEntityService; import org.zanata.service.ValidationService; import org.zanata.service.impl.LocaleServiceImpl; +import org.zanata.service.impl.WebhookServiceImpl; 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; @@ -146,6 +148,9 @@ public class VersionHome extends SlugHome implements @Inject private ZanataIdentity identity; + @Inject + private WebhookServiceImpl webhookServiceImpl; + private Map availableValidations = Maps .newHashMap(); @@ -429,10 +434,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 +456,11 @@ public String persist() { getInstance().getCustomizedValidations().putAll( project.getCustomizedValidations()); - return super.persist(); + String result = super.persist(); + webhookServiceImpl.processWebhookVersionChanged(getProjectSlug(), slug, + getProject().getWebHooks(), + VersionChangedEvent.ChangeType.CREATE); + return result; } @Override @@ -559,7 +569,12 @@ public void updateStatus(char initial) { @Transactional public void deleteSelf() { + String slug = getInstance().getSlug(); updateStatus('O'); + + webhookServiceImpl.processWebhookVersionChanged(getProjectSlug(), slug, + getProject().getWebHooks(), + VersionChangedEvent.ChangeType.DELETE); } @Transactional diff --git a/zanata-war/src/main/java/org/zanata/events/WebhookEvent.java b/zanata-war/src/main/java/org/zanata/events/WebhookEvent.java new file mode 100644 index 0000000000..cfd0590f64 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/events/WebhookEvent.java @@ -0,0 +1,29 @@ +package org.zanata.events; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Event for publish webhook after transaction. + * See {@link org.zanata.service.impl.WebhookServiceImpl#onPublishWebhook} + * + * @author Alex Eng aeng@redhat.com + */ +@AllArgsConstructor +public final class WebhookEvent { + + @Getter + @Nonnull + private final String url; + + @Getter + @Nullable + private final String secret; + + @Getter + @Nonnull + private final WebhookEventType type; +} diff --git a/zanata-war/src/main/java/org/zanata/rest/service/ProjectService.java b/zanata-war/src/main/java/org/zanata/rest/service/ProjectService.java index 6427feb72e..015f4546f4 100644 --- a/zanata-war/src/main/java/org/zanata/rest/service/ProjectService.java +++ b/zanata-war/src/main/java/org/zanata/rest/service/ProjectService.java @@ -2,6 +2,7 @@ import static org.zanata.common.EntityStatus.OBSOLETE; import static org.zanata.common.EntityStatus.READONLY; +import static org.zanata.model.ProjectRole.Maintainer; import static org.zanata.rest.service.GlossaryService.PROJECT_QUALIFIER_PREFIX; import java.net.URI; @@ -39,7 +40,9 @@ import org.zanata.rest.dto.ProjectIteration; import org.zanata.rest.dto.QualifiedName; import org.zanata.security.ZanataIdentity; +import org.zanata.service.impl.WebhookServiceImpl; import org.zanata.util.GlossaryUtil; +import org.zanata.webhook.events.ProjectMaintainerChangedEvent; import com.google.common.base.Objects; @@ -71,6 +74,9 @@ public class ProjectService implements ProjectResource { @Inject ZanataIdentity identity; + @Inject + WebhookServiceImpl webhookServiceImpl; + @Inject ETagUtils eTagUtils; @@ -184,6 +190,12 @@ public Response put(Project project) { projectDAO.makePersistent(hProject); projectDAO.flush(); etag = eTagUtils.generateTagForProject(projectSlug); + + webhookServiceImpl.processWebhookMaintainerChanged( + hProject.getSlug(), + identity.getCredentials().getUsername(), + Maintainer, hProject.getWebHooks(), + ProjectMaintainerChangedEvent.ChangeType.ADD); return response.tag(etag).build(); } diff --git a/zanata-war/src/main/java/org/zanata/service/ProjectService.java b/zanata-war/src/main/java/org/zanata/service/ProjectService.java new file mode 100644 index 0000000000..2a91fb9b73 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/service/ProjectService.java @@ -0,0 +1,68 @@ +/* + * 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.service; + +import org.apache.deltaspike.jpa.api.transaction.Transactional; +import org.zanata.model.HProject; +import org.zanata.model.PersonProjectMemberships; +import org.zanata.model.type.WebhookType; +import org.zanata.service.impl.ProjectServiceImpl; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * @author Alex Engaeng@redhat.com + */ +public interface ProjectService { + + /** + * Update all security settings for a person. + *

+ * The HPerson and HLocale entities in memberships must be attached to avoid + * persistence problems with Hibernate. + */ + List updateProjectPermissions(HProject project, + PersonProjectMemberships memberships); + + @Transactional + boolean updateWebhook(HProject project, Long webhookId, String url, + String secret, Set types); + + @Transactional + boolean addWebhook(HProject project, String url, String secret, + Set types); + + /** + * Check if project contains duplicate webhook with matching url + */ + boolean isDuplicateWebhookUrl(HProject project, String url); + + /** + * Check if project contains duplicate webhook with matching url other than + * given webhookId + */ + boolean isDuplicateWebhookUrl(HProject project, String url, Long webhookId); + + void updateLocalePermissions(HProject project, PersonProjectMemberships memberships); +} diff --git a/zanata-war/src/main/java/org/zanata/service/impl/DocumentServiceImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/DocumentServiceImpl.java index b0f6360078..209a952079 100644 --- a/zanata-war/src/main/java/org/zanata/service/impl/DocumentServiceImpl.java +++ b/zanata-war/src/main/java/org/zanata/service/impl/DocumentServiceImpl.java @@ -27,11 +27,9 @@ import java.util.concurrent.Future; import java.util.stream.Collectors; -import com.google.common.base.Optional; import com.google.common.collect.Lists; import javax.enterprise.context.RequestScoped; -import javax.enterprise.event.TransactionPhase; import javax.inject.Inject; import javax.inject.Named; @@ -48,7 +46,6 @@ import org.zanata.events.DocumentLocaleKey; import org.zanata.events.DocumentUploadedEvent; import org.zanata.model.type.WebhookType; -import org.zanata.webhook.events.DocumentMilestoneEvent; import org.zanata.i18n.Messages; import org.zanata.lock.Lock; import org.zanata.model.HAccount; @@ -74,6 +71,7 @@ import lombok.extern.slf4j.Slf4j; import javax.enterprise.event.Event; import org.zanata.util.UrlUtil; +import org.zanata.webhook.events.SourceDocumentChangedEvent; import javax.enterprise.event.Observes; @@ -125,6 +123,9 @@ public class DocumentServiceImpl implements DocumentService { @Inject @Authenticated private HAccount authenticatedAccount; + @Inject + private WebhookServiceImpl webhookServiceImpl; + @Inject private Messages msgs; @@ -187,6 +188,7 @@ public HDocument saveDocument(String projectSlug, String iterationSlug, .validateSourceLocale(sourceDoc.getLang()); boolean changed = false; + boolean isCreateOperation = false; int nextDocRev; if (document == null) { // must be a create operation nextDocRev = 1; @@ -198,12 +200,14 @@ public HDocument saveDocument(String projectSlug, String iterationSlug, document.setProjectIteration(hProjectIteration); hProjectIteration.getDocuments().put(docId, document); document = documentDAO.makePersistent(document); + isCreateOperation = true; } else if (document.isObsolete()) { // must also be a create operation nextDocRev = document.getRevision() + 1; changed = true; document.setObsolete(false); // not sure if this is needed hProjectIteration.getDocuments().put(docId, document); + isCreateOperation = true; } else { // must be an update operation nextDocRev = document.getRevision() + 1; } @@ -218,6 +222,13 @@ public HDocument saveDocument(String projectSlug, String iterationSlug, documentUploadedEvent.fire(new DocumentUploadedEvent( actorId, document.getId(), true, hLocale.getLocaleId())); clearStatsCacheForUpdatedDocument(document); + + if (isCreateOperation) { + webhookServiceImpl.processWebhookSourceDocumentChanged( + projectSlug, iterationSlug, document.getDocId(), + hProjectIteration.getProject().getWebHooks(), + SourceDocumentChangedEvent.ChangeType.ADD); + } } if (copyTrans && nextDocRev == 1) { @@ -236,6 +247,13 @@ public void makeObsolete(HDocument document) { documentDAO.makePersistent(document); documentDAO.flush(); clearStatsCacheForUpdatedDocument(document); + + HProjectIteration version = document.getProjectIteration(); + HProject proj = version.getProject(); + webhookServiceImpl.processWebhookSourceDocumentChanged( + proj.getSlug(), version.getSlug(), document.getDocId(), + proj.getWebHooks(), + SourceDocumentChangedEvent.ChangeType.REMOVE); } // TODO [CDI] simulate async event (e.g. this event was fired asyncly in seam) @@ -249,10 +267,10 @@ public void documentStatisticUpdated(@Observes DocStatsEvent event) { } List docMilestoneWebHooks = - project.getWebHooks().stream().filter( - webHook -> webHook.getWebhookType() - .equals(WebhookType.DocumentMilestoneEvent)) - .collect(Collectors.toList()); + project.getWebHooks().stream().filter( + webHook -> webHook.getTypes() + .contains(WebhookType.DocumentMilestoneEvent)) + .collect(Collectors.toList()); if (docMilestoneWebHooks.isEmpty()) { return; @@ -313,20 +331,9 @@ private void processWebHookDocumentMilestoneEvent(DocumentLocaleKey key, versionSlug, localeId, LocaleId.EN_US, document.getDocId()); - DocumentMilestoneEvent milestoneEvent = - new DocumentMilestoneEvent(projectSlug, - versionSlug, document.getDocId(), - localeId, message, editorUrl); - publishDocumentMilestoneEvent(docMilestoneWebHooks, - milestoneEvent); - } - } - - public void publishDocumentMilestoneEvent(List webHooks, - DocumentMilestoneEvent event) { - for (WebHook webHook : webHooks) { - WebHooksPublisher.publish(webHook.getUrl(), event, - Optional.fromNullable(webHook.getSecret())); + webhookServiceImpl.processDocumentMilestone(projectSlug, + versionSlug, document.getDocId(), localeId, message, + editorUrl, docMilestoneWebHooks); } } @@ -379,12 +386,14 @@ private void clearStatsCacheForUpdatedDocument(HDocument document) { public void init(ProjectIterationDAO projectIterationDAO, DocumentDAO documentDAO, TranslationStateCache translationStateCacheImpl, UrlUtil urlUtil, - ApplicationConfiguration applicationConfiguration, Messages msgs) { + ApplicationConfiguration applicationConfiguration, Messages msgs, + WebhookServiceImpl webhookService) { this.projectIterationDAO = projectIterationDAO; this.documentDAO = documentDAO; this.translationStateCacheImpl = translationStateCacheImpl; this.urlUtil = urlUtil; this.applicationConfiguration = applicationConfiguration; this.msgs = msgs; + this.webhookServiceImpl = webhookService; } } diff --git a/zanata-war/src/main/java/org/zanata/service/impl/ProjectServiceImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/ProjectServiceImpl.java new file mode 100644 index 0000000000..02224ca81f --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/service/impl/ProjectServiceImpl.java @@ -0,0 +1,249 @@ +/* + * 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.service.impl; + +import com.google.common.collect.Lists; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang.StringUtils; +import org.apache.deltaspike.jpa.api.transaction.Transactional; +import org.zanata.dao.ProjectDAO; +import org.zanata.dao.WebHookDAO; +import org.zanata.model.HLocale; +import org.zanata.model.HPerson; +import org.zanata.model.HProject; +import org.zanata.model.HProjectLocaleMember; +import org.zanata.model.HProjectMember; +import org.zanata.model.LocaleRole; +import org.zanata.model.PersonProjectMemberships; +import org.zanata.model.ProjectRole; +import org.zanata.model.WebHook; +import org.zanata.model.type.WebhookType; +import org.zanata.service.ProjectService; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static javax.faces.application.FacesMessage.SEVERITY_ERROR; +import static org.zanata.model.LocaleRole.Coordinator; +import static org.zanata.model.LocaleRole.Reviewer; +import static org.zanata.model.LocaleRole.Translator; +import static org.zanata.model.ProjectRole.Maintainer; +import static org.zanata.model.ProjectRole.TranslationMaintainer; + +/** + * @author Alex Engaeng@redhat.com + */ +@Named("projectServiceImpl") +@RequestScoped +@Slf4j +public class ProjectServiceImpl implements ProjectService { + + @Inject + private ProjectDAO projectDAO; + + @Inject + private WebHookDAO webHookDAO; + + @Override + public List updateProjectPermissions(HProject project, + PersonProjectMemberships memberships) { + HPerson person = memberships.getPerson(); + + boolean wasMaintainer = + project.getMaintainers().contains(memberships.getPerson()); + boolean isLastMaintainer = + wasMaintainer && project.getMaintainers().size() <= 1; + // business rule: every project must have at least one maintainer + boolean isMaintainer = isLastMaintainer || memberships.isMaintainer(); + + List updatedRoles = Lists.newArrayList(); + + Optional + updatedMaintainerRole = ensureMembership(project, isMaintainer, + asMember(project, person, Maintainer)); + + if (updatedMaintainerRole.isPresent()) { + updatedRoles.add(updatedMaintainerRole.get()); + } + + // business rule: if someone is a Maintainer, they must also be a TranslationMaintainer + boolean isTranslationMaintainer = memberships.isMaintainer() || + memberships.isTranslationMaintainer(); + + Optional updatedTranslationMaintainer = + ensureMembership(project, isTranslationMaintainer, + asMember(project, person, TranslationMaintainer)); + + if (updatedTranslationMaintainer.isPresent()) { + updatedRoles.add(updatedTranslationMaintainer.get()); + } + return updatedRoles; + } + + @Transactional + @Override + public boolean updateWebhook(HProject project, Long webhookId, String url, + String secret, Set types) { + if (types.isEmpty()) { + return false; + } + if (project == null) { + return false; + } + WebHook webHook = webHookDAO.findById(webhookId); + if (webHook == null) { + return false; + } + secret = StringUtils.isBlank(secret) ? null : secret; + webHook.update(url, types, secret); + webHookDAO.makePersistent(webHook); + return true; + } + + @Transactional + @Override + public boolean addWebhook(HProject project, String url, String secret, + Set types) { + if (types.isEmpty()) { + return false; + } + if (project == null) { + return false; + } + secret = StringUtils.isBlank(secret) ? null : secret; + WebHook webHook = + new WebHook(project, url, types, secret); + project.getWebHooks().add(webHook); + projectDAO.makePersistent(project); + return true; + } + + @Override + public boolean isDuplicateWebhookUrl(HProject project, String url) { + for (WebHook webHook : project.getWebHooks()) { + if (StringUtils.equalsIgnoreCase(webHook.getUrl(), url)) { + return true; + } + } + return false; + } + + @Override + public boolean isDuplicateWebhookUrl(HProject project, String url, + Long webhookId) { + for (WebHook webHook : project.getWebHooks()) { + if (!webhookId.equals(webHook.getId()) + && StringUtils.equalsIgnoreCase(webHook.getUrl(), url)) { + return true; + } + } + return false; + } + + @Override + public void updateLocalePermissions(HProject project, + PersonProjectMemberships memberships) { + HPerson person = memberships.getPerson(); + + for (PersonProjectMemberships.LocaleRoles localeRoles + : memberships.getLocaleRoles()) { + HLocale locale = localeRoles.getLocale(); + ensureMembership(project, localeRoles.isTranslator(), + asMember(project, locale, person, Translator)); + ensureMembership(project, localeRoles.isReviewer(), + asMember(project, locale, person, Reviewer)); + ensureMembership(project, localeRoles.isCoordinator(), + asMember(project, locale, person, Coordinator)); + } + } + + @Getter + @AllArgsConstructor + public class UpdatedRole { + private String username; + private ProjectRole role; + private boolean added; + } + + /** + * Get a person as a member object in this project for a role. + */ + private HProjectMember asMember(HProject project, HPerson person, + ProjectRole role) { + return new HProjectMember(project, person, role); + } + + /** + * Get a person as a member object in this project for a locale-specific role. + */ + private HProjectLocaleMember asMember(HProject project, HLocale locale, + HPerson person, LocaleRole role) { + return new HProjectLocaleMember(project, locale, person, role); + } + + /** + * Ensure the given membership is present or absent. + */ + private Optional ensureMembership(HProject project, boolean shouldBePresent, + HProjectMember membership) { + UpdatedRole updatedRole = null; + final Set members = project.getMembers(); + final boolean isPresent = members.contains(membership); + if (isPresent != shouldBePresent) { + if (shouldBePresent) { + members.add(membership); + updatedRole = new UpdatedRole( + membership.getPerson().getAccount().getUsername(), + membership.getRole(), true); + } else { + members.remove(membership); + updatedRole = new UpdatedRole( + membership.getPerson().getAccount().getUsername(), + membership.getRole(), false); + } + } + return Optional.ofNullable(updatedRole); + } + + /** + * Ensure the given locale membership is present or absent. + */ + private void ensureMembership(HProject project, boolean shouldBePresent, + HProjectLocaleMember membership) { + final Set members = project.getLocaleMembers(); + final boolean isPresent = members.contains(membership); + if (isPresent != shouldBePresent) { + if (shouldBePresent) { + members.add(membership); + } else { + members.remove(membership); + } + } + } +} diff --git a/zanata-war/src/main/java/org/zanata/service/impl/RegisterServiceImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/RegisterServiceImpl.java index c8bbd80c7d..7595afbe6c 100644 --- a/zanata-war/src/main/java/org/zanata/service/impl/RegisterServiceImpl.java +++ b/zanata-war/src/main/java/org/zanata/service/impl/RegisterServiceImpl.java @@ -49,6 +49,9 @@ import org.zanata.seam.security.ZanataJpaIdentityStore; import org.zanata.service.RegisterService; import org.zanata.util.HashUtil; +import org.zanata.webhook.events.ProjectMaintainerChangedEvent; + +import static org.zanata.model.ProjectRole.Maintainer; @Named("registerServiceImpl") @RequestScoped @@ -66,6 +69,9 @@ public class RegisterServiceImpl implements RegisterService { @Inject PersonDAO personDAO; + @Inject + WebhookServiceImpl webhookServiceImpl; + @Inject AccountRoleDAO accountRoleDAO; @@ -232,7 +238,16 @@ public void execute() { obsoletePerson.getMaintainerProjects()); for (HProject proj : maintainedProjects) { proj.addMaintainer(activePerson); + webhookServiceImpl.processWebhookMaintainerChanged(proj.getSlug(), + activePerson.getAccount().getUsername(), Maintainer, + proj.getWebHooks(), + ProjectMaintainerChangedEvent.ChangeType.ADD); + proj.removeMaintainer(obsoletePerson); + webhookServiceImpl.processWebhookMaintainerChanged(proj.getSlug(), + obsoletePerson.getAccount().getUsername(), Maintainer, + proj.getWebHooks(), + ProjectMaintainerChangedEvent.ChangeType.REMOVE); } // Merge all maintained Version Groups diff --git a/zanata-war/src/main/java/org/zanata/service/impl/TranslationUpdatedManager.java b/zanata-war/src/main/java/org/zanata/service/impl/TranslationUpdatedManager.java index fcac96e3dc..2416f9d723 100644 --- a/zanata-war/src/main/java/org/zanata/service/impl/TranslationUpdatedManager.java +++ b/zanata-war/src/main/java/org/zanata/service/impl/TranslationUpdatedManager.java @@ -12,7 +12,6 @@ import org.zanata.dao.TextFlowTargetDAO; import org.zanata.events.DocStatsEvent; import org.zanata.model.type.WebhookType; -import org.zanata.webhook.events.DocumentStatsEvent; import org.zanata.model.HDocument; import org.zanata.model.HPerson; import org.zanata.model.HProject; @@ -20,7 +19,6 @@ import org.zanata.model.WebHook; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; import lombok.extern.slf4j.Slf4j; import javax.enterprise.event.Observes; @@ -47,6 +45,9 @@ public class TranslationUpdatedManager { @Inject private DocumentDAO documentDAO; + @Inject + private WebhookServiceImpl webhookServiceImpl; + @Async public void docStatsUpdated( @Observes(during = TransactionPhase.AFTER_SUCCESS) @@ -69,10 +70,10 @@ protected void processWebHookEvent(DocStatsEvent event) { } List docStatsWebHooks = - project.getWebHooks().stream().filter( - webHook -> webHook.getWebhookType() - .equals(WebhookType.DocumentStatsEvent)) - .collect(Collectors.toList()); + project.getWebHooks().stream().filter( + webHook -> webHook.getTypes() + .contains(WebhookType.DocumentStatsEvent)) + .collect(Collectors.toList()); if (docStatsWebHooks.isEmpty()) { return; @@ -83,27 +84,18 @@ protected void processWebHookEvent(DocStatsEvent event) { String projectSlug = project.getSlug(); LocaleId localeId = event.getKey().getLocaleId(); - DocumentStatsEvent webhookEvent = - new DocumentStatsEvent(person.getAccount().getUsername(), - projectSlug, versionSlug, docId, localeId, - event.getWordDeltasByState()); - - publishWebhookEvent(docStatsWebHooks, webhookEvent); - } - - @VisibleForTesting - public void publishWebhookEvent(List webHooks, - DocumentStatsEvent event) { - for (WebHook webHook : webHooks) { - WebHooksPublisher.publish(webHook.getUrl(), event, - Optional.fromNullable(webHook.getSecret())); - } + webhookServiceImpl.processDocumentStats( + person.getAccount().getUsername(), + projectSlug, versionSlug, docId, localeId, + event.getWordDeltasByState(), docStatsWebHooks); } @VisibleForTesting - public void init(DocumentDAO documentDAO, - TextFlowTargetDAO textFlowTargetDAO) { + protected void init(DocumentDAO documentDAO, + TextFlowTargetDAO textFlowTargetDAO, + WebhookServiceImpl webhookService) { this.documentDAO = documentDAO; this.textFlowTargetDAO = textFlowTargetDAO; + this.webhookServiceImpl = webhookService; } } diff --git a/zanata-war/src/main/java/org/zanata/service/impl/WebHooksPublisher.java b/zanata-war/src/main/java/org/zanata/service/impl/WebHooksPublisher.java index f10f055b93..b969f83d1b 100644 --- a/zanata-war/src/main/java/org/zanata/service/impl/WebHooksPublisher.java +++ b/zanata-war/src/main/java/org/zanata/service/impl/WebHooksPublisher.java @@ -21,6 +21,7 @@ package org.zanata.service.impl; +import java.util.Optional; import javax.annotation.Nonnull; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; @@ -32,7 +33,6 @@ import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.zanata.events.WebhookEventType; -import com.google.common.base.Optional; import lombok.extern.slf4j.Slf4j; import org.zanata.util.HmacUtil; diff --git a/zanata-war/src/main/java/org/zanata/service/impl/WebhookServiceImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/WebhookServiceImpl.java new file mode 100644 index 0000000000..e01195d4d8 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/service/impl/WebhookServiceImpl.java @@ -0,0 +1,241 @@ +package org.zanata.service.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.event.TransactionPhase; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.lang3.StringUtils; +import org.ocpsoft.common.util.Strings; +import org.zanata.async.Async; +import org.zanata.common.ContentState; +import org.zanata.common.LocaleId; +import org.zanata.events.WebhookEvent; +import org.zanata.events.WebhookEventType; +import org.zanata.i18n.Messages; +import org.zanata.model.ProjectRole; +import org.zanata.model.WebHook; +import org.zanata.model.type.WebhookType; +import org.zanata.util.UrlUtil; +import org.zanata.webhook.events.DocumentMilestoneEvent; +import org.zanata.webhook.events.DocumentStatsEvent; +import org.zanata.webhook.events.ProjectMaintainerChangedEvent; +import org.zanata.webhook.events.SourceDocumentChangedEvent; +import org.zanata.webhook.events.TestEvent; +import org.zanata.webhook.events.VersionChangedEvent; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * @author Alex Eng aeng@redhat.com + */ +@Slf4j +@Named("webhookServiceImpl") +@RequestScoped +public class WebhookServiceImpl implements Serializable { + + @Inject + private Messages msgs; + + @Inject + private Event webhookEventEvent; + + private static final int URL_MAX_LENGTH = 255; + + /** + * Need @Async annotation for TransactionPhase.AFTER_SUCCESS event + */ + @Async + public void onPublishWebhook(@Observes( + during = TransactionPhase.AFTER_SUCCESS) WebhookEvent event) { + WebHooksPublisher + .publish(event.getUrl(), event.getType(), + Optional.ofNullable(event.getSecret())); + } + + /** + * Process TestEvent + */ + public void processTestEvent(String username, String projectSlug, + String url, String secret) { + TestEvent event = new TestEvent(username, projectSlug); + webhookEventEvent.fire(new WebhookEvent(url, secret, event)); + } + + /** + * Process VersionChangedEvent + */ + public void processWebhookVersionChanged(String projectSlug, String versionSlug, + List webHooks, VersionChangedEvent.ChangeType changeType) { + List versionWebhooks = + filterWebhookByType(webHooks, WebhookType.VersionChangedEvent); + + if (versionWebhooks.isEmpty()) { + return; + } + VersionChangedEvent event = + new VersionChangedEvent(projectSlug, versionSlug, changeType); + publishWebhooks(versionWebhooks, event); + } + + /** + * Process ProjectMaintainerChangedEvent + */ + public void processWebhookMaintainerChanged(String projectSlug, + String username, ProjectRole role, List webHooks, + ProjectMaintainerChangedEvent.ChangeType changeType) { + + List maintainerWebhooks = + filterWebhookByType(webHooks, + WebhookType.ProjectMaintainerChangedEvent); + + if (maintainerWebhooks.isEmpty()) { + return; + } + ProjectMaintainerChangedEvent event = + new ProjectMaintainerChangedEvent(projectSlug, username, + role, changeType); + publishWebhooks(maintainerWebhooks, event); + } + + /** + * Process SourceDocumentChangedEvent + */ + public void processWebhookSourceDocumentChanged(String project, + String version, String docId, List webHooks, + SourceDocumentChangedEvent.ChangeType changeType) { + List eventWebhooks = filterWebhookByType(webHooks, + WebhookType.SourceDocumentChangedEvent); + + if (eventWebhooks.isEmpty()) { + return; + } + SourceDocumentChangedEvent event = new SourceDocumentChangedEvent( + project, version, docId, changeType); + + publishWebhooks(eventWebhooks, event); + } + + /** + * Process DocumentMilestoneEvent + */ + public void processDocumentMilestone(String projectSlug, + String versionSlug, String docId, LocaleId localeId, String message, + String editorUrl, List webHooks) { + DocumentMilestoneEvent milestoneEvent = + new DocumentMilestoneEvent(projectSlug, versionSlug, docId, + localeId, message, editorUrl); + publishWebhooks(webHooks, milestoneEvent); + } + + /** + * Process DocumentStatsEvent + */ + public void processDocumentStats(String username, String projectSlug, + String versionSlug, String docId, LocaleId localeId, + Map wordDeltasByState, List webHooks) { + DocumentStatsEvent statsEvent = + new DocumentStatsEvent(username, projectSlug, versionSlug, + docId, localeId, wordDeltasByState); + publishWebhooks(webHooks, statsEvent); + } + + 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); + } + + public List getDisplayNames(Set types) { + return types.stream().map(WebhookType::getDisplayName) + .collect(Collectors.toList()); + } + + public String getTypesAsString(WebHook webHook) { + if (webHook == null) { + return ""; + } + List results = webHook.getTypes().stream().map(Enum::name) + .collect(Collectors.toList()); + return Strings.join(results, ","); + } + + public boolean isValidUrl(String url) { + return UrlUtil.isValidUrl(url) + && StringUtils.length(url) <= URL_MAX_LENGTH; + } + + public static Set getTypesFromString(String strTypes) { + return new HashSet( + Lists.transform(Lists.newArrayList(strTypes.split(",")), + convertToWebHookType)); + } + + private static Function convertToWebHookType = + new Function() { + @Override + public WebhookType apply(String input) { + return WebhookType.valueOf(input); + } + }; + + /** + * Object for all available webhook list + */ + @Getter + public final static class WebhookTypeItem { + private WebhookType type; + private String description; + + public WebhookTypeItem(WebhookType webhookType, String desc) { + this.type = webhookType; + this.description = desc; + } + } + + private void publishWebhooks(List webHooks, + WebhookEventType event) { + for (WebHook webhook : webHooks) { + webhookEventEvent.fire(new WebhookEvent(webhook.getUrl(), + webhook.getSecret(), event)); + } + } + + private List filterWebhookByType(List webHooks, + WebhookType webhookType) { + return webHooks.stream().filter(webHook -> webHook.getTypes() + .contains(webhookType)).collect(Collectors.toList()); + } +} diff --git a/zanata-war/src/main/java/org/zanata/webhook/events/ProjectMaintainerChangedEvent.java b/zanata-war/src/main/java/org/zanata/webhook/events/ProjectMaintainerChangedEvent.java new file mode 100644 index 0000000000..aace6f88c9 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/webhook/events/ProjectMaintainerChangedEvent.java @@ -0,0 +1,58 @@ +package org.zanata.webhook.events; + +import java.util.Date; + +import org.codehaus.jackson.annotate.JsonPropertyOrder; +import org.zanata.events.WebhookEventType; +import org.zanata.model.ProjectRole; +import org.zanata.model.type.WebhookType; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * Event for when a project maintainer is added or removed + * @author Alex Eng aeng@redhat.com + */ +@Getter +@Setter +@JsonPropertyOrder({"project", "username", "changeType", "role"}) +@AllArgsConstructor +@EqualsAndHashCode +public class ProjectMaintainerChangedEvent extends WebhookEventType { + private static final String EVENT_TYPE = + WebhookType.ProjectMaintainerChangedEvent.name(); + + public static enum ChangeType { + ADD, + REMOVE + } + + /** + * Target project slug. + * {@link org.zanata.model.HProject#slug} + */ + private final String project; + + /** + * Username of the maintainer + */ + private final String username; + + /** + * Changed role / + */ + private final ProjectRole role; + + /** + * Change type + */ + private final ChangeType changeType; + + @Override + public String getType() { + return EVENT_TYPE; + } +} diff --git a/zanata-war/src/main/java/org/zanata/webhook/events/SourceDocumentChangedEvent.java b/zanata-war/src/main/java/org/zanata/webhook/events/SourceDocumentChangedEvent.java new file mode 100644 index 0000000000..95f246b7cc --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/webhook/events/SourceDocumentChangedEvent.java @@ -0,0 +1,59 @@ +package org.zanata.webhook.events; + +import java.util.Date; + +import org.codehaus.jackson.annotate.JsonPropertyOrder; +import org.zanata.events.WebhookEventType; +import org.zanata.model.ProjectRole; +import org.zanata.model.type.WebhookType; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * Event for when a source document is added or removed + * @author Alex Eng aeng@redhat.com + */ +@Getter +@Setter +@JsonPropertyOrder({"project", "version", "docId", "changeType"}) +@AllArgsConstructor +@EqualsAndHashCode +public class SourceDocumentChangedEvent extends WebhookEventType { + private static final String EVENT_TYPE = + WebhookType.SourceDocumentChangedEvent.name(); + + public static enum ChangeType { + ADD, + REMOVE + } + + /** + * Target project slug. + * {@link org.zanata.model.HProject#slug} + */ + private final String project; + + /** + * Target version slug. + * {@link org.zanata.model.HProjectIteration#slug} + */ + private final String version; + + /** + * Document id + */ + private final String docId; + + /** + * Change type + */ + private final ChangeType changeType; + + @Override + public String getType() { + return EVENT_TYPE; + } +} 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..de52f0cd6a --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/webhook/events/VersionChangedEvent.java @@ -0,0 +1,79 @@ +/* + * 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"}) +@EqualsAndHashCode +public class VersionChangedEvent extends WebhookEventType { + + private static final String EVENT_TYPE = + WebhookType.VersionChangedEvent.name(); + + public static enum ChangeType { + CREATE, + DELETE + } + + /** + * 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; + + @Override + public String getType() { + return EVENT_TYPE; + } +} diff --git a/zanata-war/src/main/resources/db/changelogs/db.changelog-4.0.xml b/zanata-war/src/main/resources/db/changelogs/db.changelog-4.0.xml index 3018166188..fa589ca050 100644 --- a/zanata-war/src/main/resources/db/changelogs/db.changelog-4.0.xml +++ b/zanata-war/src/main/resources/db/changelogs/db.changelog-4.0.xml @@ -25,22 +25,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd"> - - Add webhookType to WebHook - - - - - - - - - Migrate all WebHook to type DocumentMilestoneEvent - - UPDATE WebHook set webhookType='DocumentMilestoneEvent' - - - Add table AllowedApp. @@ -152,4 +136,59 @@ constraintName="UKHProject_Glossary"/> + + Add table WebHook_WebHookType + + + + + + + + + + + + + + + + + + + + + + Insert DocumentMilestoneEvent to WebHook_WebHookType for existing webhook + + INSERT INTO WebHook_WebHookType (webhookId, type) SELECT id, 'DocumentMilestoneEvent' FROM WebHook + + + + + + + + + + Migrate all WebHookType from WebHook to WebHook_WebHookType, and drop webhookType column from WebHook + + INSERT INTO WebHook_WebHookType (webhookId, type) SELECT id, webhookType FROM WebHook + + + + + + Change url column to text, Add unique constrains of url and project + + + + diff --git a/zanata-war/src/main/resources/messages.properties b/zanata-war/src/main/resources/messages.properties index c0e86c9430..2ae8c57f1e 100644 --- a/zanata-war/src/main/resources/messages.properties +++ b/zanata-war/src/main/resources/messages.properties @@ -319,17 +319,23 @@ 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.webhookType.empty=Please select a type for this webhook. +jsf.project.UpdateWebhook=Webhook {0} updated. 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 is already in the list. {0} 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..f53682ef11 --- /dev/null +++ b/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

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

+ + +
+

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

+ +
+ + + +
    + +
  • + #{webhook.url} + #{webhookServiceImpl.getDisplayNames(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..a662b73347 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 @@ -18,33 +18,6 @@ - - - - - - - - - - - - - - - - - @@ -204,89 +177,7 @@
  • -

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

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

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

    -
    - - -
    -
    - - -
    -
    - - -
    - -
    - - - -
    -
    -
    +
  • diff --git a/zanata-war/src/main/webapp/resources/script/components-script.js b/zanata-war/src/main/webapp/resources/script/components-script.js index 1b57cff962..eaae5496d8 100644 --- a/zanata-war/src/main/webapp/resources/script/components-script.js +++ b/zanata-war/src/main/webapp/resources/script/components-script.js @@ -473,3 +473,37 @@ function onCheckboxValueChanged(checkbox, jsFunction) { var key = jQuery(checkbox).children("input").first().val(); jsFunction(key, isChecked); } + + +/* ---------------------------------------------------------- */ +/*------------------ webhook-form component ------------------*/ +/* ---------------------------------------------------------- */ + +function toggleButtons(form, disable) { + form.find('[name="addWebhookBtn"]').attr('disabled', disable); + form.find('[name="testWebhookBtn"]').attr('disabled', disable); + form.find('[name="updateWebhookBtn"]').attr('disabled', disable); +} + +function onTestWebhook(formId, callback) { + var form = jQuery('#' + formId); + var url = form.find('[name="payloadUrlInput"]').val(); + var secret = form.find('[name="secretInput"]').val(); + callback(url, secret, formId); +} + +function onAddWebhook(formId, callback) { + var form = jQuery('#' + formId); + var url = form.find('[name="payloadUrlInput"]').val(); + var secret = form.find('[name="secretInput"]').val(); + var types = form.find('[name="webhookTypes"]').val(); + callback(url, secret, types, formId); +} + +function onUpdateWebhook(id, formId, callback) { + var form = jQuery('#' + formId); + var url = form.find('[name="payloadUrlInput"]').val(); + var secret = form.find('[name="secretInput"]').val(); + var types = form.find('[name="webhookTypes"]').val(); + callback(id, url, secret, types, formId); +} diff --git a/zanata-war/src/main/webapp/resources/zanata/webbook-form.xhtml b/zanata-war/src/main/webapp/resources/zanata/webbook-form.xhtml new file mode 100644 index 0000000000..22735e3937 --- /dev/null +++ b/zanata-war/src/main/webapp/resources/zanata/webbook-form.xhtml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + +
    +
    + + +
    + +
    + + +
    + + #{webhookType.description} + +
    +
    + +
    + +
    + + + + + + + + + +
    + + +
    +
    + +
    +
    +
    + diff --git a/zanata-war/src/test/java/org/zanata/search/FilterConstraintToQueryJpaTest.java b/zanata-war/src/test/java/org/zanata/search/FilterConstraintToQueryJpaTest.java index 0fc7c2cc8e..77b908374d 100644 --- a/zanata-war/src/test/java/org/zanata/search/FilterConstraintToQueryJpaTest.java +++ b/zanata-war/src/test/java/org/zanata/search/FilterConstraintToQueryJpaTest.java @@ -31,6 +31,7 @@ import com.github.huangp.entityunit.maker.FixedValueMaker; import com.google.common.base.Function; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; /** * @author Patrick Huang types = + Sets.newHashSet(WebhookType.DocumentMilestoneEvent); webHooks.add(new WebHook(project, "http://test.example.com", - WebhookType.DocumentMilestoneEvent, key)); + types, key)); webHooks.add(new WebHook(project, "http://test1.example.com", - WebhookType.DocumentMilestoneEvent, key)); + types, key)); webHooks.add(new WebHook(project, "http://test1.example.com", - WebhookType.DocumentStatsEvent, key)); + Sets.newHashSet(WebhookType.DocumentStatsEvent), key)); when(projectIterationDAO.findById(versionId)).thenReturn(version); when(version.getProject()).thenReturn(project); @@ -141,28 +150,26 @@ public void setup() { @Test public void documentMilestoneEventTranslatedTest() { - doNothing().when(spyService).publishDocumentMilestoneEvent( - any(List.class), any(DocumentMilestoneEvent.class)); WordStatistic stats = new WordStatistic(0, 0, 0, 10, 0); when(translationStateCacheImpl.getDocumentStatistics(docId, localeId)) .thenReturn(stats); - runDocumentStatisticUpdatedTest(spyService, ContentState.New, + runDocumentStatisticUpdatedTest(documentService, ContentState.New, ContentState.Translated); - DocumentMilestoneEvent milestoneEvent = - new DocumentMilestoneEvent(projectSlug, versionSlug, - docIdString, localeId, - msgs.format("jsf.webhook.response.state", milestone, - ContentState.Translated), testUrl); + String message = msgs.format("jsf.webhook.response.state", milestone, + ContentState.Translated); ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); - verify(spyService).publishDocumentMilestoneEvent(captor.capture(), - eq(milestoneEvent)); + + verify(webhookService).processDocumentMilestone( + eq(projectSlug), eq(versionSlug), eq(docIdString), + eq(localeId), eq(message), eq(testUrl), captor.capture()); + assertThat(captor.getValue().size(), is(2)); - assertThat(((WebHook) captor.getValue().get(0)).getWebhookType(), - is(WebhookType.DocumentMilestoneEvent)); - assertThat(((WebHook) captor.getValue().get(1)).getWebhookType(), - is(WebhookType.DocumentMilestoneEvent)); + assertThat(((WebHook) captor.getValue().get(0)).getTypes(), + contains(WebhookType.DocumentMilestoneEvent)); + assertThat(((WebHook) captor.getValue().get(1)).getTypes(), + contains(WebhookType.DocumentMilestoneEvent)); } @Test @@ -170,43 +177,38 @@ public void documentMilestoneEventTranslatedNot100Test() { WordStatistic stats = new WordStatistic(0, 1, 0, 9, 0); when(translationStateCacheImpl.getDocumentStatistics(docId, localeId)) .thenReturn(stats); - runDocumentStatisticUpdatedTest(spyService, ContentState.New, + runDocumentStatisticUpdatedTest(documentService, ContentState.New, ContentState.Translated); - DocumentMilestoneEvent milestoneEvent = - new DocumentMilestoneEvent(projectSlug, versionSlug, - docIdString, localeId, testUrl, - msgs.format("jsf.webhook.response.state", milestone, - ContentState.Translated)); + String message = msgs.format("jsf.webhook.response.state", milestone, + ContentState.Translated); - verify(spyService, never()).publishDocumentMilestoneEvent( - webHooks, milestoneEvent); + verify(webhookService, never()).processDocumentMilestone( + eq(projectSlug), eq(versionSlug), eq(docIdString), + eq(localeId), eq(message), eq(testUrl), Mockito.anyList()); } @Test public void documentMilestoneEventApprovedTest() { - doNothing().when(spyService).publishDocumentMilestoneEvent( - any(List.class), any(DocumentMilestoneEvent.class)); WordStatistic stats = new WordStatistic(10, 0, 0, 0, 0); when(translationStateCacheImpl.getDocumentStatistics(docId, localeId)) .thenReturn(stats); - runDocumentStatisticUpdatedTest(spyService, ContentState.Translated, + runDocumentStatisticUpdatedTest(documentService, ContentState.Translated, ContentState.Approved); - - DocumentMilestoneEvent milestoneEvent = - new DocumentMilestoneEvent(projectSlug, - versionSlug, docIdString, - localeId, msgs.format("jsf.webhook.response.state", - milestone, ContentState.Approved), testUrl); + String message = msgs.format("jsf.webhook.response.state", + milestone, ContentState.Approved); ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); - verify(spyService).publishDocumentMilestoneEvent(captor.capture(), - eq(milestoneEvent)); + + verify(webhookService).processDocumentMilestone( + eq(projectSlug), eq(versionSlug), eq(docIdString), + eq(localeId), eq(message), eq(testUrl), captor.capture()); + assertThat(captor.getValue().size(), is(2)); - assertThat(((WebHook) captor.getValue().get(0)).getWebhookType(), - is(WebhookType.DocumentMilestoneEvent)); - assertThat(((WebHook) captor.getValue().get(1)).getWebhookType(), - is(WebhookType.DocumentMilestoneEvent)); + assertThat(((WebHook) captor.getValue().get(0)).getTypes(), + contains(WebhookType.DocumentMilestoneEvent)); + assertThat(((WebHook) captor.getValue().get(1)).getTypes(), + contains(WebhookType.DocumentMilestoneEvent)); } @Test @@ -214,17 +216,13 @@ public void documentMilestoneEventApprovedNot100Test() { WordStatistic stats = new WordStatistic(9, 0, 0, 1, 0); when(translationStateCacheImpl.getDocumentStatistics(docId, localeId)) .thenReturn(stats); - runDocumentStatisticUpdatedTest(spyService, ContentState.Translated, + runDocumentStatisticUpdatedTest(documentService, ContentState.Translated, ContentState.Approved); - - DocumentMilestoneEvent milestoneEvent = - new DocumentMilestoneEvent(projectSlug, versionSlug, - docIdString, localeId, msgs.format( - "jsf.webhook.response.state", milestone, - ContentState.Approved), testUrl); - - verify(spyService, never()).publishDocumentMilestoneEvent( - webHooks, milestoneEvent); + String message = msgs.format( + "jsf.webhook.response.state", milestone, ContentState.Approved); + verify(webhookService, never()).processDocumentMilestone( + eq(projectSlug), eq(versionSlug), eq(docIdString), + eq(localeId), eq(message), eq(testUrl), Mockito.anyList()); } @Test @@ -233,16 +231,13 @@ public void documentMilestoneEventSameStateTest1() { when(translationStateCacheImpl.getDocumentStatistics(docId, localeId)) .thenReturn(stats); - runDocumentStatisticUpdatedTest(spyService, ContentState.Approved, + runDocumentStatisticUpdatedTest(documentService, ContentState.Approved, ContentState.Approved); - - DocumentMilestoneEvent milestoneEvent = - new DocumentMilestoneEvent(projectSlug, versionSlug, - docIdString, localeId, msgs.format( - "jsf.webhook.response.state", milestone, - ContentState.Approved), testUrl); - verify(spyService, never()).publishDocumentMilestoneEvent( - webHooks, milestoneEvent); + String message = msgs.format( + "jsf.webhook.response.state", milestone, ContentState.Approved); + verify(webhookService, never()).processDocumentMilestone( + eq(projectSlug), eq(versionSlug), eq(docIdString), + eq(localeId), eq(message), eq(testUrl), Mockito.anyList()); } @Test @@ -252,16 +247,14 @@ public void documentMilestoneEventSameStateTest2() { when(translationStateCacheImpl.getDocumentStatistics(docId, localeId)) .thenReturn(stats); - runDocumentStatisticUpdatedTest(spyService, ContentState.Translated, + runDocumentStatisticUpdatedTest(documentService, ContentState.Translated, ContentState.Translated); - DocumentMilestoneEvent milestoneEvent = - new DocumentMilestoneEvent(projectSlug, versionSlug, - docIdString, localeId, msgs.format( - "jsf.webhook.response.state", milestone, - ContentState.Translated), testUrl); - verify(spyService, never()).publishDocumentMilestoneEvent( - webHooks, milestoneEvent); + String message = msgs.format( + "jsf.webhook.response.state", milestone, ContentState.Translated); + verify(webhookService, never()).processDocumentMilestone( + eq(projectSlug), eq(versionSlug), eq(docIdString), + eq(localeId), eq(message), eq(testUrl), Mockito.anyList()); } private void runDocumentStatisticUpdatedTest( diff --git a/zanata-war/src/test/java/org/zanata/service/impl/TranslationUpdatedManagerTest.java b/zanata-war/src/test/java/org/zanata/service/impl/TranslationUpdatedManagerTest.java index 43c7fa5e32..10f4897498 100644 --- a/zanata-war/src/test/java/org/zanata/service/impl/TranslationUpdatedManagerTest.java +++ b/zanata-war/src/test/java/org/zanata/service/impl/TranslationUpdatedManagerTest.java @@ -38,7 +38,6 @@ import org.zanata.events.DocStatsEvent; import org.zanata.events.DocumentLocaleKey; import org.zanata.model.type.WebhookType; -import org.zanata.webhook.events.DocumentStatsEvent; import org.zanata.model.HAccount; import org.zanata.model.HDocument; import org.zanata.model.HPerson; @@ -50,8 +49,10 @@ import org.zanata.util.StatisticsUtil; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.mockito.Matchers.eq; @@ -69,6 +70,9 @@ public class TranslationUpdatedManagerTest { @Mock private TextFlowTargetDAO textFlowTargetDAO; + @Mock + private WebhookServiceImpl webhookService; + TranslationUpdatedManager manager; List webHooks = Lists.newArrayList(); @@ -77,10 +81,8 @@ public class TranslationUpdatedManagerTest { private String strDocId = "doc/test.txt"; private Long docId = 1L; - private Long tfId = 1L; private Long tftId = 1L; private Long versionId = 1L; - private Long personId = 1L; private LocaleId localeId = LocaleId.DE; private String versionSlug = "versionSlug"; private String projectSlug = "projectSlug"; @@ -93,7 +95,7 @@ public class TranslationUpdatedManagerTest { public void setup() { MockitoAnnotations.initMocks(this); manager = new TranslationUpdatedManager(); - manager.init(documentDAO, textFlowTargetDAO); + manager.init(documentDAO, textFlowTargetDAO, webhookService); HProjectIteration version = Mockito.mock(HProjectIteration.class); HProject project = Mockito.mock(HProject.class); @@ -103,10 +105,10 @@ public void setup() { HTextFlowTarget target = Mockito.mock(HTextFlowTarget.class); webHooks = Lists - .newArrayList(new WebHook(project, "http://test.example.com", - WebhookType.DocumentMilestoneEvent, key), - new WebHook(project, "http://test.example.com", - WebhookType.DocumentStatsEvent, key)); + .newArrayList(new WebHook(project, "http://test.example.com", + Sets.newHashSet(WebhookType.DocumentMilestoneEvent), key), + new WebHook(project, "http://test.example.com", + Sets.newHashSet(WebhookType.DocumentStatsEvent), key)); when(person.getAccount()).thenReturn(account); when(account.getUsername()).thenReturn(username); @@ -141,19 +143,14 @@ public void onDocStatUpdateTest() { DocStatsEvent event = new DocStatsEvent(key, versionId, contentStates, tftId); - DocumentStatsEvent webhookEvent = - new DocumentStatsEvent(username, projectSlug, - versionSlug, strDocId, event.getKey().getLocaleId(), - contentStates); - spyManager.docStatsUpdated(event); - verify(spyManager).processWebHookEvent(event); ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); - verify(spyManager).publishWebhookEvent(captor.capture(), - eq(webhookEvent)); + verify(webhookService).processDocumentStats(eq(username), + eq(projectSlug), eq(versionSlug), eq(strDocId), eq(localeId), + eq(contentStates), captor.capture()); assertThat(captor.getValue().size(), is(1)); - assertThat(((WebHook) captor.getValue().get(0)).getWebhookType(), - is(WebhookType.DocumentStatsEvent)); + assertThat(((WebHook) captor.getValue().get(0)).getTypes(), + contains(WebhookType.DocumentStatsEvent)); } }