Browse Source

SONAR-20977 Fix notifications sent twice for FP or accepted issues

tags/10.4.0.87286
Léo Geoffroy 7 months ago
parent
commit
d3c85d4240
20 changed files with 187 additions and 108 deletions
  1. 10
    10
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandler.java
  2. 13
    0
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java
  3. 13
    0
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java
  4. 57
    36
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandlerTest.java
  5. 1
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/AddCommentActionIT.java
  6. 1
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/AssignActionIT.java
  7. 3
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ChangeStatusActionIT.java
  8. 5
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/BulkChangeActionIT.java
  9. 36
    25
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueUpdaterIT.java
  10. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java
  11. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java
  12. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java
  13. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java
  14. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.java
  15. 17
    8
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
  16. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
  17. 16
    14
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
  18. 7
    7
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
  19. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
  20. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java

+ 10
- 10
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandler.java View File

@@ -86,8 +86,8 @@ public class FPOrAcceptedNotificationHandler extends EmailNotificationHandler<Is
.map(serializer::from)
// ignore notifications which contain no issue changed to a FP or Accepted status
.filter(t -> t.getIssues().stream()
.filter(issue -> issue.getNewIssueStatus().isPresent())
.anyMatch(issue -> FP_OR_ACCEPTED_SIMPLE_STATUSES.contains(issue.getNewIssueStatus().get())))
.filter(issue -> issue.getNewIssueStatus().isPresent() && issue.getOldIssueStatus().isPresent())
.anyMatch(issue -> !issue.getNewIssueStatus().equals(issue.getOldIssueStatus()) && FP_OR_ACCEPTED_SIMPLE_STATUSES.contains(issue.getNewIssueStatus().get())))
.map(NotificationWithProjectKeys::new)
.collect(Collectors.toSet());
if (changeNotificationsWithFpOrAccepted.isEmpty()) {
@@ -144,14 +144,14 @@ public class FPOrAcceptedNotificationHandler extends EmailNotificationHandler<Is
.collect(unorderedIndex(t -> t.getNewIssueStatus().get(), issue -> issue));

return Stream.of(
of(issuesByNewIssueStatus.get(IssueStatus.FALSE_POSITIVE))
.filter(t -> !t.isEmpty())
.map(fpIssues -> new FPOrAcceptedNotification(notification.getChange(), fpIssues, FP))
.orElse(null),
of(issuesByNewIssueStatus.get(IssueStatus.ACCEPTED))
.filter(t -> !t.isEmpty())
.map(acceptedIssues -> new FPOrAcceptedNotification(notification.getChange(), acceptedIssues, ACCEPTED))
.orElse(null))
of(issuesByNewIssueStatus.get(IssueStatus.FALSE_POSITIVE))
.filter(t -> !t.isEmpty())
.map(fpIssues -> new FPOrAcceptedNotification(notification.getChange(), fpIssues, FP))
.orElse(null),
of(issuesByNewIssueStatus.get(IssueStatus.ACCEPTED))
.filter(t -> !t.isEmpty())
.map(acceptedIssues -> new FPOrAcceptedNotification(notification.getChange(), acceptedIssues, ACCEPTED))
.orElse(null))
.filter(Objects::nonNull)
.map(fpOrAcceptedNotification -> new EmailDeliveryRequest(recipient.email(), fpOrAcceptedNotification));
});

+ 13
- 0
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java View File

