]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12719 add changelog to response of WS api/hotspots/show
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 6 Dec 2019 16:08:53 +0000 (17:08 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:27 +0000 (20:46 +0100)
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
sonar-ws/src/main/protobuf/ws-hotspots.proto

index 5aa202080aec69755a3c81e3557fb4ae8c7e1094..083cf8edc54fd2684b5a82886bf722bcc0681465 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.server.hotspot.ws;
 
+import com.google.common.collect.ImmutableSet;
 import java.util.Objects;
+import java.util.Set;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.Request;
@@ -33,6 +35,8 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.IssueChangelog;
+import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext;
 import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.security.SecurityStandards;
 import org.sonar.server.user.UserSession;
@@ -54,12 +58,15 @@ public class ShowAction implements HotspotsWsAction {
   private final UserSession userSession;
   private final HotspotWsResponseFormatter responseFormatter;
   private final TextRangeResponseFormatter textRangeFormatter;
+  private final IssueChangelog issueChangelog;
 
-  public ShowAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter) {
+  public ShowAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter,
+    TextRangeResponseFormatter textRangeFormatter, IssueChangelog issueChangelog) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.responseFormatter = responseFormatter;
     this.textRangeFormatter = textRangeFormatter;
+    this.issueChangelog = issueChangelog;
   }
 
   @Override
@@ -95,6 +102,7 @@ public class ShowAction implements HotspotsWsAction {
       formatComponents(components, responseBuilder);
       formatRule(responseBuilder, rule);
       formatTextRange(hotspot, responseBuilder);
+      formatChangelog(dbSession, hotspot, components, responseBuilder);
 
       writeProtobuf(responseBuilder.build(), request, response);
     }
@@ -140,6 +148,15 @@ public class ShowAction implements HotspotsWsAction {
     textRangeFormatter.formatTextRange(hotspot, responseBuilder::setTextRange);
   }
 
