diff --git a/functional-test/src/main/java/org/zanata/page/projects/ProjectBasePage.java b/functional-test/src/main/java/org/zanata/page/projects/ProjectBasePage.java index 377ac1a3ca..6fee0fe0af 100644 --- a/functional-test/src/main/java/org/zanata/page/projects/ProjectBasePage.java +++ b/functional-test/src/main/java/org/zanata/page/projects/ProjectBasePage.java @@ -36,6 +36,7 @@ import com.google.common.base.Predicate; import lombok.extern.slf4j.Slf4j; +import org.zanata.page.projects.projectsettings.ProjectWebHooksTab; @Slf4j public class ProjectBasePage extends BasePage { @@ -67,6 +68,8 @@ public class ProjectBasePage extends BasePage { @FindBy(id = "settings-about_tab") private WebElement settingsAboutTab; + private By settingsWebHooksTab = By.id("settings-webhooks_tab"); + public ProjectBasePage(final WebDriver driver) { super(driver); } @@ -197,6 +200,13 @@ public boolean apply(WebDriver input) { return new ProjectLanguagesTab(getDriver()); } + public ProjectWebHooksTab gotoSettingsWebHooksTab() { + log.info("Click WebHooks settings sub-tab"); + clickWhenTabEnabled(waitForWebElement(settingsWebHooksTab)); + waitForWebElement(By.id("settings-webhooks")); + return new ProjectWebHooksTab(getDriver()); + } + public ProjectAboutTab gotoSettingsAboutTab() { log.info("Click About settings sub-tab"); clickWhenTabEnabled(settingsAboutTab); 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 new file mode 100644 index 0000000000..5385e02233 --- /dev/null +++ b/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectWebHooksTab.java @@ -0,0 +1,95 @@ +/* + * Copyright 2014, 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.page.projects.projectsettings; + +import com.google.common.base.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.zanata.page.projects.ProjectBasePage; +import org.zanata.util.WebElementUtil; + +import java.util.List; + +/** + * @author Damian Jansen djansen@redhat.com + */ +@Slf4j +public class ProjectWebHooksTab extends ProjectBasePage { + + private By webHooksList = By.id("settings-webhooks-list"); + private By urlInputField = By.id("payloadUrlInput"); + + public ProjectWebHooksTab(WebDriver driver) { + super(driver); + } + + public ProjectWebHooksTab enterUrl(String url) { + waitForWebElement(urlInputField).sendKeys(url + Keys.ENTER); + return new ProjectWebHooksTab(getDriver()); + } + + public List getWebHooks() { + return WebElementUtil.elementsToText(waitForWebElement(webHooksList) + .findElement(By.className("list--slat")) + .findElements(By.className("list-item"))); + } + + public ProjectWebHooksTab waitForWebHooksContains(final String url) { + waitForAMoment().until(new Predicate() { + @Override + public boolean apply(WebDriver input) { + return getWebHooks().contains(url); + } + }); + return new ProjectWebHooksTab(getDriver()); + } + + public ProjectWebHooksTab waitForWebHooksNotContains(final String url) { + waitForAMoment().until(new Predicate() { + @Override + public boolean apply(WebDriver input) { + return !getWebHooks().contains(url); + } + }); + return new ProjectWebHooksTab(getDriver()); + } + + public ProjectWebHooksTab clickRemoveOn(String url) { + List listItems = waitForWebElement(webHooksList) + .findElement(By.className("list--slat")) + .findElements(By.className("list-item")); + boolean clicked = false; + for (WebElement listItem : listItems) { + if (listItem.getText().contains(url)) { + listItem.findElement(By.tagName("a")).click(); + clicked = true; + break; + } + } + if (!clicked) { + log.info("Did not find item {}", url); + } + return new ProjectWebHooksTab(getDriver()); + } +} 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 new file mode 100644 index 0000000000..b436c180de --- /dev/null +++ b/functional-test/src/test/java/org/zanata/feature/project/EditWebHooksTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014, 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.feature.project; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.zanata.feature.Feature; +import org.zanata.feature.testharness.TestPlan.DetailedTest; +import org.zanata.feature.testharness.ZanataTestCase; +import org.zanata.page.projects.projectsettings.ProjectWebHooksTab; +import org.zanata.util.SampleProjectRule; +import org.zanata.workflow.LoginWorkFlow; + +import static org.assertj.core.api.Assertions.assertThat; +/** + * @author Damian Jansen djansen@redhat.com + */ +@Category(DetailedTest.class) +public class EditWebHooksTest extends ZanataTestCase { + + @Rule + public SampleProjectRule sampleProjectRule = new SampleProjectRule(); + + @Feature(summary = "The maintainer can add WebHooks for a project", + tcmsTestPlanIds = 5316, tcmsTestCaseIds = 0) + @Test(timeout = ZanataTestCase.MAX_SHORT_TEST_DURATION) + public void addWebHook() throws Exception { + String testUrl = "http://www.example.com"; + ProjectWebHooksTab projectWebHooksTab = new LoginWorkFlow() + .signIn("admin", "admin") + .goToProjects() + .goToProject("about fedora") + .gotoSettingsTab() + .gotoSettingsWebHooksTab() + .enterUrl(testUrl) + .waitForWebHooksContains(testUrl); + + assertThat(projectWebHooksTab.getWebHooks()) + .contains(testUrl) + .as("The web hook was added"); + } + + @Feature(summary = "The maintainer can add WebHooks for a project", + tcmsTestPlanIds = 5316, tcmsTestCaseIds = 0) + @Test(timeout = ZanataTestCase.MAX_SHORT_TEST_DURATION) + public void removeWebHook() throws Exception { + String testUrl = "http://www.example.com"; + ProjectWebHooksTab projectWebHooksTab = new LoginWorkFlow() + .signIn("admin", "admin") + .goToProjects() + .goToProject("about fedora") + .gotoSettingsTab() + .gotoSettingsWebHooksTab() + .enterUrl(testUrl) + .waitForWebHooksContains(testUrl) + .clickRemoveOn(testUrl) + .waitForWebHooksNotContains(testUrl); + + assertThat(projectWebHooksTab.getWebHooks()) + .doesNotContain(testUrl) + .as("The web hook was removed"); + } +} 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 072bb10f88..5eb3b8e0f1 100644 --- a/zanata-model/src/main/java/org/zanata/model/HProject.java +++ b/zanata-model/src/main/java/org/zanata/model/HProject.java @@ -31,6 +31,7 @@ import javax.persistence.Access; import javax.persistence.AccessType; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; @@ -52,8 +53,10 @@ import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Cascade; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.Where; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; import org.hibernate.validator.constraints.NotEmpty; @@ -118,6 +121,9 @@ public class HProject extends SlugEntityBase implements Serializable, name = "localeId")) private Set customizedLocales = Sets.newHashSet(); + @OneToMany(mappedBy = "project", cascade = CascadeType.ALL) + private List webHooks = Lists.newArrayList(); + @Enumerated(EnumType.STRING) private ProjectType defaultProjectType; diff --git a/zanata-model/src/main/java/org/zanata/model/WebHook.java b/zanata-model/src/main/java/org/zanata/model/WebHook.java new file mode 100644 index 0000000000..33da2e277a --- /dev/null +++ b/zanata-model/src/main/java/org/zanata/model/WebHook.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014, 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; + +import java.io.Serializable; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import org.zanata.model.validator.Url; + +import com.google.common.annotations.VisibleForTesting; + +/** + * @author Alex Eng aeng@redhat.com + */ +@Entity +@Getter +@Setter(AccessLevel.PRIVATE) +@NoArgsConstructor +public class WebHook implements Serializable { + + private Long id; + + private HProject project; + + @Url + private String url; + + public WebHook(HProject project, String url) { + this.project = project; + this.url = url; + } + + @Id + @GeneratedValue + public Long getId() { + return id; + } + + @ManyToOne + @JoinColumn(name = "projectId", nullable = false) + public HProject getProject() { + return project; + } +} 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 b45a434e8b..d001185a8b 100644 --- a/zanata-war/src/main/java/org/zanata/action/ProjectHome.java +++ b/zanata-war/src/main/java/org/zanata/action/ProjectHome.java @@ -52,6 +52,7 @@ import org.zanata.common.LocaleId; import org.zanata.common.ProjectType; import org.zanata.dao.AccountRoleDAO; +import org.zanata.dao.WebHookDAO; import org.zanata.i18n.Messages; import org.zanata.model.HAccount; import org.zanata.model.HAccountRole; @@ -59,6 +60,7 @@ import org.zanata.model.HPerson; import org.zanata.model.HProject; import org.zanata.model.HProjectIteration; +import org.zanata.model.WebHook; import org.zanata.seam.scope.ConversationScopeMessages; import org.zanata.security.ZanataIdentity; import org.zanata.service.LocaleService; @@ -69,6 +71,7 @@ import org.zanata.ui.autocomplete.LocaleAutocomplete; import org.zanata.ui.autocomplete.MaintainerAutocomplete; import org.zanata.util.ComparatorUtil; +import org.zanata.util.UrlUtil; import org.zanata.webtrans.shared.model.ValidationAction; import org.zanata.webtrans.shared.model.ValidationId; import org.zanata.webtrans.shared.validation.ValidationFactory; @@ -110,6 +113,9 @@ public class ProjectHome extends SlugHome { @In private AccountRoleDAO accountRoleDAO; + @In + private WebHookDAO webHookDAO; + @In private ValidationService validationServiceImpl; @@ -533,6 +539,41 @@ public List getValidationList() { return sortedList; } + @Restrict("#{s:hasPermission(projectHome.instance, 'update')}") + public void addWebHook(String url) { + if (isValidUrl(url)) { + WebHook webHook = new WebHook(this.getInstance(), url); + getInstance().getWebHooks().add(webHook); + update(); + FacesMessages.instance().add(StatusMessage.Severity.INFO, + msgs.format("jsf.project.AddNewWebhook", webHook.getUrl())); + } + } + + @Restrict("#{s:hasPermission(projectHome.instance, 'update')}") + public void removeWebHook(WebHook webHook) { + getInstance().getWebHooks().remove(webHook); + webHookDAO.makeTransient(webHook); + FacesMessages.instance().add(StatusMessage.Severity.INFO, + msgs.format("jsf.project.RemoveWebhook", webHook.getUrl())); + } + + private boolean isValidUrl(String url) { + if (!UrlUtil.isValidUrl(url)) { + FacesMessages.instance().add(StatusMessage.Severity.ERROR, + msgs.format("jsf.project.InvalidUrl", url)); + return false; + } + for(WebHook webHook: getInstance().getWebHooks()) { + if(StringUtils.equalsIgnoreCase(webHook.getUrl(), url)) { + FacesMessages.instance().add(StatusMessage.Severity.ERROR, + msgs.format("jsf.project.DuplicateUrl", url)); + return false; + } + } + return true; + } + /** * If this action is enabled(Warning or Error), then it's exclusive * validation will be turn off diff --git a/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java b/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java index 334acfeb2c..16e3a1bba7 100644 --- a/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java +++ b/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java @@ -113,6 +113,16 @@ public List getTextFlowAndTarget(List idList, Long localeId) { return q.list(); } + public int getWordCount(Long id) { + Query q = getSession().createQuery( + "select tf.wordCount from HTextFlow tf where tf.id = :id"); + q.setCacheable(true). + setComment("TextFlowDAO.getWordCount"). + setParameter("id", id); + Long totalCount = (Long) q.uniqueResult(); + return totalCount == null ? 0 : totalCount.intValue(); + } + public int getTotalWords() { Query q = getSession() diff --git a/zanata-war/src/main/java/org/zanata/dao/WebHookDAO.java b/zanata-war/src/main/java/org/zanata/dao/WebHookDAO.java new file mode 100644 index 0000000000..bdad7a872d --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/dao/WebHookDAO.java @@ -0,0 +1,22 @@ +package org.zanata.dao; + +import org.hibernate.Session; +import org.jboss.seam.ScopeType; +import org.jboss.seam.annotations.AutoCreate; +import org.jboss.seam.annotations.Name; +import org.jboss.seam.annotations.Scope; +import org.zanata.model.WebHook; + +@Name("webHookDAO") +@AutoCreate +@Scope(ScopeType.STATELESS) +public class WebHookDAO extends AbstractDAOImpl { + + public WebHookDAO() { + super(WebHook.class); + } + + public WebHookDAO(Session session) { + super(WebHook.class, session); + } +} diff --git a/zanata-war/src/main/java/org/zanata/events/DocumentMilestoneEvent.java b/zanata-war/src/main/java/org/zanata/events/DocumentMilestoneEvent.java new file mode 100644 index 0000000000..ed6218543f --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/events/DocumentMilestoneEvent.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014, 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.events; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import org.codehaus.jackson.annotate.JsonPropertyOrder; +import org.zanata.common.ContentState; +import org.zanata.common.LocaleId; + +import java.util.Collection; + +/** + * @author Alex Eng aeng@redhat.com + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@JsonPropertyOrder({ "project", "version", "docId", "locale", "milestone"}) +@EqualsAndHashCode +public class DocumentMilestoneEvent extends JSONType { + + public static final String EVENT_NAME = + "org.zanata.event.DocumentMilestoneEvent"; + + private String project; + private String version; + private String docId; + private LocaleId locale; + private String milestone; + + @Override + public String getEventType() { + return EVENT_NAME; + } +} diff --git a/zanata-war/src/main/java/org/zanata/events/DocumentStatisticUpdatedEvent.java b/zanata-war/src/main/java/org/zanata/events/DocumentStatisticUpdatedEvent.java new file mode 100644 index 0000000000..bedd646c94 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/events/DocumentStatisticUpdatedEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014, 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.events; + +import lombok.Data; + +import org.zanata.common.ContentState; +import org.zanata.common.LocaleId; +import org.zanata.ui.model.statistic.WordStatistic; + +@Data +public final class DocumentStatisticUpdatedEvent { + public static final String EVENT_NAME = + "org.zanata.event.DocumentStatisticUpdated"; + + private final WordStatistic oldStats; + private final WordStatistic newStats; + + private final Long projectIterationId; + private final Long documentId; + private final LocaleId localeId; + + private final ContentState previousState; + private final ContentState newState; +} diff --git a/zanata-war/src/main/java/org/zanata/events/JSONType.java b/zanata-war/src/main/java/org/zanata/events/JSONType.java new file mode 100644 index 0000000000..b131d5b383 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/events/JSONType.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014, 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.events; + +import java.io.IOException; +import java.io.Serializable; + +import org.codehaus.jackson.annotate.JsonIgnore; +import org.codehaus.jackson.map.ObjectMapper; + +/** + * @author Alex Eng aeng@redhat.com + */ +public abstract class JSONType implements Serializable { + + public abstract String getEventType(); + + @JsonIgnore + public String getJSON() { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.writeValueAsString(this); + } catch (IOException e) { + return this.getClass().getName() + "@" + + Integer.toHexString(this.hashCode()); + } + } +} diff --git a/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java b/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java index 6aaa03ae14..3dfb5facc7 100644 --- a/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java +++ b/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java @@ -86,6 +86,7 @@ public Response getDocumentStatistics( return Response.ok(entity).build(); } + //TODO: need to merge with StatisticsServiceImpl.getDocStatistics public ContainerTranslationStatistics getDocStatistics(Long documentId, LocaleId localeId) { ContainerTranslationStatistics result = diff --git a/zanata-war/src/main/java/org/zanata/service/DocumentService.java b/zanata-war/src/main/java/org/zanata/service/DocumentService.java index 570638452c..62279dc787 100644 --- a/zanata-war/src/main/java/org/zanata/service/DocumentService.java +++ b/zanata-war/src/main/java/org/zanata/service/DocumentService.java @@ -21,8 +21,11 @@ package org.zanata.service; import org.zanata.async.AsyncTaskHandle; +import org.zanata.events.DocumentStatisticUpdatedEvent; +import org.zanata.events.TextFlowTargetStateEvent; import org.zanata.model.HDocument; import org.zanata.rest.dto.resource.Resource; +import org.zanata.ui.model.statistic.WordStatistic; import java.util.Set; import java.util.concurrent.Future; @@ -32,6 +35,9 @@ * href="mailto:camunoz@redhat.com">camunoz@redhat.com */ public interface DocumentService { + // milestone for contentState to publish event (percentage) + public static final int DOC_EVENT_MILESTONE= 100; + /** * Creates or Updates a document. * @@ -104,4 +110,12 @@ public Future saveDocumentAsync(String projectSlug, * The document to make obsolete. */ public void makeObsolete(HDocument document); + + /** + * Post process when statistic in document changes + * (on DocumentStatisticUpdatedEvent) + * + * @param event + */ + public void documentStatisticUpdated(DocumentStatisticUpdatedEvent event); } 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 970e7cae8e..24780cdf8b 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 @@ -20,12 +20,15 @@ */ package org.zanata.service.impl; +import java.util.Collection; import java.util.Set; import java.util.concurrent.Future; +import com.google.common.collect.Lists; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Name; +import org.jboss.seam.annotations.Observer; import org.jboss.seam.annotations.Scope; import org.jboss.seam.annotations.Transactional; import org.jboss.seam.core.Events; @@ -35,14 +38,20 @@ import org.zanata.async.AsyncTaskHandle; import org.zanata.async.AsyncTaskResult; import org.zanata.async.ContainsAsyncMethods; +import org.zanata.common.ContentState; import org.zanata.dao.DocumentDAO; import org.zanata.dao.ProjectIterationDAO; +import org.zanata.events.DocumentMilestoneEvent; +import org.zanata.events.DocumentStatisticUpdatedEvent; import org.zanata.events.DocumentUploadedEvent; +import org.zanata.i18n.Messages; import org.zanata.lock.Lock; import org.zanata.model.HAccount; import org.zanata.model.HDocument; import org.zanata.model.HLocale; +import org.zanata.model.HProject; import org.zanata.model.HProjectIteration; +import org.zanata.model.WebHook; import org.zanata.rest.dto.resource.Resource; import org.zanata.rest.service.ResourceUtils; import org.zanata.security.ZanataIdentity; @@ -52,6 +61,10 @@ import org.zanata.service.LockManagerService; import org.zanata.service.TranslationStateCache; import org.zanata.service.VersionStateCache; +import org.zanata.ui.model.statistic.WordStatistic; + +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; /** * Default implementation of the {@link DocumentService} business service @@ -63,8 +76,9 @@ @Name("documentServiceImpl") @Scope(ScopeType.STATELESS) @ContainsAsyncMethods +@Slf4j public class DocumentServiceImpl implements DocumentService { - @In + @In(required = false) private ZanataIdentity identity; @In @@ -93,9 +107,13 @@ public class DocumentServiceImpl implements DocumentService { @In private ApplicationConfiguration applicationConfiguration; - @In(value = JpaIdentityStore.AUTHENTICATED_USER, scope = ScopeType.SESSION) + + @In(value = JpaIdentityStore.AUTHENTICATED_USER, scope = ScopeType.SESSION, + required = false) private HAccount authenticatedAccount; + @In + private Messages msgs; @Override @Transactional @@ -124,7 +142,8 @@ public HDocument saveDocument(String projectSlug, String iterationSlug, @Override @Async @Transactional - public Future saveDocumentAsync(String projectSlug, String iterationSlug, + public Future saveDocumentAsync(String projectSlug, + String iterationSlug, Resource sourceDoc, Set extensions, boolean copyTrans, boolean lock, AsyncTaskHandle handle) { // TODO Use the pased in handle @@ -180,7 +199,7 @@ public HDocument saveDocument(String projectSlug, String iterationSlug, long actorId = authenticatedAccount.getPerson().getId(); if (changed) { - if( Events.exists() ) { + if (Events.exists()) { Events.instance().raiseTransactionSuccessEvent( DocumentUploadedEvent.EVENT_NAME, new DocumentUploadedEvent(actorId, document.getId(), @@ -207,6 +226,80 @@ public void makeObsolete(HDocument document) { clearStatsCacheForUpdatedDocument(document); } + @Observer(DocumentStatisticUpdatedEvent.EVENT_NAME) + public void documentStatisticUpdated(DocumentStatisticUpdatedEvent event) { + processWebHookDocumentMilestoneEvent(event, + ContentState.TRANSLATED_STATES, + msgs.format("jsf.webhook.response.state", DOC_EVENT_MILESTONE, + ContentState.Translated), DOC_EVENT_MILESTONE); + + processWebHookDocumentMilestoneEvent(event, + Lists.newArrayList(ContentState.Approved), + msgs.format("jsf.webhook.response.state", DOC_EVENT_MILESTONE, + ContentState.Approved), DOC_EVENT_MILESTONE); + } + + private void processWebHookDocumentMilestoneEvent( + DocumentStatisticUpdatedEvent event, + Collection contentStates, String message, + int percentMilestone) { + + boolean shouldPublish = + hasContentStateReachedMilestone(event.getOldStats(), + event.getNewStats(), contentStates, percentMilestone); + + if (shouldPublish) { + HProjectIteration version = + projectIterationDAO.findById(event.getProjectIterationId()); + HProject project = version.getProject(); + + if (!project.getWebHooks().isEmpty()) { + HDocument document = documentDAO.getById(event.getDocumentId()); + DocumentMilestoneEvent milestoneEvent = + new DocumentMilestoneEvent(project.getSlug(), + version.getSlug(), document.getDocId(), + event.getLocaleId(), message); + for (WebHook webHook : project.getWebHooks()) { + publishDocumentMilestoneEvent(webHook, milestoneEvent); + } + } + } + } + + public void publishDocumentMilestoneEvent(WebHook webHook, + DocumentMilestoneEvent milestoneEvent) { + WebHooksPublisher.publish(webHook.getUrl(), milestoneEvent); + log.info("firing webhook: {}:{}:{}:{}", + webHook.getUrl(), milestoneEvent.getProject(), + milestoneEvent.getVersion(), milestoneEvent.getDocId()); + } + + /** + * Check if contentStates in statistic has reached given + * milestone(percentage) and not equals to contentStates in previous + * statistic + * + * @param oldStats + * @param newStats + * @param contentStates + * @param percentMilestone + */ + private boolean hasContentStateReachedMilestone(WordStatistic oldStats, + WordStatistic newStats, Collection contentStates, + int percentMilestone) { + int oldStateCount = 0, newStateCount = 0; + double percent = 0; + + for (ContentState contentState : contentStates) { + oldStateCount += oldStats.get(contentState); + newStateCount += newStats.get(contentState); + percent += newStats.getPercentage(contentState); + } + + return oldStateCount != newStateCount && + Double.compare(percent, percentMilestone) == 0; + } + /** * Invoke the copy trans function for a document. * @@ -220,8 +313,17 @@ private void copyTranslations(HDocument document) { } private void clearStatsCacheForUpdatedDocument(HDocument document) { - versionStateCacheImpl.clearVersionStatsCache(document.getProjectIteration() + versionStateCacheImpl.clearVersionStatsCache(document + .getProjectIteration() .getId()); translationStateCacheImpl.clearDocumentStatistics(document.getId()); } + + @VisibleForTesting + public void init(ProjectIterationDAO projectIterationDAO, + DocumentDAO documentDAO, Messages msgs) { + this.projectIterationDAO = projectIterationDAO; + this.documentDAO = documentDAO; + this.msgs = msgs; + } } diff --git a/zanata-war/src/main/java/org/zanata/service/impl/TranslationStateCacheImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/TranslationStateCacheImpl.java index 6978b76370..e44771eafb 100644 --- a/zanata-war/src/main/java/org/zanata/service/impl/TranslationStateCacheImpl.java +++ b/zanata-war/src/main/java/org/zanata/service/impl/TranslationStateCacheImpl.java @@ -37,7 +37,6 @@ import org.jboss.seam.annotations.Destroy; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Name; -import org.jboss.seam.annotations.Observer; import org.jboss.seam.annotations.Scope; import org.zanata.cache.CacheWrapper; import org.zanata.cache.EhcacheWrapper; @@ -176,11 +175,6 @@ public Boolean textFlowTargetHasWarningOrError(Long targetId, } } - /** - * This method contains all logic to be run immediately after a Text Flow - * Target has been successfully translated. - */ - @Observer(TextFlowTargetStateEvent.EVENT_NAME) @Override public void textFlowStateUpdated(TextFlowTargetStateEvent event) { DocumentLocaleKey key = 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 new file mode 100644 index 0000000000..437947f711 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/service/impl/TranslationUpdatedManager.java @@ -0,0 +1,79 @@ +package org.zanata.service.impl; + +import com.google.common.annotations.VisibleForTesting; +import org.jboss.seam.ScopeType; +import org.jboss.seam.annotations.AutoCreate; +import org.jboss.seam.annotations.In; +import org.jboss.seam.annotations.Name; +import org.jboss.seam.annotations.Observer; +import org.jboss.seam.annotations.Scope; +import org.jboss.seam.core.Events; +import org.zanata.dao.TextFlowDAO; +import org.zanata.events.DocumentStatisticUpdatedEvent; +import org.zanata.events.TextFlowTargetStateEvent; +import org.zanata.service.DocumentService; +import org.zanata.service.TranslationStateCache; +import org.zanata.ui.model.statistic.WordStatistic; +import org.zanata.util.StatisticsUtil; + +import lombok.extern.slf4j.Slf4j; + +/** + * Manager that handles post update of translation. Important: + * TextFlowTargetStateEvent IS NOT asynchronous, that is why + * DocumentStatisticUpdatedEvent is used for webhook processes. See + * {@link org.zanata.events.TextFlowTargetStateEvent} See + * {@link org.zanata.events.DocumentStatisticUpdatedEvent} + * + * @author Alex Eng aeng@redhat.com + */ +@Name("translationUpdatedManager") +@Scope(ScopeType.STATELESS) +@Slf4j +public class TranslationUpdatedManager { + + @In + private TranslationStateCache translationStateCacheImpl; + + @In + private TextFlowDAO textFlowDAO; + + /** + * This method contains all logic to be run immediately after a Text Flow + * Target has been successfully translated. + */ + @Observer(TextFlowTargetStateEvent.EVENT_NAME) + public void textFlowStateUpdated(TextFlowTargetStateEvent event) { + translationStateCacheImpl.textFlowStateUpdated(event); + publishAsyncEvent(event); + } + + // Fire asynchronous event + public void publishAsyncEvent(TextFlowTargetStateEvent event) { + if (Events.exists()) { + WordStatistic stats = + translationStateCacheImpl.getDocumentStatistics( + event.getDocumentId(), event.getLocaleId()); + + int wordCount = textFlowDAO.getWordCount(event.getTextFlowId()); + + WordStatistic oldStats = StatisticsUtil.copyWordStatistic(stats); + oldStats.decrement(event.getNewState(), wordCount); + oldStats.increment(event.getPreviousState(), wordCount); + + Events.instance().raiseAsynchronousEvent( + DocumentStatisticUpdatedEvent.EVENT_NAME, + new DocumentStatisticUpdatedEvent(oldStats, stats, + event.getProjectIterationId(), + event.getDocumentId(), event.getLocaleId(), + event.getPreviousState(), event.getNewState())); + } + } + + @VisibleForTesting + public void init(TranslationStateCache translationStateCacheImpl, + TextFlowDAO textFlowDAO) { + this.translationStateCacheImpl = translationStateCacheImpl; + this.textFlowDAO = textFlowDAO; + } +} diff --git a/zanata-war/src/main/java/org/zanata/service/impl/VersionGroupServiceImpl.java b/zanata-war/src/main/java/org/zanata/service/impl/VersionGroupServiceImpl.java index dbd0902a88..c769c03fdb 100644 --- a/zanata-war/src/main/java/org/zanata/service/impl/VersionGroupServiceImpl.java +++ b/zanata-war/src/main/java/org/zanata/service/impl/VersionGroupServiceImpl.java @@ -46,7 +46,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import com.google.common.collect.Sets; /** * @author Alex Eng aeng@redhat.com 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 new file mode 100644 index 0000000000..aaf5877a14 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/service/impl/WebHooksPublisher.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014, 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 javax.annotation.Nonnull; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.client.ClientRequest; +import org.jboss.resteasy.client.ClientResponse; +import org.zanata.events.JSONType; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author Alex Eng aeng@redhat.com + */ +@Slf4j +public class WebHooksPublisher { + + private static ClientResponse publish(@Nonnull String url, + @Nonnull String data, @Nonnull MediaType acceptType, + @Nonnull MediaType mediaType) { + try { + ClientRequest request = new ClientRequest(url); + request.accept(acceptType); + request.body(mediaType, data); + return request.post(); + } catch (Exception e) { + log.error("Error on webHooks post {}", e, url); + return null; + } + } + + public static ClientResponse publish(@Nonnull String url, + @Nonnull JSONType event) { + return publish(url, event.getJSON(), MediaType.APPLICATION_JSON_TYPE, + MediaType.APPLICATION_JSON_TYPE); + } +} diff --git a/zanata-war/src/main/java/org/zanata/ui/model/statistic/AbstractStatistic.java b/zanata-war/src/main/java/org/zanata/ui/model/statistic/AbstractStatistic.java index e619f77ad4..6cef08e48a 100644 --- a/zanata-war/src/main/java/org/zanata/ui/model/statistic/AbstractStatistic.java +++ b/zanata-war/src/main/java/org/zanata/ui/model/statistic/AbstractStatistic.java @@ -118,6 +118,24 @@ public int getRejected() { return rejected; } + public double getPercentage(ContentState contentState) { + switch (contentState) { + case Translated: + return getPercentTranslated(); + case NeedReview: + return getPercentFuzzy(); + case New: + return getPercentUntranslated(); + case Approved: + return getPercentApproved(); + case Rejected: + return getPercentRejected(); + default: + throw new RuntimeException("not implemented for state " + + contentState.name()); + } + } + public double getPercentTranslated() { return getPercentage(getTranslated()); } diff --git a/zanata-war/src/main/java/org/zanata/util/StatisticsUtil.java b/zanata-war/src/main/java/org/zanata/util/StatisticsUtil.java index b6ed13dfbf..469033a01f 100644 --- a/zanata-war/src/main/java/org/zanata/util/StatisticsUtil.java +++ b/zanata-war/src/main/java/org/zanata/util/StatisticsUtil.java @@ -13,7 +13,7 @@ public class StatisticsUtil { public static int calculateUntranslated(Long totalCount, - BaseTranslationCount translationCount) { + BaseTranslationCount translationCount) { return totalCount.intValue() - translationCount.get(ContentState.Translated) - translationCount.get(ContentState.NeedReview) @@ -26,6 +26,18 @@ public static double getRemainingHours(WordStatistic wordsStatistic) { wordsStatistic.getNeedReview() + wordsStatistic.getRejected()); } + public static WordStatistic copyWordStatistic(WordStatistic from) { + if (from == null) { + return null; + } + WordStatistic copy = + new WordStatistic(from.getApproved(), from.getNeedReview(), + from.getUntranslated(), from.getTranslated(), + from.getRejected()); + copy.setRemainingHours(from.getRemainingHours()); + return copy; + } + public static double getRemainingHours( TranslationStatistics translationStatistics) { return getRemainingHours(translationStatistics.getUntranslated(), diff --git a/zanata-war/src/main/java/org/zanata/util/UrlUtil.java b/zanata-war/src/main/java/org/zanata/util/UrlUtil.java index 05fc0e57f2..ec6dd71f21 100644 --- a/zanata-war/src/main/java/org/zanata/util/UrlUtil.java +++ b/zanata-war/src/main/java/org/zanata/util/UrlUtil.java @@ -22,6 +22,8 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; @@ -29,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.AutoCreate; import org.jboss.seam.annotations.Name; @@ -151,4 +154,16 @@ public static String decodeString(String var) { throw new RuntimeException(e); } } + + public static boolean isValidUrl(String url) { + if (StringUtils.isEmpty(url)) { + return false; + } + try { + new URL(url); + return true; + } catch (MalformedURLException e) { + return false; + } + } } diff --git a/zanata-war/src/main/resources/db/changelogs/db.changelog-3.6.xml b/zanata-war/src/main/resources/db/changelogs/db.changelog-3.6.xml index 4ae3a8552e..336aae6ad0 100644 --- a/zanata-war/src/main/resources/db/changelogs/db.changelog-3.6.xml +++ b/zanata-war/src/main/resources/db/changelogs/db.changelog-3.6.xml @@ -10,4 +10,24 @@ + + Create WebHook table + + + + + + + + + + + + + + + + diff --git a/zanata-war/src/main/resources/messages.properties b/zanata-war/src/main/resources/messages.properties index 2ba0b65069..2d11b4f702 100644 --- a/zanata-war/src/main/resources/messages.properties +++ b/zanata-war/src/main/resources/messages.properties @@ -286,6 +286,13 @@ jsf.project.writable.Message=Your project will be viewable by the public and new jsf.project.LanguageRemoved=Language "{0}" has been removed from project. jsf.project.LanguageAdded=Language "{0}" has been added to project. jsf.project.LanguageUpdateFromGlobal=Updated languages from global settings. +jsf.project.AddWebhook=Add webhook +jsf.project.RemoveWebhook=Webhook {0} removed. +jsf.project.AddNewWebhook=Webhook {0} added. +jsf.project.PayloadURL=Payload URL +jsf.project.InvalidUrl=Invalid URL: {0} +jsf.project.DuplicateUrl=URL needs to be unique. +jsf.webhook.response.state={0}% {1} #------ [home] > Projects > [project-id] ------ @@ -338,6 +345,7 @@ jsf.tooltip.TranslateOptions=Translate Options jsf.tooltip.DocumentOptions=Document Options jsf.tooltip.options=Options jsf.Permissions=Permissions +jsf.project.WebHooks=Webhooks jsf.project.MaintainerRemoved=Maintainer "{0}" has been removed from project. jsf.project.NeedAtLeastOneMaintainer=Need at least 1 maintainer in project. jsf.project.MaintainerAdded=Maintainer "{0}" has been added to project. diff --git a/zanata-war/src/main/webapp-jboss/WEB-INF/classes/META-INF/persistence.xml b/zanata-war/src/main/webapp-jboss/WEB-INF/classes/META-INF/persistence.xml index 853d9e56e0..5851432535 100644 --- a/zanata-war/src/main/webapp-jboss/WEB-INF/classes/META-INF/persistence.xml +++ b/zanata-war/src/main/webapp-jboss/WEB-INF/classes/META-INF/persistence.xml @@ -48,6 +48,7 @@ org.zanata.model.tm.TransMemoryUnit org.zanata.model.tm.TransMemoryUnitVariant org.zanata.model.tm.TransMemory + org.zanata.model.WebHook