@@ -65,11 +65,13 @@ public class IssuesChangesNotificationBuilder {
private final User assignee;
private final Rule rule;
private final Project project;
private final IssueStatus oldIssueStatus;

public ChangedIssue(Builder builder) {
this.key = requireNonNull(builder.key, KEY_CANT_BE_NULL_MESSAGE);
this.newStatus = requireNonNull(builder.newStatus, "newStatus can't be null");
this.newIssueStatus = builder.newIssueStatus;
this.oldIssueStatus = builder.oldIssueStatus;
this.assignee = builder.assignee;
this.rule = requireNonNull(builder.rule, "rule can't be null");
this.project = requireNonNull(builder.project, "project can't be null");
@@ -87,6 +89,10 @@ public class IssuesChangesNotificationBuilder {
return ofNullable(newIssueStatus);
}

public Optional<IssueStatus> getOldIssueStatus() {
return ofNullable(oldIssueStatus);
}

public Optional<User> getAssignee() {
return ofNullable(assignee);
}
@@ -103,6 +109,8 @@ public class IssuesChangesNotificationBuilder {
private final String key;
@CheckForNull
private IssueStatus newIssueStatus;
@CheckForNull
private IssueStatus oldIssueStatus;
private String newStatus;
@CheckForNull
private User assignee;
@@ -128,6 +136,11 @@ public class IssuesChangesNotificationBuilder {
return this;
}

public Builder setOldIssueStatus(@Nullable IssueStatus oldIssueStatus) {
this.oldIssueStatus = oldIssueStatus;
return this;
}

public Builder setRule(Rule rule) {
this.rule = rule;
return this;

+ 13
- 0
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java View File

@@ -89,6 +89,7 @@ public class IssuesChangesNotificationSerializer {
.map(issue -> new ChangedIssue.Builder(issue.key)
.setNewStatus(issue.newStatus)
.setNewIssueStatus(issue.newIssueStatus == null ? null : IssueStatus.valueOf(issue.newIssueStatus))
.setOldIssueStatus(issue.oldIssueStatus == null ? null : IssueStatus.valueOf(issue.oldIssueStatus))
.setAssignee(issue.assignee)
.setRule(rules.get(issue.ruleKey))
.setProject(projects.get(issue.projectUuid))
@@ -125,6 +126,8 @@ public class IssuesChangesNotificationSerializer {
notification.setFieldValue(issuePropertyPrefix + ".newStatus", issue.getNewStatus());
issue.getNewIssueStatus()
.ifPresent(newIssueStatus -> notification.setFieldValue(issuePropertyPrefix + ".newIssueStatus", newIssueStatus.name()));
issue.getOldIssueStatus()
.ifPresent(oldIssueStatus -> notification.setFieldValue(issuePropertyPrefix + ".oldIssueStatus", oldIssueStatus.name()));
notification.setFieldValue(issuePropertyPrefix + ".ruleKey", issue.getRule().getKey().toString());
notification.setFieldValue(issuePropertyPrefix + ".projectUuid", issue.getProject().getUuid());
}
@@ -137,6 +140,7 @@ public class IssuesChangesNotificationSerializer {
.setNewStatus(getIssueFieldValue(notification, issuePropertyPrefix + ".newStatus", index))
.setNewResolution(notification.getFieldValue(issuePropertyPrefix + ".newResolution"))
.setNewIssueStatus(notification.getFieldValue(issuePropertyPrefix + ".newIssueStatus"))
.setOldIssueStatus(notification.getFieldValue(issuePropertyPrefix + ".oldIssueStatus"))
.setAssignee(assignee)
.setRuleKey(getIssueFieldValue(notification, issuePropertyPrefix + ".ruleKey", index))
.setProjectUuid(getIssueFieldValue(notification, issuePropertyPrefix + ".projectUuid", index))
@@ -254,6 +258,8 @@ public class IssuesChangesNotificationSerializer {
@CheckForNull
private final String newIssueStatus;
@CheckForNull
private final String oldIssueStatus;
@CheckForNull
private final User assignee;
private final RuleKey ruleKey;
private final String projectUuid;
@@ -266,9 +272,11 @@ public class IssuesChangesNotificationSerializer {
this.ruleKey = RuleKey.parse(builder.ruleKey);
this.projectUuid = builder.projectUuid;
this.newIssueStatus = builder.newIssueStatus;
this.oldIssueStatus = builder.oldIssueStatus;
}

static class Builder {
private String oldIssueStatus = null;
private String key = null;
private String newStatus = null;
@CheckForNull
@@ -299,6 +307,11 @@ public class IssuesChangesNotificationSerializer {
return this;
}

public Builder setOldIssueStatus(@Nullable String oldIssueStatus) {
this.oldIssueStatus = oldIssueStatus;
return this;
}

public Builder setAssignee(@Nullable User assignee) {
this.assignee = assignee;
return this;

+ 57
- 36
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandlerTest.java View File

@@ -24,7 +24,6 @@ import com.google.common.collect.ListMultimap;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Random;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.IntStream;
@@ -62,7 +61,6 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
@@ -114,7 +112,7 @@ public class FPOrAcceptedNotificationHandlerTest {
@Test
public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
when(emailNotificationChannel.isActivated()).thenReturn(false);
Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
Set<IssuesChangesNotification> notifications = IntStream.range(0, 5)
.mapToObj(i -> mock(IssuesChangesNotification.class))
.collect(toSet());

@@ -129,7 +127,7 @@ public class FPOrAcceptedNotificationHandlerTest {

@Test
public void deliver_parses_every_notification_in_order() {
Set<IssuesChangesNotification> notifications = IntStream.range(0, 5 + new Random().nextInt(10))
Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
.mapToObj(i -> mock(IssuesChangesNotification.class))
.collect(toSet());
when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -143,7 +141,7 @@ public class FPOrAcceptedNotificationHandlerTest {

@Test
public void deliver_fails_with_IAE_if_serializer_throws_IAE() {
Set<IssuesChangesNotification> notifications = IntStream.range(0, 3 + new Random().nextInt(10))
Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
.mapToObj(i -> mock(IssuesChangesNotification.class))
.collect(toSet());
when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -168,8 +166,31 @@ public class FPOrAcceptedNotificationHandlerTest {
public void deliver_has_no_effect_if_no_issue_has_FP_or_wontfix_resolution(IssueStatus newIssueStatus) {
when(emailNotificationChannel.isActivated()).thenReturn(true);
Change changeMock = mock(Change.class);
Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewIssueStatus(newIssueStatus)).collect(toSet()), changeMock))
Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
.mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()), changeMock))
.map(serializer::serialize)
.collect(toSet());
reset(serializer);

int deliver = underTest.deliver(notifications);

assertThat(deliver).isZero();
verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class));
verifyNoInteractions(changeMock);
verifyNoMoreInteractions(serializer);
verifyNoInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
}

@Test
@UseDataProvider("FPorWontFixResolutionWithCorrespondingIssueStatus")
public void deliver_shouldNotSendNotification_WhenIssueStatusHasNotChanged(String newResolution,
IssueStatus newIssueStatus) {
when(emailNotificationChannel.isActivated()).thenReturn(true);
Change changeMock = mock(Change.class);
Set<IssuesChangesNotification> notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setNewIssueStatus(newIssueStatus).setOldIssueStatus(newIssueStatus)).collect(toSet()), changeMock))
.map(serializer::serialize)
.collect(toSet());
reset(serializer);
@@ -204,21 +225,21 @@ public class FPOrAcceptedNotificationHandlerTest {
Project projectKey4 = newProject(randomAlphabetic(7));
Change changeMock = mock(Change.class);
// some notifications with some issues on project1
Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(
randomIssues(t -> t.setProject(projectKey1).setNewIssueStatus(newIssueStatus)).collect(toSet()),
streamOfIssues(t -> t.setProject(projectKey1).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
changeMock));
// some notifications with some issues on project2
Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(
randomIssues(t -> t.setProject(projectKey2).setNewIssueStatus(newIssueStatus)).collect(toSet()),
streamOfIssues(t -> t.setProject(projectKey2).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
changeMock));
// some notifications with some issues on project3 and project 4
Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(
Stream.concat(
randomIssues(t -> t.setProject(projectKey3).setNewIssueStatus(newIssueStatus)),
randomIssues(t -> t.setProject(projectKey4).setNewIssueStatus(newIssueStatus)))
streamOfIssues(t -> t.setProject(projectKey3).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)),
streamOfIssues(t -> t.setProject(projectKey4).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)))
.collect(toSet()),
changeMock));
when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -250,40 +271,40 @@ public class FPOrAcceptedNotificationHandlerTest {
User otherChangeAuthor = newUser("otherChangeAuthor");

// subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 only
Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(
randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)).collect(toSet()),
streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)).collect(toSet()),
newUserChange(subscriber1)))
.collect(toSet());
// subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 and subscriber2
Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(
Stream.concat(
randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)),
randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber1)))
streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)),
streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
.collect(toSet()),
newUserChange(subscriber1)))
.collect(toSet());
// subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 only
Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(
randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)).collect(toSet()),
streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)).collect(toSet()),
newUserChange(subscriber2)))
.collect(toSet());
// subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 and subscriber 3
Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(
Stream.concat(
randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)),
randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber3)))
streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)),
streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber3)))
.collect(toSet()),
newUserChange(subscriber2)))
.collect(toSet());
// subscriber3 is the changeAuthor of no notification
// otherChangeAuthor has some notifications
Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 1 + new Random().nextInt(2))
.mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setProject(project)
.setNewIssueStatus(newIssueStatus)).collect(toSet()),
Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 5)
.mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setProject(project)
.setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
newUserChange(otherChangeAuthor)))
.collect(toSet());
when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -292,7 +313,7 @@ public class FPOrAcceptedNotificationHandlerTest {
when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
.thenReturn(subscriberLogins.stream().map(FPOrAcceptedNotificationHandlerTest::emailRecipientOf).collect(toSet()));

int deliveredCount = new Random().nextInt(200);
int deliveredCount = 200;
when(emailNotificationChannel.deliverAll(anySet()))
.thenReturn(deliveredCount)
.thenThrow(new IllegalStateException("deliver should be called only once"));
@@ -368,12 +389,12 @@ public class FPOrAcceptedNotificationHandlerTest {
User changeAuthor = newUser("changeAuthor");

Set<ChangedIssue> fpIssues = projects.stream()
.flatMap(project -> randomIssues(t -> t.setProject(project)
.setNewIssueStatus(IssueStatus.FALSE_POSITIVE).setAssignee(subscriber1)))
.flatMap(project -> streamOfIssues(t -> t.setProject(project)
.setNewIssueStatus(IssueStatus.FALSE_POSITIVE).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
.collect(toSet());
Set<ChangedIssue> wontFixIssues = projects.stream()
.flatMap(project -> randomIssues(t -> t.setProject(project)
.setNewIssueStatus(IssueStatus.ACCEPTED).setAssignee(subscriber1)))
.flatMap(project -> streamOfIssues(t -> t.setProject(project)
.setNewIssueStatus(IssueStatus.ACCEPTED).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
.collect(toSet());
UserChange userChange = newUserChange(changeAuthor);
IssuesChangesNotificationBuilder fpAndWontFixNotifications = new IssuesChangesNotificationBuilder(
@@ -383,7 +404,7 @@ public class FPOrAcceptedNotificationHandlerTest {
projects.forEach(project -> when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
.thenReturn(singleton(emailRecipientOf(subscriber1.getLogin()))));

int deliveredCount = new Random().nextInt(200);
int deliveredCount = 200;
when(emailNotificationChannel.deliverAll(anySet()))
.thenReturn(deliveredCount)
.thenThrow(new IllegalStateException("deliver should be called only once"));
@@ -414,7 +435,7 @@ public class FPOrAcceptedNotificationHandlerTest {
public static Object[][] oneOrMoreProjectCounts() {
return new Object[][] {
{1},
{2 + new Random().nextInt(3)},
{5},
};
}

@@ -452,8 +473,8 @@ public class FPOrAcceptedNotificationHandlerTest {
};
}

private static Stream<ChangedIssue> randomIssues(Consumer<ChangedIssue.Builder> consumer) {
return IntStream.range(0, 1 + new Random().nextInt(5))
private static Stream<ChangedIssue> streamOfIssues(Consumer<ChangedIssue.Builder> consumer) {
return IntStream.range(0, 5)
.mapToObj(i -> {
ChangedIssue.Builder builder = new ChangedIssue.Builder("key_" + i)
.setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))

+ 1
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/AddCommentActionIT.java View File

@@ -242,6 +242,7 @@ public class AddCommentActionIT {
verify(issueFieldsSetter).addComment(defaultIssueCaptor.capture(), eq(comment), eq(issueChangeContext));
verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
any(DbSession.class),
any(IssueDto.class),
defaultIssueCaptor.capture(),
eq(issueChangeContext));


+ 1
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/AssignActionIT.java View File

@@ -566,6 +566,7 @@ public class AssignActionIT {
verify(issueFieldsSetter).assign(defaultIssueCaptor.capture(), userMatcher(assignee), any(IssueChangeContext.class));
verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
any(DbSession.class),
any(IssueDto.class),
defaultIssueCaptor.capture(),
any(IssueChangeContext.class));


+ 3
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ChangeStatusActionIT.java View File

@@ -441,6 +441,7 @@ public class ChangeStatusActionIT {
if (transitionDone) {
verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
any(DbSession.class),
any(IssueDto.class),
defaultIssueCaptor.capture(),
eq(issueChangeContext));

@@ -546,6 +547,7 @@ public class ChangeStatusActionIT {
if (transitionDone) {
verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
any(DbSession.class),
any(IssueDto.class),
defaultIssueCaptor.capture(),
eq(issueChangeContext));

@@ -595,6 +597,7 @@ public class ChangeStatusActionIT {
verify(issueFieldsSetter).addComment(defaultIssueCaptor.capture(), eq(comment), eq(issueChangeContext));
verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
any(DbSession.class),
any(IssueDto.class),
defaultIssueCaptor.capture(),
eq(issueChangeContext));


+ 5
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/BulkChangeActionIT.java View File

@@ -32,6 +32,7 @@ import org.sonar.api.impl.utils.TestSystem2;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.core.issue.status.IssueStatus;
import org.sonar.core.util.SequenceUuidFactory;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
@@ -418,6 +419,8 @@ public class BulkChangeActionIT {
ChangedIssue changedIssue = builder.getIssues().iterator().next();
assertThat(changedIssue.getKey()).isEqualTo(issue.getKey());
assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_CONFIRMED);
assertThat(changedIssue.getNewIssueStatus()).contains(IssueStatus.CONFIRMED);
assertThat(changedIssue.getOldIssueStatus()).contains(IssueStatus.OPEN);
assertThat(changedIssue.getAssignee()).isEmpty();
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
assertThat(changedIssue.getProject()).isEqualTo(projectBranchOf(db, branch));
@@ -484,6 +487,8 @@ public class BulkChangeActionIT {
ChangedIssue changedIssue = builder.getIssues().iterator().next();
assertThat(changedIssue.getKey()).isEqualTo(issue3.getKey());
assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_OPEN);
assertThat(changedIssue.getNewIssueStatus()).contains(IssueStatus.OPEN);
assertThat(changedIssue.getOldIssueStatus()).contains(IssueStatus.OPEN);
assertThat(changedIssue.getAssignee()).isEmpty();
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
assertThat(changedIssue.getProject()).isEqualTo(projectOf(project));

+ 36
- 25
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueUpdaterIT.java View File

@@ -55,6 +55,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
import static org.sonar.api.rule.Severity.BLOCKER;
import static org.sonar.api.rule.Severity.MAJOR;
import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByUserBuilder;
@@ -90,12 +91,13 @@ public class IssueUpdaterIT {

@Test
public void update_issue() {
DefaultIssue issue = db.issues().insertIssue(i -> i.setSeverity(MAJOR)).toDefaultIssue();
IssueDto originalIssueDto = db.issues().insertIssue(i -> i.setSeverity(MAJOR));
DefaultIssue issue = originalIssueDto.toDefaultIssue();
UserDto user = db.users().insertUser();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), user.getUuid()).build();
issueFieldsSetter.setSeverity(issue, BLOCKER, context);

underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

IssueDto issueReloaded = dbClient.issueDao().selectByKey(db.getSession(), issue.key()).get();
assertThat(issueReloaded.getSeverity()).isEqualTo(BLOCKER);
@@ -107,14 +109,15 @@ public class IssueUpdaterIT {
RuleDto rule = db.rules().insertIssueRule();
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
DefaultIssue issue = db.issues().insertIssue(rule, project, file,
t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()))
IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file,
t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()));
DefaultIssue issue = originalIssueDto
.toDefaultIssue();
UserDto changeAuthor = db.users().insertUser();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
issueFieldsSetter.setSeverity(issue, BLOCKER, context);

underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -135,14 +138,16 @@ public class IssueUpdaterIT {
RuleDto rule = db.rules().insertIssueRule();
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
DefaultIssue issue = db.issues().insertIssue(rule, project, file,
t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()))
IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file,
t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()));
DefaultIssue issue = originalIssueDto
.toDefaultIssue();
UserDto changeAuthor = db.users().insertUser();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
issueFieldsSetter.setResolution(issue, RESOLUTION_FIXED, context);
issueFieldsSetter.setStatus(issue, STATUS_RESOLVED, context);

underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -151,6 +156,8 @@ public class IssueUpdaterIT {
ChangedIssue changedIssue = builder.getIssues().iterator().next();
assertThat(changedIssue.getKey()).isEqualTo(issue.key());
assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status());
assertThat(changedIssue.getOldIssueStatus()).contains(originalIssueDto.getIssueStatus());
assertThat(changedIssue.getNewIssueStatus()).contains(issue.getIssueStatus());
assertThat(changedIssue.getAssignee()).contains(userOf(assignee));
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
assertThat(changedIssue.getProject()).isEqualTo(projectOf(project));
@@ -163,13 +170,14 @@ public class IssueUpdaterIT {
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto branch = db.components().insertProjectBranch(project, t -> t.setBranchType(BRANCH));
ComponentDto file = db.components().insertComponent(newFileDto(branch, project.uuid()));
DefaultIssue issue = db.issues().insertIssue(rule, branch, file,
t -> t.setSeverity(MAJOR)).toDefaultIssue();
IssueDto originalIssueDto = db.issues().insertIssue(rule, branch, file,
t -> t.setSeverity(MAJOR));
DefaultIssue issue = originalIssueDto.toDefaultIssue();
UserDto changeAuthor = db.users().insertUser();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
issueFieldsSetter.setSeverity(issue, BLOCKER, context);

underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -190,11 +198,12 @@ public class IssueUpdaterIT {
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto branch = db.components().insertProjectBranch(project, t -> t.setBranchType(BranchType.PULL_REQUEST));
ComponentDto file = db.components().insertComponent(newFileDto(branch, project.uuid()));
DefaultIssue issue = db.issues().insertIssue(rule, branch, file, t -> t.setSeverity(MAJOR)).toDefaultIssue();
IssueDto originalIssueDto = db.issues().insertIssue(rule, branch, file, t -> t.setSeverity(MAJOR));
DefaultIssue issue = originalIssueDto.toDefaultIssue();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), "user_uuid").build();
issueFieldsSetter.setSeverity(issue, BLOCKER, context);

underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

verifyNoInteractions(notificationManager);
}
@@ -204,11 +213,12 @@ public class IssueUpdaterIT {
RuleDto rule = db.rules().insertIssueRule(r -> r.setStatus(RuleStatus.REMOVED));
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
DefaultIssue issue = db.issues().insertIssue(rule, project, file, t -> t.setSeverity(MAJOR)).toDefaultIssue();
IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file, t -> t.setSeverity(MAJOR));
DefaultIssue issue = originalIssueDto.toDefaultIssue();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), "user_uuid").build();
issueFieldsSetter.setSeverity(issue, BLOCKER, context);

underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

verifyNoInteractions(notificationManager);
}
@@ -219,14 +229,15 @@ public class IssueUpdaterIT {
RuleDto rule = db.rules().insertIssueRule();
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
DefaultIssue issue = db.issues().insertIssue(rule, project, file, t -> t.setAssigneeUuid(oldAssignee.getUuid()))
IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file, t -> t.setAssigneeUuid(oldAssignee.getUuid()));
DefaultIssue issue = originalIssueDto
.toDefaultIssue();
UserDto changeAuthor = db.users().insertUser();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
UserDto newAssignee = db.users().insertUser();
issueFieldsSetter.assign(issue, newAssignee, context);

underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -246,18 +257,18 @@ public class IssueUpdaterIT {
RuleDto rule = db.rules().insertIssueRule();
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
IssueDto issueDto = db.issues().insertIssue(rule, project, file);
DefaultIssue issue = issueDto.setSeverity(MAJOR).toDefaultIssue();
IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file);
DefaultIssue issue = originalIssueDto.setSeverity(MAJOR).toDefaultIssue();
UserDto changeAuthor = db.users().insertUser();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).withRefreshMeasures().build();
issueFieldsSetter.setSeverity(issue, BLOCKER, context);

SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

assertThat(preloadedSearchResponseData.getIssues())
.hasSize(1);
assertThat(preloadedSearchResponseData.getIssues().iterator().next())
.isNotSameAs(issueDto);
.isNotSameAs(originalIssueDto);
assertThat(preloadedSearchResponseData.getRules())
.extracting(RuleDto::getKey)
.containsOnly(rule.getKey());
@@ -272,17 +283,17 @@ public class IssueUpdaterIT {
RuleDto rule = db.rules().insertIssueRule(r -> r.setStatus(RuleStatus.REMOVED));
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = db.components().insertComponent(newFileDto(project));
IssueDto issueDto = db.issues().insertIssue(rule, project, file);
DefaultIssue issue = issueDto.setSeverity(MAJOR).toDefaultIssue();
IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file);
DefaultIssue issue = originalIssueDto.setSeverity(MAJOR).toDefaultIssue();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), "user_uuid").build();
issueFieldsSetter.setSeverity(issue, BLOCKER, context);

SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);