+  private void formatChangelog(DbSession dbSession, IssueDto hotspot, Components components, ShowWsResponse.Builder responseBuilder) {
+    Set<ComponentDto> preloadedComponents = ImmutableSet.of(components.project, components.component);
+    ChangelogLoadingContext changelogLoadingContext = issueChangelog
+      .newChangelogLoadingContext(dbSession, hotspot, ImmutableSet.of(), preloadedComponents);
+
+    issueChangelog.formatChangelog(dbSession, changelogLoadingContext)
+      .forEach(responseBuilder::addChangelog);
+  }
+
   private RuleDefinitionDto loadRule(DbSession dbSession, IssueDto hotspot) {
     RuleKey ruleKey = hotspot.getRuleKey();
     return dbClient.ruleDao().selectDefinitionByKey(dbSession, ruleKey)
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java
new file mode 100644 (file)
index 0000000..56f0e70
--- /dev/null
@@ -0,0 +1,237 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.core.issue.FieldDiffs;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.user.UserDto;
+import org.sonarqube.ws.Common;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static java.util.Collections.emptyMap;
+import static java.util.Optional.ofNullable;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.server.issue.IssueFieldsSetter.FILE;
+import static org.sonar.server.issue.IssueFieldsSetter.TECHNICAL_DEBT;
+
+public class IssueChangelog {
+  private static final String EFFORT_CHANGELOG_KEY = "effort";
+
+  private final DbClient dbClient;
+  private final AvatarResolver avatarFactory;
+
+  public IssueChangelog(DbClient dbClient, AvatarResolver avatarFactory) {
+    this.dbClient = dbClient;
+    this.avatarFactory = avatarFactory;
+  }
+
+  public ChangelogLoadingContext newChangelogLoadingContext(DbSession dbSession, IssueDto dto) {
+    return newChangelogLoadingContext(dbSession, dto, ImmutableSet.of(), ImmutableSet.of());
+  }
+
+  public ChangelogLoadingContext newChangelogLoadingContext(DbSession dbSession, IssueDto dto, Set<UserDto> preloadedUsers, Set<ComponentDto> preloadedComponents) {
+    List<FieldDiffs> changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, dto.getKey());
+    return new ChangelogLoadingContextImpl(changes, preloadedUsers, preloadedComponents);
+  }
+
+  public Stream<Common.Changelog> formatChangelog(DbSession dbSession, ChangelogLoadingContext loadingContext) {
+    Map<String, UserDto> usersByUuid = loadUsers(dbSession, loadingContext);
+    Map<String, ComponentDto> filesByUuid = loadFiles(dbSession, loadingContext);
+    FormatableChangeLog changeLogResults = new FormatableChangeLog(loadingContext.getChanges(), usersByUuid, filesByUuid);
+
+    return changeLogResults.changes.stream()
+      .map(toWsChangelog(changeLogResults));
+  }
+
+  private Map<String, UserDto> loadUsers(DbSession dbSession, ChangelogLoadingContext loadingContext) {
+    List<FieldDiffs> changes = loadingContext.getChanges();
+    if (changes.isEmpty()) {
+      return emptyMap();
+    }
+
+    Set<UserDto> usersByUuid = loadingContext.getPreloadedUsers();
+
+    Set<String> userUuids = changes.stream()
+      .filter(change -> change.userUuid() != null)
+      .map(FieldDiffs::userUuid)
+      .collect(toSet());
+    if (userUuids.isEmpty()) {
+      return emptyMap();
+    }
+
+    Set<String> missingUsersUuids = Sets.difference(userUuids, usersByUuid).immutableCopy();
+    if (missingUsersUuids.isEmpty()) {
+      return usersByUuid.stream()
+        .filter(t -> userUuids.contains(t.getUuid()))
+        .collect(uniqueIndex(UserDto::getUuid, userUuids.size()));
+    }
+
+    return Stream.concat(
+      usersByUuid.stream(),
+      dbClient.userDao().selectByUuids(dbSession, missingUsersUuids).stream())
+      .filter(t -> userUuids.contains(t.getUuid()))
+      .collect(uniqueIndex(UserDto::getUuid, userUuids.size()));
+  }
+
+  private Map<String, ComponentDto> loadFiles(DbSession dbSession, ChangelogLoadingContext loadingContext) {
+    List<FieldDiffs> changes = loadingContext.getChanges();
+    if (changes.isEmpty()) {
+      return emptyMap();
+    }
+
+    Set<String> fileUuids = changes.stream()
+      .filter(diffs -> diffs.diffs().containsKey(FILE))
+      .flatMap(diffs -> Stream.of(diffs.get(FILE).newValue().toString(), diffs.get(FILE).oldValue().toString()))
+      .collect(toSet());
+    if (fileUuids.isEmpty()) {
+      return emptyMap();
+    }
+
+    Set<ComponentDto> preloadedComponents = loadingContext.getPreloadedComponents();
+    Set<String> preloadedComponentUuids = preloadedComponents.stream()
+      .map(ComponentDto::uuid)
+      .collect(toSet(preloadedComponents.size()));
+    Set<String> missingFileUuids = Sets.difference(fileUuids, preloadedComponentUuids).immutableCopy();
+    if (missingFileUuids.isEmpty()) {
+      return preloadedComponents.stream()
+        .filter(t -> fileUuids.contains(t.uuid()))
+        .collect(uniqueIndex(ComponentDto::uuid, fileUuids.size()));
+    }
+
+    return Stream.concat(
+      preloadedComponents.stream(),
+      dbClient.componentDao().selectByUuids(dbSession, missingFileUuids).stream())
+      .filter(t -> fileUuids.contains(t.uuid()))
+      .collect(uniqueIndex(ComponentDto::uuid, fileUuids.size()));
+  }
+
+  public interface ChangelogLoadingContext {
+    List<FieldDiffs> getChanges();
+
+    Set<UserDto> getPreloadedUsers();
+
+    Set<ComponentDto> getPreloadedComponents();
+  }
+
+  @Immutable
+  public static final class ChangelogLoadingContextImpl implements ChangelogLoadingContext {
+    private final List<FieldDiffs> changes;
+    private final Set<UserDto> preloadedUsers;
+    private final Set<ComponentDto> preloadedComponents;
+
+    private ChangelogLoadingContextImpl(List<FieldDiffs> changes, Set<UserDto> preloadedUsers, Set<ComponentDto> preloadedComponents) {
+      this.changes = ImmutableList.copyOf(changes);
+      this.preloadedUsers = ImmutableSet.copyOf(preloadedUsers);
+      this.preloadedComponents = ImmutableSet.copyOf(preloadedComponents);
+    }
+
+    @Override
+    public List<FieldDiffs> getChanges() {
+      return changes;
+    }
+
+    @Override
+    public Set<UserDto> getPreloadedUsers() {
+      return preloadedUsers;
+    }
+
+    @Override
+    public Set<ComponentDto> getPreloadedComponents() {
+      return preloadedComponents;
+    }
+  }
+
+  private Function<FieldDiffs, Common.Changelog> toWsChangelog(FormatableChangeLog results) {
+    return change -> {
+      String userUUuid = change.userUuid();
+      Common.Changelog.Builder changelogBuilder = Common.Changelog.newBuilder();
+      changelogBuilder.setCreationDate(formatDateTime(change.creationDate()));
+      UserDto user = userUUuid == null ? null : results.users.get(userUUuid);
+      if (user != null) {
+        changelogBuilder.setUser(user.getLogin());
+        changelogBuilder.setIsUserActive(user.isActive());
+        ofNullable(user.getName()).ifPresent(changelogBuilder::setUserName);
+        ofNullable(emptyToNull(user.getEmail())).ifPresent(email -> changelogBuilder.setAvatar(avatarFactory.create(user)));
+      }
+      change.diffs().entrySet().stream()
+        .map(toWsDiff(results))
+        .forEach(changelogBuilder::addDiffs);
+      return changelogBuilder.build();
+    };
+  }
+
+  private static Function<Map.Entry<String, FieldDiffs.Diff>, Common.Changelog.Diff> toWsDiff(FormatableChangeLog results) {
+    return diff -> {
+      FieldDiffs.Diff value = diff.getValue();
+      Common.Changelog.Diff.Builder diffBuilder = Common.Changelog.Diff.newBuilder();
+      String key = diff.getKey();
+      String oldValue = value.oldValue() != null ? value.oldValue().toString() : null;
+      String newValue = value.newValue() != null ? value.newValue().toString() : null;
+      if (key.equals(FILE)) {
+        diffBuilder.setKey(key);
+        ofNullable(results.getFileLongName(emptyToNull(newValue))).ifPresent(diffBuilder::setNewValue);
+        ofNullable(results.getFileLongName(emptyToNull(oldValue))).ifPresent(diffBuilder::setOldValue);
+      } else {
+        diffBuilder.setKey(key.equals(TECHNICAL_DEBT) ? EFFORT_CHANGELOG_KEY : key);
+        ofNullable(emptyToNull(newValue)).ifPresent(diffBuilder::setNewValue);
+        ofNullable(emptyToNull(oldValue)).ifPresent(diffBuilder::setOldValue);
+      }
+      return diffBuilder.build();
+    };
+  }
+
+  private static final class FormatableChangeLog {
+    private final List<FieldDiffs> changes;
+    private final Map<String, UserDto> users;
+    private final Map<String, ComponentDto> files;
+
+    private FormatableChangeLog(List<FieldDiffs> changes, Map<String, UserDto> users, Map<String, ComponentDto> files) {
+      this.changes = changes;
+      this.users = users;
+      this.files = files;
+    }
+
+    @CheckForNull
+    String getFileLongName(@Nullable String fileUuid) {
+      if (fileUuid == null) {
+        return null;
+      }
+      ComponentDto file = files.get(fileUuid);
+      return file == null ? null : file.longName();
+    }
+
+  }
+}
index 675da21d846ef57efb1eb1a947602cd20c1fd515..ff6246b5149dedcf58c768b33cca0eb129af6baa 100644 (file)
  */
 package org.sonar.server.issue.ws;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.io.Resources;
