diff options
author | Julien Lancelot <julien.lancelot@gmail.com> | 2013-06-21 15:02:43 +0200 |
---|---|---|
committer | Julien Lancelot <julien.lancelot@gmail.com> | 2013-06-21 15:02:43 +0200 |
commit | 847f1c45da27d089ecf6a310930820c0d5b1e6b9 (patch) | |
tree | 67e32e0a7f86e9c663694582eb44c2f1b7fe7e0f | |
parent | 0ce40ec70d380493eb20bf56c3aef9ab5ba498c1 (diff) | |
download | sonarqube-847f1c45da27d089ecf6a310930820c0d5b1e6b9.tar.gz sonarqube-847f1c45da27d089ecf6a310930820c0d5b1e6b9.zip |
SONAR-3714 New service to execute bulk change on issues
4 files changed, 484 insertions, 1 deletions
diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index 7f271b3e6be..6c4af322a57 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -218,7 +218,7 @@ <configuration> <rules> <requireFilesSize> - <maxsize>60000000</maxsize> + <maxsize>61000000</maxsize> <minsize>54000000</minsize> <files> <file>${project.build.directory}/sonar-${project.version}.zip</file> diff --git a/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeQuery.java b/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeQuery.java new file mode 100644 index 00000000000..b4e0e31dabb --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeQuery.java @@ -0,0 +1,176 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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 3 of the License, or (at your option) any later version. + * + * SonarQube 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 program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.issue; + +import org.apache.commons.lang.builder.ReflectionToStringBuilder; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import java.util.Collection; +import java.util.Collections; + +import static com.google.common.collect.Lists.newArrayList; + +/** + * @since 3.7 + */ +public class IssueBulkChangeQuery { + + private static final String ASSIGNEE = "ASSIGNEE"; + private static final String PLAN = "PLAN"; + private static final String SEVERITY = "SEVERITY"; + private static final String TRANSITION = "TRANSITION"; + private static final String COMMENT = "COMMENT"; + + private final Collection<String> actions; + private final Collection<String> issueKeys; + private final String assignee; + private final String plan; + private final String severity; + private final String transition; + private final String comment; + + private IssueBulkChangeQuery(Builder builder) { + this.actions = defaultCollection(builder.actions); + this.issueKeys = defaultCollection(builder.issueKeys); + this.assignee = builder.assignee; + this.plan = builder.plan; + this.severity = builder.severity; + this.transition = builder.transition; + this.comment = builder.comment; + } + + public Collection<String> issueKeys() { + return issueKeys; + } + + @CheckForNull + public String assignee() { + return assignee; + } + + @CheckForNull + public String plan() { + return plan; + } + + @CheckForNull + public String severity() { + return severity; + } + + @CheckForNull + public String transition() { + return transition; + } + + @CheckForNull + public String comment() { + return comment; + } + + public boolean isOnAssignee() { + return actions.contains(ASSIGNEE); + } + + public boolean isOnActionPlan() { + return actions.contains(PLAN); + } + + public boolean isOnSeverity() { + return actions.contains(SEVERITY); + } + + public boolean isOnTransition() { + return actions.contains(TRANSITION); + } + + public boolean isOnComment() { + return actions.contains(COMMENT); + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private Collection<String> actions = newArrayList(); + private Collection<String> issueKeys; + private String assignee; + private String plan; + private String severity; + private String transition; + private String comment; + + private Builder() { + } + + public Builder issueKeys(@Nullable Collection<String> l) { + this.issueKeys = l; + return this; + } + + public Builder assignee(@Nullable String assignee) { + actions.add(IssueBulkChangeQuery.ASSIGNEE); + this.assignee = assignee; + return this; + } + + public Builder plan(@Nullable String plan) { + actions.add(IssueBulkChangeQuery.PLAN); + this.plan = plan; + return this; + } + + public Builder severity(@Nullable String severity) { + actions.add(IssueBulkChangeQuery.SEVERITY); + this.severity = severity; + return this; + } + + public Builder transition(@Nullable String transition) { + actions.add(IssueBulkChangeQuery.TRANSITION); + this.transition = transition; + return this; + } + + public Builder comment(@Nullable String comment) { + actions.add(IssueBulkChangeQuery.COMMENT); + this.comment = comment; + return this; + } + + public IssueBulkChangeQuery build() { + return new IssueBulkChangeQuery(this); + } + } + + private static <T> Collection<T> defaultCollection(@Nullable Collection<T> c) { + return c == null ? Collections.<T>emptyList() : Collections.unmodifiableCollection(c); + } + +} diff --git a/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java b/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java new file mode 100644 index 00000000000..d399748e7b5 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java @@ -0,0 +1,119 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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 3 of the License, or (at your option) any later version. + * + * SonarQube 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 program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.issue; + +import com.google.common.base.Strings; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.IssueQuery; +import org.sonar.api.issue.IssueQueryResult; +import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.api.user.UserFinder; +import org.sonar.api.web.UserRole; +import org.sonar.core.issue.IssueNotifications; +import org.sonar.core.issue.IssueUpdater; +import org.sonar.core.issue.db.IssueStorage; +import org.sonar.core.issue.workflow.IssueWorkflow; +import org.sonar.server.user.UserSession; + +import java.util.Date; +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; + +public class IssueBulkChangeService { + + private final DefaultIssueFinder issueFinder; + private final IssueWorkflow workflow; + private final IssueUpdater issueUpdater; + private final IssueStorage issueStorage; + private final IssueNotifications issueNotifications; + private final ActionPlanService actionPlanService; + private final UserFinder userFinder; + + public IssueBulkChangeService(DefaultIssueFinder issueFinder, IssueWorkflow workflow, ActionPlanService actionPlanService, UserFinder userFinder, + IssueUpdater issueUpdater, IssueStorage issueStorage, IssueNotifications issueNotifications) { + this.issueFinder = issueFinder; + this.workflow = workflow; + this.issueUpdater = issueUpdater; + this.issueStorage = issueStorage; + this.issueNotifications = issueNotifications; + this.actionPlanService = actionPlanService; + this.userFinder = userFinder; + } + + public Result<List<Issue>> execute(IssueBulkChangeQuery issueBulkChangeQuery, UserSession userSession) { + Result<List<Issue>> result = Result.of(); + List<Issue> issues = newArrayList(); + verifyLoggedIn(userSession); + + IssueQueryResult issueQueryResult = issueFinder.find(IssueQuery.builder().issueKeys(issueBulkChangeQuery.issueKeys()).requiredRole(UserRole.USER).build()); + + String assignee = issueBulkChangeQuery.assignee(); + if (assignee != null && userFinder.findByLogin(assignee) == null) { + throw new IllegalArgumentException("Unknown user: " + assignee); + } + + String actionPlanKey = issueBulkChangeQuery.plan(); + if (!Strings.isNullOrEmpty(actionPlanKey) && actionPlanService.findByKey(actionPlanKey, userSession) == null) { + throw new IllegalArgumentException("Unknown action plan: " + actionPlanKey); + } + String severity = issueBulkChangeQuery.severity(); + String transition = issueBulkChangeQuery.transition(); + String comment = issueBulkChangeQuery.comment(); + + IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login()); + for (Issue issue : issueQueryResult.issues()) { + DefaultIssue defaultIssue = (DefaultIssue) issue; + try { + if (issueBulkChangeQuery.isOnAssignee()) { + issueUpdater.assign(defaultIssue, assignee, context); + } + if (issueBulkChangeQuery.isOnActionPlan()) { + issueUpdater.plan(defaultIssue, actionPlanKey, context); + } + if (issueBulkChangeQuery.isOnSeverity()) { + issueUpdater.setManualSeverity(defaultIssue, severity, context); + } + if (issueBulkChangeQuery.isOnTransition()) { + workflow.doTransition(defaultIssue, transition, context); + } + if (issueBulkChangeQuery.isOnComment()) { + issueUpdater.addComment(defaultIssue, comment, context); + } + issueStorage.save(defaultIssue); + issueNotifications.sendChanges(defaultIssue, context, issueQueryResult); + issues.add(defaultIssue); + } catch (Exception e) { + result.addError(e.getMessage()); + } + } + result.set(issues); + return result; + } + + private void verifyLoggedIn(UserSession userSession) { + if (!userSession.isLoggedIn()) { + // must be logged + throw new IllegalStateException("User is not logged in"); + } + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java b/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java new file mode 100644 index 00000000000..eac959c0105 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java @@ -0,0 +1,188 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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 3 of the License, or (at your option) any later version. + * + * SonarQube 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 program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.issue; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.IssueQuery; +import org.sonar.api.issue.IssueQueryResult; +import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.api.user.UserFinder; +import org.sonar.core.issue.DefaultActionPlan; +import org.sonar.core.issue.IssueNotifications; +import org.sonar.core.issue.IssueUpdater; +import org.sonar.core.issue.db.IssueStorage; +import org.sonar.core.issue.workflow.IssueWorkflow; +import org.sonar.core.user.DefaultUser; +import org.sonar.server.user.UserSession; + +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class IssueBulkChangeServiceTest { + + private DefaultIssueFinder finder = mock(DefaultIssueFinder.class); + private IssueWorkflow workflow = mock(IssueWorkflow.class); + private IssueUpdater issueUpdater = mock(IssueUpdater.class); + private IssueStorage issueStorage = mock(IssueStorage.class); + private IssueNotifications issueNotifications = mock(IssueNotifications.class); + private ActionPlanService actionPlanService = mock(ActionPlanService.class); + private UserFinder userFinder = mock(UserFinder.class); + + private IssueQueryResult issueQueryResult = mock(IssueQueryResult.class); + private UserSession userSession = mock(UserSession.class); + private DefaultIssue issue = new DefaultIssue().setKey("ABCD"); + + private IssueBulkChangeService service; + + @Before + public void before(){ + when(userSession.isLoggedIn()).thenReturn(true); + when(userSession.userId()).thenReturn(10); + when(userSession.login()).thenReturn("fred"); + + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + when(issueQueryResult.issues()).thenReturn(newArrayList((Issue) issue)); + + service = new IssueBulkChangeService(finder, workflow, actionPlanService, userFinder, issueUpdater, issueStorage, issueNotifications); + } + + @Test + public void should_do_bulk_assign(){ + String assignee = "perceval"; + when(userFinder.findByLogin(assignee)).thenReturn(new DefaultUser()); + + IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).assignee(assignee).build(); + Result result = service.execute(issueBulkChangeQuery, userSession); + assertThat(result.ok()).isTrue(); + assertThat((List)result.get()).hasSize(1); + + verify(issueUpdater).assign(eq(issue), eq(assignee), any(IssueChangeContext.class)); + verifyNoMoreInteractions(issueUpdater); + verify(issueStorage).save(eq(issue)); + verifyNoMoreInteractions(issueStorage); + verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult)); + verifyNoMoreInteractions(issueNotifications); + } + + @Test + public void should_do_bulk_plan(){ + String actionPlanKey = "EFGH"; + when(actionPlanService.findByKey(actionPlanKey, userSession)).thenReturn(new DefaultActionPlan()); + + IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).plan(actionPlanKey).build(); + Result result = service.execute(issueBulkChangeQuery, userSession); + assertThat(result.ok()).isTrue(); + assertThat((List)result.get()).hasSize(1); + + verify(issueUpdater).plan(eq(issue), eq(actionPlanKey), any(IssueChangeContext.class)); + verifyNoMoreInteractions(issueUpdater); + verify(issueStorage).save(eq(issue)); + verifyNoMoreInteractions(issueStorage); + verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult)); + verifyNoMoreInteractions(issueNotifications); + } + + @Test + public void should_do_bulk_change_severity(){ + String severity = "MINOR"; + + IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).severity(severity).build(); + Result result = service.execute(issueBulkChangeQuery, userSession); + assertThat(result.ok()).isTrue(); + assertThat((List)result.get()).hasSize(1); + + verify(issueUpdater).setManualSeverity(eq(issue), eq(severity), any(IssueChangeContext.class)); + verifyNoMoreInteractions(issueUpdater); + verify(issueStorage).save(eq(issue)); + verifyNoMoreInteractions(issueStorage); + verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult)); + verifyNoMoreInteractions(issueNotifications); + } + + @Test + public void should_do_bulk_transition(){ + String transition = "reopen"; + + when(workflow.doTransition(eq(issue), eq(transition), any(IssueChangeContext.class))).thenReturn(true); + + IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).transition(transition).build(); + Result result = service.execute(issueBulkChangeQuery, userSession); + assertThat(result.ok()).isTrue(); + assertThat((List)result.get()).hasSize(1); + + verify(workflow).doTransition(eq(issue), eq(transition), any(IssueChangeContext.class)); + verifyNoMoreInteractions(issueUpdater); + verify(issueStorage).save(eq(issue)); + verifyNoMoreInteractions(issueStorage); + verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult)); + verifyNoMoreInteractions(issueNotifications); + } + + @Test + public void should_do_bulk_comment(){ + String comment = "Bulk change comment"; + + IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).comment(comment).build(); + Result result = service.execute(issueBulkChangeQuery, userSession); + assertThat(result.ok()).isTrue(); + assertThat((List)result.get()).hasSize(1); + + verify(issueUpdater).addComment(eq(issue), eq(comment), any(IssueChangeContext.class)); + verifyNoMoreInteractions(issueUpdater); + verify(issueStorage).save(eq(issue)); + verifyNoMoreInteractions(issueStorage); + verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult)); + verifyNoMoreInteractions(issueNotifications); + } + + @Test + public void should_ignore_an_issue_if_an_action_fail(){ + when(issueQueryResult.issues()).thenReturn(newArrayList((Issue) issue, new DefaultIssue().setKey("EFGH"))); + + // Bulk change with 2 actions : severity and comment + IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList("ABCD", "EFGH")).severity("MAJOR").comment("Bulk change comment").build(); + + // The first call the change severity is ok, the second will fail + when(issueUpdater.setManualSeverity(any(DefaultIssue.class), eq("MAJOR"),any(IssueChangeContext.class))).thenReturn(true).thenThrow(new RuntimeException("Cant change severity")); + + Result result = service.execute(issueBulkChangeQuery, userSession); + assertThat(result.ok()).isFalse(); + assertThat(((Result.Message) result.errors().get(0)).text()).isEqualTo("Cant change severity"); + + List<Issue> issues = (List) result.get(); + assertThat(issues).hasSize(1); + assertThat(issues.get(0).key()).isEqualTo("ABCD"); + + verify(issueStorage).save(eq(issue)); + verifyNoMoreInteractions(issueStorage); + verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult)); + verifyNoMoreInteractions(issueNotifications); + } + +} |