assertThat(preloadedSearchResponseData.getIssues())
.hasSize(1);
assertThat(preloadedSearchResponseData.getIssues().iterator().next())
.isNotSameAs(issueDto);
.isNotSameAs(originalIssueDto);
assertThat(preloadedSearchResponseData.getRules()).isNullOrEmpty();
assertThat(preloadedSearchResponseData.getComponents())
.extracting(ComponentDto::uuid)

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java View File

@@ -85,7 +85,7 @@ public class AddCommentAction implements HotspotsWsAction {
DefaultIssue defaultIssue = hotspot.toDefaultIssue();
IssueChangeContext context = hotspotWsSupport.newIssueChangeContextWithoutMeasureRefresh();
issueFieldsSetter.addComment(defaultIssue, comment, context);
issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context);
issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, hotspot, defaultIssue, context);
response.noContent();
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java View File

@@ -126,7 +126,7 @@ public class AssignAction implements HotspotsWsAction {
}

if (issueFieldsSetter.assign(defaultIssue, assignee, context)) {
issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context);
issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, hotspotDto, defaultIssue, context);

BranchDto branch = issueUpdater.getBranch(dbSession, defaultIssue);
if (BRANCH.equals(branch.getBranchType())) {

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java View File

@@ -162,7 +162,7 @@ public class ChangeStatusAction implements HotspotsWsAction {
issueFieldsSetter.addComment(defaultIssue, comment, context);
}

issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context);
issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, defaultIssue, context);