-import java.util.List;
-import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Stream;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.core.issue.FieldDiffs;
-import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.issue.AvatarResolver;
+import org.sonar.server.issue.IssueChangelog;
 import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.user.UserSession;
-import org.sonarqube.ws.Common.Changelog;
 import org.sonarqube.ws.Issues.ChangelogWsResponse;
 
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.base.Strings.emptyToNull;
-import static java.util.Optional.ofNullable;
-import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
-import static org.sonar.server.issue.IssueFieldsSetter.FILE;
-import static org.sonar.server.issue.IssueFieldsSetter.TECHNICAL_DEBT;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_CHANGELOG;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
 
 public class ChangelogAction implements IssuesWsAction {
 
-  private static final String EFFORT_CHANGELOG_KEY = "effort";
-
   private final DbClient dbClient;
   private final IssueFinder issueFinder;
-  private final AvatarResolver avatarFactory;
   private final UserSession userSession;
+  private final IssueChangelog issueChangelog;
 
-  public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, AvatarResolver avatarFactory, UserSession userSession) {
+  public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, UserSession userSession, IssueChangelog issueChangelog) {
     this.dbClient = dbClient;
     this.issueFinder = issueFinder;
-    this.avatarFactory = avatarFactory;
     this.userSession = userSession;
+    this.issueChangelog = issueChangelog;
   }
 
   @Override
