]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3714 New service to execute bulk change on issues
authorJulien Lancelot <julien.lancelot@gmail.com>
Fri, 21 Jun 2013 13:02:43 +0000 (15:02 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Fri, 21 Jun 2013 13:02:43 +0000 (15:02 +0200)
sonar-application/pom.xml
sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeQuery.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java [new file with mode: 0644]

index 7f271b3e6bec65976ab724f611be5b92953f2ae4..6c4af322a572d0e33a828a502e03d90d69a5e43a 100644 (file)
             <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 (file)
index 0000000..b4e0e31
--- /dev/null
@@ -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 (file)
index 0000000..d399748
--- /dev/null
@@ -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 (file)
index 0000000..eac959c
--- /dev/null
@@ -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);
+  }
+
+}