BranchDto branch = issueUpdater.getBranch(session, defaultIssue);
if (BRANCH.equals(branch.getBranchType())) {

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java View File

@@ -103,7 +103,7 @@ public class AddCommentAction implements IssuesWsAction {
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).build();
DefaultIssue defaultIssue = issueDto.toDefaultIssue();
issueFieldsSetter.addComment(defaultIssue, wsRequest.getText(), context);
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context);
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issueDto, defaultIssue, context);
responseWriter.write(defaultIssue.key(), preloadedSearchResponseData, request, response);
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.java View File

@@ -111,7 +111,7 @@ public class AssignAction implements IssuesWsAction {
UserDto user = getUser(dbSession, login);
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).build();
if (issueFieldsSetter.assign(issue, user, context)) {
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context);
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issueDto, issue, context);
}
return new SearchResponseData(issueDto);
}

+ 17
- 8
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java View File

@@ -47,7 +47,6 @@ import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.issue.status.IssueStatus;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
@@ -152,7 +151,7 @@ public class BulkChangeAction implements IssuesWsAction {
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction(ACTION_BULK_CHANGE)
.setDescription("Bulk change on issues. Up to 500 issues can be updated. <br/>" +
"Requires authentication.")
"Requires authentication.")
.setSince("3.7")
.setChangelog(
new Change("10.4", ("Transitions '%s' and '%s' are now deprecated. Use transition '%s' instead. " +
@@ -196,7 +195,7 @@ public class BulkChangeAction implements IssuesWsAction {
.setExampleValue("security,java8");
action.createParam(PARAM_COMMENT)
.setDescription("Add a comment. "
+ "The comment will only be added to issues that are affected either by a change of type or a change of severity as a result of the same WS call.")
+ "The comment will only be added to issues that are affected either by a change of type or a change of severity as a result of the same WS call.")
.setExampleValue("Here is my comment");
action.createParam(PARAM_SEND_NOTIFICATIONS)
.setSince("4.0")
@@ -334,11 +333,13 @@ public class BulkChangeAction implements IssuesWsAction {
if (ruleDefinitionDto == null || projectDto == null) {
return null;
}
IssueDto oldIssueDto = bulkChangeData.originalIssueByKey.get(issue.key());

Optional<UserDto> assignee = Optional.ofNullable(issue.assignee()).map(userDtoByUuid::get);
return new ChangedIssue.Builder(issue.key())
.setNewStatus(issue.status())
.setNewIssueStatus(IssueStatus.of(issue.status(), issue.resolution()))
.setNewIssueStatus(issue.getIssueStatus())
.setOldIssueStatus(oldIssueDto.getIssueStatus())
.setAssignee(assignee.map(u -> new User(u.getUuid(), u.getLogin(), u.getName())).orElse(null))
.setRule(new IssuesChangesNotificationBuilder.Rule(ruleDefinitionDto.getKey(), RuleType.valueOfNullable(ruleDefinitionDto.getType()), ruleDefinitionDto.getName()))
.setProject(new Project.Builder(projectDto.uuid())
@@ -382,6 +383,7 @@ public class BulkChangeAction implements IssuesWsAction {
private final Map<String, ComponentDto> componentsByUuid;
private final Map<RuleKey, RuleDto> rulesByKey;
private final List<Action> availableActions;
private final Map<String, IssueDto> originalIssueByKey;

BulkChangeData(DbSession dbSession, Request request) {
this.sendNotification = request.mandatoryParamAsBoolean(PARAM_SEND_NOTIFICATIONS);
@@ -398,12 +400,14 @@ public class BulkChangeAction implements IssuesWsAction {
this.branchComponentByUuid = getAuthorizedComponents(allBranches).stream().collect(toMap(ComponentDto::uuid, identity()));
this.branchesByProjectUuid = dbClient.branchDao().selectByUuids(dbSession, branchComponentByUuid.keySet()).stream()
.collect(toMap(BranchDto::getUuid, identity()));
this.issues = getAuthorizedIssues(allIssues);
List<IssueDto> authorizedIssues = getAuthorizedIssues(allIssues);
this.originalIssueByKey = authorizedIssues.stream().collect(toMap(IssueDto::getKee, identity()));
this.issues = toDefaultIssues(authorizedIssues);
this.componentsByUuid = getComponents(dbSession,
issues.stream().map(DefaultIssue::componentUuid).collect(Collectors.toSet())).stream()
.collect(toMap(ComponentDto::uuid, identity()));
.collect(toMap(ComponentDto::uuid, identity()));
this.rulesByKey = dbClient.ruleDao().selectByKeys(dbSession,
issues.stream().map(DefaultIssue::ruleKey).collect(Collectors.toSet())).stream()
issues.stream().map(DefaultIssue::ruleKey).collect(Collectors.toSet())).stream()
.collect(toMap(RuleDto::getKey, identity()));

this.availableActions = actions.stream()
@@ -420,10 +424,15 @@ public class BulkChangeAction implements IssuesWsAction {
return userSession.keepAuthorizedComponents(UserRole.USER, projectDtos);
}

private List<DefaultIssue> getAuthorizedIssues(List<IssueDto> allIssues) {
private List<IssueDto> getAuthorizedIssues(List<IssueDto> allIssues) {
Set<String> branchUuids = branchComponentByUuid.values().stream().map(ComponentDto::uuid).collect(Collectors.toSet());
return allIssues.stream()
.filter(issue -> branchUuids.contains(issue.getProjectUuid()))
.toList();
}

private List<DefaultIssue> toDefaultIssues(List<IssueDto> allIssues) {
return allIssues.stream()
.map(IssueDto::toDefaultIssue)
.toList();
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java View File

@@ -128,7 +128,7 @@ public class DoTransitionAction implements IssuesWsAction {
transitionService.checkTransitionPermission(transitionKey, defaultIssue);
if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
BranchDto branch = issueUpdater.getBranch(session, defaultIssue);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, branch);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, defaultIssue, context, branch);

if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(defaultIssue.projectUuid()) != null) {
issueChangeEventService.distributeIssueChangeEvent(defaultIssue, null, null, transitionKey, branch,

+ 16
- 14
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java View File

@@ -71,16 +71,17 @@ public class IssueUpdater {
this.notificationSerializer = notificationSerializer;
}

public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context) {
BranchDto branch = getBranch(dbSession, issue);
return saveIssueAndPreloadSearchResponseData(dbSession, issue, context, branch);
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, IssueDto originalIssue, DefaultIssue newIssue, IssueChangeContext context) {
BranchDto branch = getBranch(dbSession, newIssue);
return saveIssueAndPreloadSearchResponseData(dbSession, originalIssue, newIssue, context, branch);
}

public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, BranchDto branch) {
Optional<RuleDto> rule = getRuleByKey(dbSession, issue.getRuleKey());
ComponentDto branchComponent = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid());
ComponentDto component = getComponent(dbSession, issue, issue.componentUuid());
IssueDto issueDto = doSaveIssue(dbSession, issue, context, rule.orElse(null), branchComponent, branch);
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, IssueDto originalIssue,
DefaultIssue newIssue, IssueChangeContext context, BranchDto branch) {
Optional<RuleDto> rule = getRuleByKey(dbSession, newIssue.getRuleKey());
ComponentDto branchComponent = dbClient.componentDao().selectOrFailByUuid(dbSession, newIssue.projectUuid());
ComponentDto component = getComponent(dbSession, newIssue, newIssue.componentUuid());
IssueDto issueDto = doSaveIssue(dbSession, originalIssue, newIssue, context, rule.orElse(null), branchComponent, branch);

SearchResponseData result = new SearchResponseData(issueDto);
rule.ifPresent(r -> result.addRules(singletonList(r)));
@@ -107,18 +108,18 @@ public class IssueUpdater {
return branchDto;
}

private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context,
private IssueDto doSaveIssue(DbSession session, IssueDto originalIssue, DefaultIssue issue, IssueChangeContext context,
@Nullable RuleDto ruleDto, ComponentDto project, BranchDto branchDto) {
IssueDto issueDto = issueStorage.save(session, singletonList(issue)).iterator().next();
Date updateDate = issue.updateDate();
if (
// since this method is called after an update of the issue, date should never be null
updateDate == null
// name of rule is displayed in notification, rule must therefor be present
|| ruleDto == null
// notification are not supported on PRs
|| !hasNotificationSupport(branchDto)
|| context.getWebhookSource() != null) {
// name of rule is displayed in notification, rule must therefor be present
|| ruleDto == null
// notification are not supported on PRs
|| !hasNotificationSupport(branchDto)
|| context.getWebhookSource() != null) {
return issueDto;
}

@@ -131,6 +132,7 @@ public class IssueUpdater {
new ChangedIssue.Builder(issue.key())
.setNewStatus(issue.status())
.setNewIssueStatus(IssueStatus.of(issue.status(), issue.resolution()))
.setOldIssueStatus(originalIssue.getIssueStatus())
.setAssignee(assignee.map(assigneeDto -> new User(assigneeDto.getUuid(), assigneeDto.getLogin(), assigneeDto.getName())).orElse(null))
.setRule(new Rule(ruleDto.getKey(), RuleType.valueOfNullable(ruleDto.getType()), ruleDto.getName()))
.setProject(new Project.Builder(project.uuid())

+ 7
- 7
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java View File

@@ -71,12 +71,12 @@ public class SetSeverityAction implements IssuesWsAction {
public void define(WebService.NewController controller) {
WebService.NewAction action = controller.createAction(ACTION_SET_SEVERITY)
.setDescription("Change severity.<br/>" +
"Requires the following permissions:" +
"<ul>" +
" <li>'Authentication'</li>" +
" <li>'Browse' rights on project of the specified issue</li>" +
" <li>'Administer Issues' rights on project of the specified issue</li>" +
"</ul>")
"Requires the following permissions:" +
"<ul>" +
" <li>'Authentication'</li>" +
" <li>'Browse' rights on project of the specified issue</li>" +
" <li>'Administer Issues' rights on project of the specified issue</li>" +
"</ul>")
.setSince("3.6")
.setChangelog(
new Change("10.4", "The response fields 'status' and 'resolution' are deprecated. Please use 'issueStatus' instead."),
@@ -121,7 +121,7 @@ public class SetSeverityAction implements IssuesWsAction {
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), userSession.getUuid()).withRefreshMeasures().build();
if (issueFieldsSetter.setManualSeverity(issue, severity, context)) {
BranchDto branch = issueUpdater.getBranch(session, issue);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, branch);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, issue, context, branch);

if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
issueChangeEventService.distributeIssueChangeEvent(issue, severity, null, null,

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java View File

@@ -109,7 +109,7 @@ public class SetTagsAction implements IssuesWsAction {
DefaultIssue issue = issueDto.toDefaultIssue();
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), userSession.getUuid()).build();
if (issueFieldsSetter.setTags(issue, tags, context)) {
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context);
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, issue, context);
}
return new SearchResponseData(issueDto);
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java View File

@@ -126,7 +126,7 @@ public class SetTypeAction implements IssuesWsAction {
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).withRefreshMeasures().build();
if (issueFieldsSetter.setType(issue, ruleType, context)) {
BranchDto branch = issueUpdater.getBranch(session, issue);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, branch);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, issue, context, branch);
if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
issueChangeEventService.distributeIssueChangeEvent(issue, null, ruleType.name(), null, branch,
response.getComponentByUuid(issue.projectUuid()).getKey());

Loading…
Cancel
Save