@@ -95,112 +74,31 @@ public class ChangelogAction implements IssuesWsAction {
   @Override
   public void handle(Request request, Response response) throws Exception {
     try (DbSession dbSession = dbClient.openSession(false)) {
-      ChangelogWsResponse wsResponse = Stream.of(request)
-        .map(searchChangelog(dbSession))
-        .map(buildResponse())
-        .collect(MoreCollectors.toOneElement());
-      writeProtobuf(wsResponse, request, response);
-    }
-  }
-
-  private Function<Request, ChangeLogResults> searchChangelog(DbSession dbSession) {
-    return request -> new ChangeLogResults(dbSession, request.mandatoryParam(PARAM_ISSUE));
-  }
-
-  private Function<ChangeLogResults, ChangelogWsResponse> buildResponse() {
-    return result -> Stream.of(ChangelogWsResponse.newBuilder())
-      .peek(addChanges(result))
-      .map(ChangelogWsResponse.Builder::build)
-      .collect(MoreCollectors.toOneElement());
-  }
-
-  private Consumer<ChangelogWsResponse.Builder> addChanges(ChangeLogResults results) {
-    return response -> results.changes.stream()
-      .map(toWsChangelog(results))
-      .forEach(response::addChangelog);
-  }
+      IssueDto issue = issueFinder.getByKey(dbSession, request.mandatoryParam(PARAM_ISSUE));
 
-  private Function<FieldDiffs, Changelog> toWsChangelog(ChangeLogResults results) {
-    return change -> {
-      String userUUuid = change.userUuid();
-      Changelog.Builder changelogBuilder = Changelog.newBuilder();
-      changelogBuilder.setCreationDate(formatDateTime(change.creationDate()));
-      UserDto user = userUUuid == null ? null : results.users.get(userUUuid);
-      if (user != null) {
-        changelogBuilder.setUser(user.getLogin());
-        changelogBuilder.setIsUserActive(user.isActive());
-        ofNullable(user.getName()).ifPresent(changelogBuilder::setUserName);
-        ofNullable(emptyToNull(user.getEmail())).ifPresent(email -> changelogBuilder.setAvatar(avatarFactory.create(user)));
-      }
-      change.diffs().entrySet().stream()
-        .map(toWsDiff(results))
-        .forEach(changelogBuilder::addDiffs);
-      return changelogBuilder.build();
-    };
-  }
-
-  private static Function<Map.Entry<String, FieldDiffs.Diff>, Changelog.Diff> toWsDiff(ChangeLogResults results) {
-    return diff -> {
-      FieldDiffs.Diff value = diff.getValue();
-      Changelog.Diff.Builder diffBuilder = Changelog.Diff.newBuilder();
-      String key = diff.getKey();
-      String oldValue = value.oldValue() != null ? value.oldValue().toString() : null;
-      String newValue = value.newValue() != null ? value.newValue().toString() : null;
-      if (key.equals(FILE)) {
-        diffBuilder.setKey(key);
-        ofNullable(results.getFileLongName(emptyToNull(newValue))).ifPresent(diffBuilder::setNewValue);
-        ofNullable(results.getFileLongName(emptyToNull(oldValue))).ifPresent(diffBuilder::setOldValue);
-      } else {
-        diffBuilder.setKey(key.equals(TECHNICAL_DEBT) ? EFFORT_CHANGELOG_KEY : key);
-        ofNullable(emptyToNull(newValue)).ifPresent(diffBuilder::setNewValue);
-        ofNullable(emptyToNull(oldValue)).ifPresent(diffBuilder::setOldValue);
-      }
-      return diffBuilder.build();
-    };
-  }
-
-  private class ChangeLogResults {
-    private final List<FieldDiffs> changes;
-    private final Map<String, UserDto> users;
-    private final Map<String, ComponentDto> files;
-
-    ChangeLogResults(DbSession dbSession, String issueKey) {
-      IssueDto issue = issueFinder.getByKey(dbSession, issueKey);
-      if (isMember(dbSession, issue)) {
-        this.changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, issue.getKey());
-        List<String> userUuids = changes.stream().filter(change -> change.userUuid() != null).map(FieldDiffs::userUuid).collect(MoreCollectors.toList());
-        this.users = dbClient.userDao().selectByUuids(dbSession, userUuids).stream().collect(MoreCollectors.uniqueIndex(UserDto::getUuid));
-        this.files = dbClient.componentDao().selectByUuids(dbSession, getFileUuids(changes)).stream().collect(MoreCollectors.uniqueIndex(ComponentDto::uuid, Function.identity()));
-      } else {
-        changes = ImmutableList.of();
-        users = ImmutableMap.of();
-        files = ImmutableMap.of();
-      }
+      ChangelogWsResponse build = handle(dbSession, issue);
+      writeProtobuf(build, request, response);
     }
+  }
 
-    private boolean isMember(DbSession dbSession, IssueDto issue) {
-      Optional<ComponentDto> project = dbClient.componentDao().selectByUuid(dbSession, issue.getProjectUuid());
-      checkState(project.isPresent(), "Cannot find the project with uuid %s from issue.id %s", issue.getProjectUuid(), issue.getId());
-      Optional<OrganizationDto> organization = dbClient.organizationDao().selectByUuid(dbSession, project.get().getOrganizationUuid());
-      checkState(organization.isPresent(), "Cannot find the organization with uuid %s from issue.id %s", project.get().getOrganizationUuid(), issue.getId());
-      return userSession.hasMembership(organization.get());
+  public ChangelogWsResponse handle(DbSession dbSession, IssueDto issue) {
+    if (!isMember(dbSession, issue)) {
+      return ChangelogWsResponse.newBuilder().build();
     }
 
-    private Set<String> getFileUuids(List<FieldDiffs> changes) {
-      return changes.stream()
-        .filter(diffs -> diffs.diffs().containsKey(FILE))
-        .flatMap(diffs -> Stream.of(diffs.get(FILE).newValue().toString(), diffs.get(FILE).oldValue().toString()))
-        .collect(MoreCollectors.toSet());
-    }
+    IssueChangelog.ChangelogLoadingContext loadingContext = issueChangelog.newChangelogLoadingContext(dbSession, issue);
 
-    @CheckForNull
-    String getFileLongName(@Nullable String fileUuid) {
-      if (fileUuid == null) {
-        return null;
-      }
-      ComponentDto file = files.get(fileUuid);
-      return file == null ? null : file.longName();
-    }
+    ChangelogWsResponse.Builder builder = ChangelogWsResponse.newBuilder();
+    issueChangelog.formatChangelog(dbSession, loadingContext)
+      .forEach(builder::addChangelog);
+    return builder.build();
+  }
 
+  private boolean isMember(DbSession dbSession, IssueDto issue) {
+    Optional<ComponentDto> project = dbClient.componentDao().selectByUuid(dbSession, issue.getProjectUuid());
+    checkState(project.isPresent(), "Cannot find the project with uuid %s from issue.id %s", issue.getProjectUuid(), issue.getId());
+    Optional<OrganizationDto> organization = dbClient.organizationDao().selectByUuid(dbSession, project.get().getOrganizationUuid());
+    checkState(organization.isPresent(), "Cannot find the organization with uuid %s from issue.id %s", project.get().getOrganizationUuid(), issue.getId());
+    return userSession.hasMembership(organization.get());
   }
 }
index 1103eee6efafd0e8f960479230d9586bd49f8cd5..e66b4f306914aab9884ad04e94187e67d4f3f9a0 100644 (file)
@@ -21,9 +21,10 @@ package org.sonar.server.issue.ws;
 
 import org.sonar.core.platform.Module;
 import org.sonar.server.issue.AvatarResolverImpl;
-import org.sonar.server.issue.TextRangeResponseFormatter;
+import org.sonar.server.issue.IssueChangelog;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.issue.TransitionService;
 import org.sonar.server.issue.WebIssueStorage;
 import org.sonar.server.issue.index.IssueQueryFactory;
@@ -45,6 +46,7 @@ public class IssueWsModule extends Module {
       IssueQueryFactory.class,
       IssuesWs.class,
       AvatarResolverImpl.class,
+      IssueChangelog.class,
       SearchResponseLoader.class,
       TextRangeResponseFormatter.class,
       SearchResponseFormat.class,
index 61bcb3c5bec6ff035ea3b13169f64229034cb13d..0c68ab3f42034c3d35ae6fe9fcfbcfe5ff1b2cec 100644 (file)
@@ -25,20 +25,25 @@ import com.tngtech.java.junit.dataprovider.DataProviderRunner;
 import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Random;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.System2;
 import org.sonar.api.web.UserRole;
 import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
@@ -51,6 +56,8 @@ import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.IssueChangelog;
+import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.security.SecurityStandards;
 import org.sonar.server.security.SecurityStandards.SQCategory;
@@ -63,6 +70,12 @@ import org.sonarqube.ws.Hotspots;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 
@@ -82,8 +95,9 @@ public class ShowActionTest {
 
   private TextRangeResponseFormatter commonFormatter = new TextRangeResponseFormatter();
   private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
+  private IssueChangelog issueChangelog = Mockito.mock(IssueChangelog.class);
 
-  private ShowAction underTest = new ShowAction(dbClient, userSessionRule, responseFormatter, commonFormatter);
+  private ShowAction underTest = new ShowAction(dbClient, userSessionRule, responseFormatter, commonFormatter, issueChangelog);
   private WsActionTester actionTester = new WsActionTester(underTest);
 
   @Test
@@ -353,6 +367,36 @@ public class ShowActionTest {
     verifyComponent(response.getComponent(), project);
   }
 
+  @Test
+  public void returns_hotspot_changelog() {
+    ComponentDto project = dbTester.components().insertPublicProject();
+    userSessionRule.registerComponents(project);
+    RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule)
+      .setLocations(DbIssues.Locations.newBuilder()
+        .setTextRange(DbCommons.TextRange.newBuilder().build())
+        .build()));
+    ChangelogLoadingContext changelogLoadingContext = Mockito.mock(ChangelogLoadingContext.class);
+    List<Common.Changelog> changelog = IntStream.range(0, 1 + new Random().nextInt(12))
+      .mapToObj(i -> Common.Changelog.newBuilder().setUser("u" + i).build())
+      .collect(Collectors.toList());
+    when(issueChangelog.newChangelogLoadingContext(any(), any(), anySet(), anySet())).thenReturn(changelogLoadingContext);
+    when(issueChangelog.formatChangelog(any(), eq(changelogLoadingContext)))
+      .thenReturn(changelog.stream());
+
+    Hotspots.ShowWsResponse response = newRequest(hotspot)
+      .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+    assertThat(response.getChangelogList())
+      .extracting(Common.Changelog::getUser)
+      .containsExactly(changelog.stream().map(Common.Changelog::getUser).toArray(String[]::new));
+    verify(issueChangelog).newChangelogLoadingContext(any(DbSession.class),
+      argThat(new IssueDtoArgumentMatcher(hotspot)),
+      eq(Collections.emptySet()), eq(ImmutableSet.of(project, file)));
+    verify(issueChangelog).formatChangelog(any(DbSession.class), eq(changelogLoadingContext));
+  }
+
   public void verifyRule(Hotspots.Rule wsRule, RuleDefinitionDto dto) {
     assertThat(wsRule.getKey()).isEqualTo(dto.getKey().toString());
     assertThat(wsRule.getName()).isEqualTo(dto.getName());
@@ -394,4 +438,17 @@ public class ShowActionTest {
     return ruleDefinition;
   }
 
+  private static class IssueDtoArgumentMatcher implements ArgumentMatcher<IssueDto> {
+    private final IssueDto expected;
+
+    private IssueDtoArgumentMatcher(IssueDto expected) {
+      this.expected = expected;
+    }
+
+    @Override
+    public boolean matches(IssueDto argument) {
+      return argument != null && argument.getKey().equals(expected.getKey());
+    }
+  }
+
 }
index 02aa097e1dd33be64eedf989583e4650c536d2c8..aa447b0402e51e3eb453a111ec190d07a5e824c0 100644 (file)
@@ -38,6 +38,7 @@ import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserTesting;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.issue.AvatarResolverImpl;
+import org.sonar.server.issue.IssueChangelog;
 import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
@@ -69,7 +70,10 @@ public class ChangelogActionTest {
 
   private ComponentDto project;
   private ComponentDto file;
-  private WsActionTester tester = new WsActionTester(new ChangelogAction(db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new AvatarResolverImpl(), userSession));
+  private IssueFinder issueFinder = new IssueFinder(db.getDbClient(), userSession);
+  private IssueChangelog issueChangelog = new IssueChangelog(db.getDbClient(), new AvatarResolverImpl());
+  private ChangelogAction underTest = new ChangelogAction(db.getDbClient(), issueFinder, userSession, issueChangelog);
+  private WsActionTester tester = new WsActionTester(underTest);
 
   @Before
   public void setUp() {
index 4e41efcfd6fb061bb9f9fc1365fc0ecd5fb0de8c..7b1220885fe2548349152e69d6fec79565a040ef 100644 (file)
@@ -30,7 +30,7 @@ public class IssueWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new IssueWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 29);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 30);
   }
 }
 
index e851afb7adbf6e47f046dbf4dc0cf8a59e091edd..a4ce5baed97ec33a149ed47cb3462cc18c2a65e8 100644 (file)
@@ -66,6 +66,7 @@ message ShowWsResponse {
   optional string creationDate = 11;
   optional string updateDate = 12;
   optional sonarqube.ws.commons.TextRange textRange = 13;
+  repeated sonarqube.ws.commons.Changelog changelog = 14;
 }
 
 message Component {