From: Sébastien Lesaint Date: Mon, 9 Dec 2019 11:04:26 +0000 (+0100) Subject: SONAR-12720 add comments to response of WS api/hotspots/show X-Git-Tag: 8.2.0.32929~185 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=914778df852bc2bcc9e4274045820435923f4793;p=sonarqube.git SONAR-12720 add comments to response of WS api/hotspots/show --- diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java index 7b5c3f5c1e5..21ab159e185 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java @@ -38,8 +38,9 @@ import org.sonar.db.issue.IssueDto; import org.sonar.db.rule.RuleDefinitionDto; import org.sonar.db.user.UserDto; import org.sonar.server.exceptions.NotFoundException; -import org.sonar.server.issue.IssueChangelog; -import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext; +import org.sonar.server.issue.IssueChangeWSSupport; +import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; +import org.sonar.server.issue.IssueChangeWSSupport.Load; import org.sonar.server.issue.TextRangeResponseFormatter; import org.sonar.server.issue.ws.UserResponseFormatter; import org.sonar.server.security.SecurityStandards; @@ -50,6 +51,7 @@ import org.sonarqube.ws.Hotspots.ShowWsResponse; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.nullToEmpty; import static java.lang.String.format; +import static java.util.Collections.singleton; import static java.util.Optional.ofNullable; import static org.sonar.api.utils.DateUtils.formatDateTime; import static org.sonar.server.ws.WsUtils.writeProtobuf; @@ -63,17 +65,17 @@ public class ShowAction implements HotspotsWsAction { private final HotspotWsResponseFormatter responseFormatter; private final TextRangeResponseFormatter textRangeFormatter; private final UserResponseFormatter userFormatter; - private final IssueChangelog issueChangelog; + private final IssueChangeWSSupport issueChangeSupport; public ShowAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter, - UserResponseFormatter userFormatter, IssueChangelog issueChangelog) { + UserResponseFormatter userFormatter, IssueChangeWSSupport issueChangeSupport) { this.dbClient = dbClient; this.hotspotWsSupport = hotspotWsSupport; this.responseFormatter = responseFormatter; this.textRangeFormatter = textRangeFormatter; this.userFormatter = userFormatter; - this.issueChangelog = issueChangelog; + this.issueChangeSupport = issueChangeSupport; } @Override @@ -109,7 +111,7 @@ public class ShowAction implements HotspotsWsAction { formatComponents(components, responseBuilder); formatRule(responseBuilder, rule); formatTextRange(hotspot, responseBuilder); - formatChangelog(dbSession, hotspot, components, responseBuilder); + formatChangeLogAndComments(dbSession, hotspot, components, responseBuilder); writeProtobuf(responseBuilder.build(), request, response); } @@ -175,13 +177,15 @@ public class ShowAction implements HotspotsWsAction { textRangeFormatter.formatTextRange(hotspot, responseBuilder::setTextRange); } - private void formatChangelog(DbSession dbSession, IssueDto hotspot, Components components, ShowWsResponse.Builder responseBuilder) { + private void formatChangeLogAndComments(DbSession dbSession, IssueDto hotspot, Components components, ShowWsResponse.Builder responseBuilder) { Set preloadedComponents = ImmutableSet.of(components.project, components.component); - ChangelogLoadingContext changelogLoadingContext = issueChangelog - .newChangelogLoadingContext(dbSession, hotspot, ImmutableSet.of(), preloadedComponents); + FormattingContext formattingContext = issueChangeSupport + .newFormattingContext(dbSession, singleton(hotspot), Load.ALL, ImmutableSet.of(), preloadedComponents); - issueChangelog.formatChangelog(dbSession, changelogLoadingContext) + issueChangeSupport.formatChangelog(hotspot, formattingContext) .forEach(responseBuilder::addChangelog); + issueChangeSupport.formatComments(hotspot, Common.Comment.newBuilder(), formattingContext) + .forEach(responseBuilder::addComment); } private RuleDefinitionDto loadRule(DbSession dbSession, IssueDto hotspot) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangeWSSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangeWSSupport.java new file mode 100644 index 00000000000..986dc4dcb24 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangeWSSupport.java @@ -0,0 +1,354 @@ +/* + * 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.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.Ordering; +import com.google.common.collect.Sets; +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.utils.DateUtils; +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.IssueChangeDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.user.UserDto; +import org.sonar.markdown.Markdown; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.Common; + +import static com.google.common.base.Strings.emptyToNull; +import static java.util.Collections.emptyMap; +import static java.util.Optional.empty; +import static java.util.Optional.ofNullable; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.core.util.stream.MoreCollectors.toList; +import static org.sonar.core.util.stream.MoreCollectors.toSet; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonar.db.issue.IssueChangeDto.TYPE_COMMENT; +import static org.sonar.db.issue.IssueChangeDto.TYPE_FIELD_CHANGE; +import static org.sonar.server.issue.IssueFieldsSetter.FILE; +import static org.sonar.server.issue.IssueFieldsSetter.TECHNICAL_DEBT; + +public class IssueChangeWSSupport { + private static final String EFFORT_CHANGELOG_KEY = "effort"; + private static final Ordering ISSUE_CHANGE_CREATED_AT_COMPARATOR = Ordering.natural().onResultOf(IssueChangeDto::getCreatedAt); + + private final DbClient dbClient; + private final AvatarResolver avatarFactory; + private final UserSession userSession; + + public IssueChangeWSSupport(DbClient dbClient, AvatarResolver avatarFactory, UserSession userSession) { + this.dbClient = dbClient; + this.avatarFactory = avatarFactory; + this.userSession = userSession; + } + + public enum Load { + CHANGE_LOG, COMMENTS, ALL; + } + + public interface FormattingContext { + + List getChanges(IssueDto dto); + + List getComments(IssueDto dto); + + Optional getUserByUuid(@Nullable String uuid); + + Optional getFileByUuid(@Nullable String uuid); + + boolean isUpdatableComment(IssueChangeDto comment); + } + + public FormattingContext newFormattingContext(DbSession dbSession, Set dtos, Load load) { + return newFormattingContext(dbSession, dtos, load, ImmutableSet.of(), ImmutableSet.of()); + } + + public FormattingContext newFormattingContext(DbSession dbSession, Set dtos, Load load, + Set preloadedUsers, Set preloadedComponents) { + Set issueKeys = dtos.stream().map(IssueDto::getKey).collect(toSet()); + + List changes = ImmutableList.of(); + List comments = ImmutableList.of(); + switch (load) { + case CHANGE_LOG: + changes = dbClient.issueChangeDao().selectByTypeAndIssueKeys(dbSession, issueKeys, TYPE_FIELD_CHANGE); + break; + case COMMENTS: + comments = dbClient.issueChangeDao().selectByTypeAndIssueKeys(dbSession, issueKeys, TYPE_COMMENT); + break; + case ALL: + List all = dbClient.issueChangeDao().selectByIssueKeys(dbSession, issueKeys); + changes = all.stream() + .filter(t -> TYPE_FIELD_CHANGE.equals(t.getChangeType())) + .collect(toList()); + comments = all.stream() + .filter(t -> TYPE_COMMENT.equals(t.getChangeType())) + .collect(toList()); + break; + default: + throw new IllegalStateException("Unsupported Load value:" + load); + } + + Map> changesByRuleKey = indexAndSort(changes, IssueChangeDto::toFieldDiffs); + Map> commentsByIssueKey = indexAndSort(comments, t -> t); + Map usersByUuid = loadUsers(dbSession, changesByRuleKey, commentsByIssueKey, preloadedUsers); + Map filesByUuid = loadFiles(dbSession, changesByRuleKey, preloadedComponents); + Map updatableCommentByKey = loadUpdatableFlag(commentsByIssueKey); + return new FormattingContextImpl(changesByRuleKey, commentsByIssueKey, usersByUuid, filesByUuid, updatableCommentByKey); + } + + private static Map> indexAndSort(List changes, Function transform) { + Multimap unordered = changes.stream() + .collect(MoreCollectors.index(IssueChangeDto::getIssueKey, t -> t)); + return unordered.asMap().entrySet().stream() + .collect(uniqueIndex( + Map.Entry::getKey, + t -> t.getValue().stream() + .sorted(ISSUE_CHANGE_CREATED_AT_COMPARATOR) + .map(transform) + .collect(toList(t.getValue().size())))); + } + + private Map loadUsers(DbSession dbSession, Map> changesByRuleKey, + Map> commentsByIssueKey, Set preloadedUsers) { + Set userUuids = Stream.concat( + changesByRuleKey.values().stream() + .flatMap(Collection::stream) + .map(FieldDiffs::userUuid), + commentsByIssueKey.values().stream() + .flatMap(Collection::stream) + .map(IssueChangeDto::getUserUuid)) + .filter(Objects::nonNull) + .collect(toSet()); + if (userUuids.isEmpty()) { + return emptyMap(); + } + + Set preloadedUserUuids = preloadedUsers.stream().map(UserDto::getUuid).collect(Collectors.toSet()); + Set missingUsersUuids = Sets.difference(userUuids, preloadedUserUuids).immutableCopy(); + if (missingUsersUuids.isEmpty()) { + return preloadedUsers.stream() + .filter(t -> userUuids.contains(t.getUuid())) + .collect(uniqueIndex(UserDto::getUuid, userUuids.size())); + } + + return Stream.concat( + preloadedUsers.stream(), + dbClient.userDao().selectByUuids(dbSession, missingUsersUuids).stream()) + .filter(t -> userUuids.contains(t.getUuid())) + .collect(uniqueIndex(UserDto::getUuid, userUuids.size())); + } + + private Map loadFiles(DbSession dbSession, Map> changesByRuleKey, Set preloadedComponents) { + Set fileUuids = changesByRuleKey.values().stream() + .flatMap(Collection::stream) + .flatMap(diffs -> { + FieldDiffs.Diff diff = diffs.get(FILE); + if (diff == null) { + return Stream.empty(); + } + return Stream.of(toString(diff.newValue()), toString(diff.oldValue())); + }) + .map(Strings::emptyToNull) + .filter(Objects::nonNull) + .collect(toSet()); + if (fileUuids.isEmpty()) { + return emptyMap(); + } + + Set preloadedFileUuids = preloadedComponents.stream().map(ComponentDto::uuid).collect(Collectors.toSet()); + Set missingFileUuids = Sets.difference(fileUuids, preloadedFileUuids).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())); + } + + private Map loadUpdatableFlag(Map> commentsByIssueKey) { + if (!userSession.isLoggedIn()) { + return emptyMap(); + } + String userUuid = userSession.getUuid(); + if (userUuid == null) { + return emptyMap(); + } + + return commentsByIssueKey.values().stream() + .flatMap(Collection::stream) + .collect(uniqueIndex(IssueChangeDto::getKey, t -> userUuid.equals(t.getUserUuid()))); + } + + public Stream formatChangelog(IssueDto dto, FormattingContext formattingContext) { + return formattingContext.getChanges(dto).stream() + .map(toWsChangelog(formattingContext)); + } + + private Function toWsChangelog(FormattingContext formattingContext) { + return change -> { + String userUUuid = change.userUuid(); + Common.Changelog.Builder changelogBuilder = Common.Changelog.newBuilder(); + changelogBuilder.setCreationDate(formatDateTime(change.creationDate())); + formattingContext.getUserByUuid(userUUuid) + .ifPresent(user -> { + 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(formattingContext)) + .forEach(changelogBuilder::addDiffs); + return changelogBuilder.build(); + }; + } + + private static Function, Common.Changelog.Diff> toWsDiff(FormattingContext formattingContext) { + return diff -> { + FieldDiffs.Diff value = diff.getValue(); + Common.Changelog.Diff.Builder diffBuilder = Common.Changelog.Diff.newBuilder(); + String key = diff.getKey(); + String oldValue = emptyToNull(toString(value.oldValue())); + String newValue = emptyToNull(toString(value.newValue())); + if (key.equals(FILE)) { + diffBuilder.setKey(key); + formattingContext.getFileByUuid(newValue).map(ComponentDto::longName).ifPresent(diffBuilder::setNewValue); + formattingContext.getFileByUuid(oldValue).map(ComponentDto::longName).ifPresent(diffBuilder::setOldValue); + } else { + diffBuilder.setKey(key.equals(TECHNICAL_DEBT) ? EFFORT_CHANGELOG_KEY : key); + ofNullable(newValue).ifPresent(diffBuilder::setNewValue); + ofNullable(oldValue).ifPresent(diffBuilder::setOldValue); + } + return diffBuilder.build(); + }; + } + + public Stream formatComments(IssueDto dto, Common.Comment.Builder commentBuilder, FormattingContext formattingContext) { + return formattingContext.getComments(dto).stream() + .map(comment -> { + commentBuilder + .clear() + .setKey(comment.getKey()) + .setUpdatable(formattingContext.isUpdatableComment(comment)) + .setCreatedAt(DateUtils.formatDateTime(new Date(comment.getIssueChangeCreationDate()))); + String markdown = comment.getChangeData(); + formattingContext.getUserByUuid(comment.getUserUuid()).ifPresent(user -> commentBuilder.setLogin(user.getLogin())); + if (markdown != null) { + commentBuilder + .setHtmlText(Markdown.convertToHtml(markdown)) + .setMarkdown(markdown); + } + return commentBuilder.build(); + }); + } + + private static String toString(@Nullable Serializable serializable) { + if (serializable != null) { + return serializable.toString(); + } + return null; + } + + @Immutable + public static final class FormattingContextImpl implements FormattingContext { + private final Map> changesByIssueKey; + private final Map> commentsByIssueKey; + private final Map usersByUuid; + private final Map filesByUuid; + private final Map updatableCommentByKey; + + private FormattingContextImpl(Map> changesByIssueKey, + Map> commentsByIssueKey, + Map usersByUuid, Map filesByUuid, + Map updatableCommentByKey) { + this.changesByIssueKey = changesByIssueKey; + this.commentsByIssueKey = commentsByIssueKey; + this.usersByUuid = usersByUuid; + this.filesByUuid = filesByUuid; + this.updatableCommentByKey = updatableCommentByKey; + } + + @Override + public List getChanges(IssueDto dto) { + List fieldDiffs = changesByIssueKey.get(dto.getKey()); + if (fieldDiffs == null) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(fieldDiffs); + } + + @Override + public List getComments(IssueDto dto) { + List comments = commentsByIssueKey.get(dto.getKey()); + if (comments == null) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(comments); + } + + @Override + public Optional getUserByUuid(@Nullable String uuid) { + if (uuid == null) { + return empty(); + } + return Optional.ofNullable(usersByUuid.get(uuid)); + } + + @Override + public Optional getFileByUuid(@Nullable String uuid) { + if (uuid == null) { + return empty(); + } + return Optional.ofNullable(filesByUuid.get(uuid)); + } + + @Override + public boolean isUpdatableComment(IssueChangeDto comment) { + Boolean flag = updatableCommentByKey.get(comment.getKey()); + return flag != null && flag; + } + } + +} 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 deleted file mode 100644 index 56f0e70d017..00000000000 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * 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 preloadedUsers, Set preloadedComponents) { - List changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, dto.getKey()); - return new ChangelogLoadingContextImpl(changes, preloadedUsers, preloadedComponents); - } - - public Stream formatChangelog(DbSession dbSession, ChangelogLoadingContext loadingContext) { - Map usersByUuid = loadUsers(dbSession, loadingContext); - Map filesByUuid = loadFiles(dbSession, loadingContext); - FormatableChangeLog changeLogResults = new FormatableChangeLog(loadingContext.getChanges(), usersByUuid, filesByUuid); - - return changeLogResults.changes.stream() - .map(toWsChangelog(changeLogResults)); - } - - private Map loadUsers(DbSession dbSession, ChangelogLoadingContext loadingContext) { - List changes = loadingContext.getChanges(); - if (changes.isEmpty()) { - return emptyMap(); - } - - Set usersByUuid = loadingContext.getPreloadedUsers(); - - Set userUuids = changes.stream() - .filter(change -> change.userUuid() != null) - .map(FieldDiffs::userUuid) - .collect(toSet()); - if (userUuids.isEmpty()) { - return emptyMap(); - } - - Set 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 loadFiles(DbSession dbSession, ChangelogLoadingContext loadingContext) { - List changes = loadingContext.getChanges(); - if (changes.isEmpty()) { - return emptyMap(); - } - - Set 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 preloadedComponents = loadingContext.getPreloadedComponents(); - Set preloadedComponentUuids = preloadedComponents.stream() - .map(ComponentDto::uuid) - .collect(toSet(preloadedComponents.size())); - Set 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 getChanges(); - - Set getPreloadedUsers(); - - Set getPreloadedComponents(); - } - - @Immutable - public static final class ChangelogLoadingContextImpl implements ChangelogLoadingContext { - private final List changes; - private final Set preloadedUsers; - private final Set preloadedComponents; - - private ChangelogLoadingContextImpl(List changes, Set preloadedUsers, Set preloadedComponents) { - this.changes = ImmutableList.copyOf(changes); - this.preloadedUsers = ImmutableSet.copyOf(preloadedUsers); - this.preloadedComponents = ImmutableSet.copyOf(preloadedComponents); - } - - @Override - public List getChanges() { - return changes; - } - - @Override - public Set getPreloadedUsers() { - return preloadedUsers; - } - - @Override - public Set getPreloadedComponents() { - return preloadedComponents; - } - } - - private Function 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, 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 changes; - private final Map users; - private final Map files; - - private FormatableChangeLog(List changes, Map users, Map 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(); - } - - } -} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java index ff6246b5149..eccc9a86d1b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java @@ -30,12 +30,14 @@ 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.server.issue.IssueChangelog; +import org.sonar.server.issue.IssueChangeWSSupport; +import org.sonar.server.issue.IssueChangeWSSupport.Load; import org.sonar.server.issue.IssueFinder; import org.sonar.server.user.UserSession; import org.sonarqube.ws.Issues.ChangelogWsResponse; import static com.google.common.base.Preconditions.checkState; +import static java.util.Collections.singleton; import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_CHANGELOG; @@ -46,13 +48,13 @@ public class ChangelogAction implements IssuesWsAction { private final DbClient dbClient; private final IssueFinder issueFinder; private final UserSession userSession; - private final IssueChangelog issueChangelog; + private final IssueChangeWSSupport issueChangeSupport; - public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, UserSession userSession, IssueChangelog issueChangelog) { + public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, UserSession userSession, IssueChangeWSSupport issueChangeSupport) { this.dbClient = dbClient; this.issueFinder = issueFinder; this.userSession = userSession; - this.issueChangelog = issueChangelog; + this.issueChangeSupport = issueChangeSupport; } @Override @@ -86,10 +88,10 @@ public class ChangelogAction implements IssuesWsAction { return ChangelogWsResponse.newBuilder().build(); } - IssueChangelog.ChangelogLoadingContext loadingContext = issueChangelog.newChangelogLoadingContext(dbSession, issue); + IssueChangeWSSupport.FormattingContext formattingContext = issueChangeSupport.newFormattingContext(dbSession, singleton(issue), Load.CHANGE_LOG); ChangelogWsResponse.Builder builder = ChangelogWsResponse.newBuilder(); - issueChangelog.formatChangelog(dbSession, loadingContext) + issueChangeSupport.formatChangelog(issue, formattingContext) .forEach(builder::addChangelog); return builder.build(); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index dd92918d965..904eacb305e 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -21,7 +21,7 @@ package org.sonar.server.issue.ws; import org.sonar.core.platform.Module; import org.sonar.server.issue.AvatarResolverImpl; -import org.sonar.server.issue.IssueChangelog; +import org.sonar.server.issue.IssueChangeWSSupport; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.TextRangeResponseFormatter; @@ -46,7 +46,7 @@ public class IssueWsModule extends Module { IssueQueryFactory.class, IssuesWs.class, AvatarResolverImpl.class, - IssueChangelog.class, + IssueChangeWSSupport.class, SearchResponseLoader.class, TextRangeResponseFormatter.class, UserResponseFormatter.class, diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java index 2317cee6b05..1a0b8b61336 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java @@ -59,8 +59,9 @@ import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.issue.AvatarResolver; import org.sonar.server.issue.AvatarResolverImpl; -import org.sonar.server.issue.IssueChangelog; -import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext; +import org.sonar.server.issue.IssueChangeWSSupport; +import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; +import org.sonar.server.issue.IssueChangeWSSupport.Load; import org.sonar.server.issue.TextRangeResponseFormatter; import org.sonar.server.issue.ws.UserResponseFormatter; import org.sonar.server.organization.TestDefaultOrganizationProvider; @@ -101,12 +102,12 @@ public class ShowActionTest { private AvatarResolver avatarResolver = new AvatarResolverImpl(); private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider); - private IssueChangelog issueChangelog = Mockito.mock(IssueChangelog.class); + private IssueChangeWSSupport issueChangeSupport = Mockito.mock(IssueChangeWSSupport.class); private HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, System2.INSTANCE); private UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl()); private TextRangeResponseFormatter textRangeFormatter = new TextRangeResponseFormatter(); - private ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, textRangeFormatter, userFormatter, issueChangelog); + private ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, textRangeFormatter, userFormatter, issueChangeSupport); private WsActionTester actionTester = new WsActionTester(underTest); @Test @@ -556,7 +557,7 @@ public class ShowActionTest { } @Test - public void returns_hotspot_changelog() { + public void returns_hotspot_changelog_and_comments() { ComponentDto project = dbTester.components().insertPublicProject(); userSessionRule.registerComponents(project); RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); @@ -565,13 +566,16 @@ public class ShowActionTest { .setLocations(DbIssues.Locations.newBuilder() .setTextRange(DbCommons.TextRange.newBuilder().build()) .build())); - ChangelogLoadingContext changelogLoadingContext = Mockito.mock(ChangelogLoadingContext.class); + FormattingContext formattingContext = Mockito.mock(FormattingContext.class); List 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()); + List comments = IntStream.range(0, 1 + new Random().nextInt(12)) + .mapToObj(i -> Common.Comment.newBuilder().setKey("u" + i).build()) + .collect(Collectors.toList()); + when(issueChangeSupport.newFormattingContext(any(), any(), any(), anySet(), anySet())).thenReturn(formattingContext); + when(issueChangeSupport.formatChangelog(any(), any())).thenReturn(changelog.stream()); + when(issueChangeSupport.formatComments(any(), any(), any())).thenReturn(comments.stream()); Hotspots.ShowWsResponse response = newRequest(hotspot) .executeProtobuf(Hotspots.ShowWsResponse.class); @@ -579,10 +583,15 @@ public class ShowActionTest { 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)), + assertThat(response.getCommentList()) + .extracting(Common.Comment::getKey) + .containsExactly(comments.stream().map(Common.Comment::getKey).toArray(String[]::new)); + verify(issueChangeSupport).newFormattingContext(any(DbSession.class), + argThat(new IssueDtoSetArgumentMatcher(hotspot)), + eq(Load.ALL), eq(Collections.emptySet()), eq(ImmutableSet.of(project, file))); - verify(issueChangelog).formatChangelog(any(DbSession.class), eq(changelogLoadingContext)); + verify(issueChangeSupport).formatChangelog(argThat(new IssueDtoArgumentMatcher(hotspot)), eq(formattingContext)); + verify(issueChangeSupport).formatComments(argThat(new IssueDtoArgumentMatcher(hotspot)), any(Common.Comment.Builder.class), eq(formattingContext)); } public void verifyRule(Hotspots.Rule wsRule, RuleDefinitionDto dto) { @@ -636,6 +645,24 @@ public class ShowActionTest { return ruleDefinition; } + private static class IssueDtoSetArgumentMatcher implements ArgumentMatcher> { + private final IssueDto expected; + + private IssueDtoSetArgumentMatcher(IssueDto expected) { + this.expected = expected; + } + + @Override + public boolean matches(Set argument) { + return argument != null && argument.size() == 1 && argument.iterator().next().getKey().equals(expected.getKey()); + } + + @Override + public String toString() { + return "Set[" + expected.getKey() + "]"; + } + } + private static class IssueDtoArgumentMatcher implements ArgumentMatcher { private final IssueDto expected; @@ -647,6 +674,11 @@ public class ShowActionTest { public boolean matches(IssueDto argument) { return argument != null && argument.getKey().equals(expected.getKey()); } + + @Override + public String toString() { + return "IssueDto[key=" + expected.getKey() + "]"; + } } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/IssueChangeWSSupportTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/IssueChangeWSSupportTest.java new file mode 100644 index 00000000000..eafb637a3b3 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/IssueChangeWSSupportTest.java @@ -0,0 +1,606 @@ +/* + * 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.ImmutableSet; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sonar.api.utils.System2; +import org.sonar.core.issue.FieldDiffs; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.issue.IssueChangeDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.issue.IssueTesting; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.db.user.UserDto; +import org.sonar.markdown.Markdown; +import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; +import org.sonar.server.issue.IssueChangeWSSupport.Load; +import org.sonar.server.tester.UserSessionRule; +import org.sonarqube.ws.Common.Changelog; +import org.sonarqube.ws.Common.Comment; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.db.issue.IssueChangeDto.TYPE_COMMENT; +import static org.sonar.db.issue.IssueChangeDto.TYPE_FIELD_CHANGE; + +@RunWith(DataProviderRunner.class) +public class IssueChangeWSSupportTest { + private static final Random RANDOM = new Random(); + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + + private DbClient dbClient = dbTester.getDbClient(); + private AvatarResolverImpl avatarResolver = new AvatarResolverImpl(); + + private IssueChangeWSSupport underTest = new IssueChangeWSSupport(dbClient, avatarResolver, userSessionRule); + + @Test + public void newFormattingContext_with_Load_CHANGE_LOG_loads_only_changelog() { + IssueDto issue = dbTester.issues().insertIssue(); + List comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newComment(issue).setKey("comment_" + i)) + .collect(Collectors.toList()); + List fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newFieldChange(issue) + .setChangeData(new FieldDiffs() + .setDiff("f_change_" + i, null, null) + .toEncodedString())) + .collect(Collectors.toList()); + insertInRandomOrder(comments, fieldChanges); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.CHANGE_LOG); + + assertThat(formattingContext.getChanges(issue)) + .extracting(FieldDiffs::toEncodedString) + .containsExactlyInAnyOrder(fieldChanges.stream().map(t -> t.toFieldDiffs().toEncodedString()).toArray(String[]::new)); + assertThat(formattingContext.getComments(issue)).isEmpty(); + } + + @Test + public void newFormattingContext_with_Load_COMMENTS_loads_only_comments() { + IssueDto issue = dbTester.issues().insertIssue(); + List comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newComment(issue).setKey("comment_" + i)) + .collect(Collectors.toList()); + List fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newFieldChange(issue) + .setChangeData(new FieldDiffs() + .setDiff("f_change_" + i, null, null) + .toEncodedString())) + .collect(Collectors.toList()); + insertInRandomOrder(comments, fieldChanges); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.COMMENTS); + + assertThat(formattingContext.getComments(issue)) + .extracting(IssueChangeDto::getKey) + .containsExactlyInAnyOrder(comments.stream().map(IssueChangeDto::getKey).toArray(String[]::new)); + assertThat(formattingContext.getChanges(issue)).isEmpty(); + } + + @Test + public void newFormattingContext_with_Load_ALL_loads_changelog_and_comments() { + IssueDto issue = dbTester.issues().insertIssue(); + List comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newComment(issue).setKey("comment_" + i)) + .collect(Collectors.toList()); + List fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newFieldChange(issue) + .setChangeData(new FieldDiffs() + .setDiff("f_change_" + i, null, null) + .toEncodedString())) + .collect(Collectors.toList()); + insertInRandomOrder(comments, fieldChanges); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL); + + assertThat(formattingContext.getComments(issue)) + .extracting(IssueChangeDto::getKey) + .containsExactlyInAnyOrder(comments.stream().map(IssueChangeDto::getKey).toArray(String[]::new)); + assertThat(formattingContext.getComments(issue)) + .extracting(IssueChangeDto::getKey) + .containsExactlyInAnyOrder(comments.stream().map(IssueChangeDto::getKey).toArray(String[]::new)); + } + + @Test + public void newFormattingContext_with_load_CHANGE_LOG_loads_users_of_field_changes() { + IssueDto issue = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(); + UserDto user3 = dbTester.users().insertUser(); + String uuid = randomAlphabetic(30); + IssueChangeDto fieldChangeUser1 = newFieldChange(issue) + .setUserUuid(user1.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_1", null, null).toEncodedString()); + IssueChangeDto fieldChangeUser2a = newFieldChange(issue) + .setUserUuid(user2.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_2a", null, null).toEncodedString()); + IssueChangeDto fieldChangeUser2b = newFieldChange(issue) + .setUserUuid(user2.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_2b", null, null).toEncodedString()); + IssueChangeDto fieldChangeNonExistingUser = newFieldChange(issue) + .setUserUuid(uuid) + .setChangeData(new FieldDiffs().setDiff("f_change_user_unknown", null, null).toEncodedString()); + insertInRandomOrder(Arrays.asList(fieldChangeUser1, fieldChangeUser2a, fieldChangeUser2b, fieldChangeNonExistingUser)); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.CHANGE_LOG); + + assertThat(formattingContext.getUserByUuid(user1.getUuid())).isNotEmpty(); + assertThat(formattingContext.getUserByUuid(user2.getUuid())).isNotEmpty(); + assertThat(formattingContext.getUserByUuid(user3.getUuid())).isEmpty(); + assertThat(formattingContext.getUserByUuid(uuid)).isEmpty(); + } + + @Test + public void newFormattingContext_with_load_COMMENTS_loads_users_of_comments() { + IssueDto issue = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(); + UserDto user3 = dbTester.users().insertUser(); + String uuid = randomAlphabetic(30); + IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); + IssueChangeDto issueChangeUser2a = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto issueChangeUser2b = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto issueChangeNonExistingUser = newComment(issue).setUserUuid(uuid); + insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2a, issueChangeUser2b, issueChangeNonExistingUser)); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.COMMENTS); + + assertThat(formattingContext.getUserByUuid(user1.getUuid())).isNotEmpty(); + assertThat(formattingContext.getUserByUuid(user2.getUuid())).isNotEmpty(); + assertThat(formattingContext.getUserByUuid(user3.getUuid())).isEmpty(); + assertThat(formattingContext.getUserByUuid(uuid)).isEmpty(); + } + + @Test + public void newFormattingContext_with_load_ALL_loads_users_of_fieldChanges_and_comments() { + IssueDto issue = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(); + UserDto user3 = dbTester.users().insertUser(); + UserDto user4 = dbTester.users().insertUser(); + String uuid = randomAlphabetic(30); + IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); + IssueChangeDto issueChangeUser2a = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto issueChangeUser2b = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto issueChangeNonExistingUser = newComment(issue).setUserUuid(uuid); + IssueChangeDto fieldChangeUser1 = newFieldChange(issue) + .setUserUuid(user1.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_1", null, null).toEncodedString()); + IssueChangeDto fieldChangeUser4 = newFieldChange(issue) + .setUserUuid(user4.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_4", null, null).toEncodedString()); + IssueChangeDto fieldChangeNonExistingUser = newFieldChange(issue) + .setUserUuid(uuid) + .setChangeData(new FieldDiffs().setDiff("f_change_user_unknown", null, null).toEncodedString()); + insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2a, issueChangeUser2b, issueChangeNonExistingUser), + Arrays.asList(fieldChangeUser1, fieldChangeUser4, fieldChangeNonExistingUser)); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL); + + assertThat(formattingContext.getUserByUuid(user1.getUuid())).isNotEmpty(); + assertThat(formattingContext.getUserByUuid(user2.getUuid())).isNotEmpty(); + assertThat(formattingContext.getUserByUuid(user3.getUuid())).isEmpty(); + assertThat(formattingContext.getUserByUuid(user4.getUuid())).isNotEmpty(); + assertThat(formattingContext.getUserByUuid(uuid)).isEmpty(); + } + + @Test + public void newFormattingContext_with_load_CHANGE_LOG_loads_files_of_file_fieldChanges() { + newFormattingContext_loads_files_of_file_fieldChanges(Load.CHANGE_LOG); + } + + @Test + public void newFormattingContext_with_load_ALL_loads_files_of_file_fieldChanges() { + newFormattingContext_loads_files_of_file_fieldChanges(Load.ALL); + } + + private void newFormattingContext_loads_files_of_file_fieldChanges(Load load) { + IssueDto issue = dbTester.issues().insertIssue(); + ComponentDto file1 = insertFile(); + ComponentDto file2 = insertFile(); + ComponentDto file3 = insertFile(); + ComponentDto file4 = insertFile(); + ComponentDto file5 = insertFile(); + String uuid = randomAlphabetic(30); + IssueChangeDto fileChangeFile1 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", file1.uuid(), null).toEncodedString()); + IssueChangeDto fileChangeFile2 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", file2.uuid(), null).toEncodedString()); + IssueChangeDto fileChangeFile3 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", null, file3.uuid()).toEncodedString()); + IssueChangeDto fileChangeFile4 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", file4.uuid(), file4.uuid()).toEncodedString()); + IssueChangeDto fileChangeNotExistingFile = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", uuid, uuid).toEncodedString()); + insertInRandomOrder(Arrays.asList(fileChangeFile1, fileChangeFile2, fileChangeFile3, fileChangeFile4, fileChangeNotExistingFile)); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); + + assertThat(formattingContext.getFileByUuid(file1.uuid())).isNotEmpty(); + assertThat(formattingContext.getFileByUuid(file2.uuid())).isNotEmpty(); + assertThat(formattingContext.getFileByUuid(file3.uuid())).isNotEmpty(); + assertThat(formattingContext.getFileByUuid(file4.uuid())).isNotEmpty(); + assertThat(formattingContext.getFileByUuid(file5.uuid())).isEmpty(); + assertThat(formattingContext.getFileByUuid(uuid)).isEmpty(); + } + + @Test + public void newFormattingContext_does_not_load_preloaded_users_from_DB() { + IssueDto issue = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(); + UserDto user3 = dbTester.users().insertUser(); + UserDto user4 = dbTester.users().insertUser(); + IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); + IssueChangeDto issueChangeUser2 = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto fieldChangeUser1 = newFieldChange(issue) + .setUserUuid(user1.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_1", null, null).toEncodedString()); + IssueChangeDto fieldChangeUser3 = newFieldChange(issue) + .setUserUuid(user3.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_3", null, null).toEncodedString()); + IssueChangeDto fieldChangeUser4 = newFieldChange(issue) + .setUserUuid(user4.getUuid()) + .setChangeData(new FieldDiffs().setDiff("f_change_user_4", null, null).toEncodedString()); + insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2), + Arrays.asList(fieldChangeUser1, fieldChangeUser3, fieldChangeUser4)); + user1.setEmail("post_insert_changed" + user1.getUuid()); + user2.setEmail("post_insert_changed" + user2.getUuid()); + user3.setEmail("post_insert_changed" + user3.getUuid()); + user4.setEmail("post_insert_changed" + user4.getUuid()); + + // no users are preloaded + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL, + emptySet(), emptySet()); + assertThat(formattingContext.getUserByUuid(user1.getUuid()).get().getEmail()).isNotEqualTo(user1.getEmail()); + assertThat(formattingContext.getUserByUuid(user2.getUuid()).get().getEmail()).isNotEqualTo(user2.getEmail()); + assertThat(formattingContext.getUserByUuid(user3.getUuid()).get().getEmail()).isNotEqualTo(user3.getEmail()); + assertThat(formattingContext.getUserByUuid(user4.getUuid()).get().getEmail()).isNotEqualTo(user4.getEmail()); + + // some users are preloaded + formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL, + ImmutableSet.of(user1, user4), emptySet()); + assertThat(formattingContext.getUserByUuid(user1.getUuid()).get().getEmail()).isEqualTo(user1.getEmail()); + assertThat(formattingContext.getUserByUuid(user2.getUuid()).get().getEmail()).isNotEqualTo(user2.getEmail()); + assertThat(formattingContext.getUserByUuid(user3.getUuid()).get().getEmail()).isNotEqualTo(user3.getEmail()); + assertThat(formattingContext.getUserByUuid(user4.getUuid()).get().getEmail()).isEqualTo(user4.getEmail()); + + // all users are preloaded + formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL, + ImmutableSet.of(user1, user2, user3, user4), emptySet()); + assertThat(formattingContext.getUserByUuid(user1.getUuid()).get().getEmail()).isEqualTo(user1.getEmail()); + assertThat(formattingContext.getUserByUuid(user2.getUuid()).get().getEmail()).isEqualTo(user2.getEmail()); + assertThat(formattingContext.getUserByUuid(user3.getUuid()).get().getEmail()).isEqualTo(user3.getEmail()); + assertThat(formattingContext.getUserByUuid(user4.getUuid()).get().getEmail()).isEqualTo(user4.getEmail()); + } + + @Test + @UseDataProvider("loadAllOrChangelog") + public void newFormattingContext_does_not_load_preloaded_files_from_DB(Load load) { + IssueDto issue = dbTester.issues().insertIssue(); + ComponentDto file1 = insertFile(); + ComponentDto file2 = insertFile(); + ComponentDto file3 = insertFile(); + ComponentDto file4 = insertFile(); + IssueChangeDto fileChangeFile1 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", file1.uuid(), null).toEncodedString()); + IssueChangeDto fileChangeFile2 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", file2.uuid(), null).toEncodedString()); + IssueChangeDto fileChangeFile3 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", null, file3.uuid()).toEncodedString()); + IssueChangeDto fileChangeFile4 = newFieldChange(issue) + .setChangeData(new FieldDiffs().setDiff("file", file4.uuid(), file4.uuid()).toEncodedString()); + insertInRandomOrder(Arrays.asList(fileChangeFile1, fileChangeFile2, fileChangeFile3, fileChangeFile4)); + file1.setName("preloaded_name" + file1.uuid()); + file2.setName("preloaded_name" + file2.uuid()); + file3.setName("preloaded_name" + file3.uuid()); + file4.setName("preloaded_name" + file4.uuid()); + + // no files are preloaded + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load, + emptySet(), emptySet()); + + assertThat(formattingContext.getFileByUuid(file1.uuid()).get().name()).isNotEqualTo(file1.name()); + assertThat(formattingContext.getFileByUuid(file2.uuid()).get().name()).isNotEqualTo(file2.name()); + assertThat(formattingContext.getFileByUuid(file3.uuid()).get().name()).isNotEqualTo(file3.name()); + assertThat(formattingContext.getFileByUuid(file4.uuid()).get().name()).isNotEqualTo(file4.name()); + + // some files are preloaded + formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load, + emptySet(), ImmutableSet.of(file2, file3)); + + assertThat(formattingContext.getFileByUuid(file1.uuid()).get().name()).isNotEqualTo(file1.name()); + assertThat(formattingContext.getFileByUuid(file2.uuid()).get().name()).isEqualTo(file2.name()); + assertThat(formattingContext.getFileByUuid(file3.uuid()).get().name()).isEqualTo(file3.name()); + assertThat(formattingContext.getFileByUuid(file4.uuid()).get().name()).isNotEqualTo(file4.name()); + + // all files are preloaded + formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load, + emptySet(), ImmutableSet.of(file1, file2, file3, file4)); + + assertThat(formattingContext.getFileByUuid(file1.uuid()).get().name()).isEqualTo(file1.name()); + assertThat(formattingContext.getFileByUuid(file2.uuid()).get().name()).isEqualTo(file2.name()); + assertThat(formattingContext.getFileByUuid(file3.uuid()).get().name()).isEqualTo(file3.name()); + assertThat(formattingContext.getFileByUuid(file4.uuid()).get().name()).isEqualTo(file4.name()); + } + + @Test + @UseDataProvider("loadAllOrComments") + public void newFormattingContext_comments_without_userUuid_or_with_unknown_userUuid_are_not_updatable(Load load) { + IssueDto issue = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + String uuid = randomAlphabetic(30); + IssueChangeDto issueChangeUser1 = newComment(issue); + IssueChangeDto issueChangeUserUnknown = newComment(issue).setUserUuid(uuid); + insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUserUnknown)); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); + + assertThat(formattingContext.isUpdatableComment(issueChangeUser1)).isFalse(); + assertThat(formattingContext.isUpdatableComment(issueChangeUserUnknown)).isFalse(); + } + + @Test + @UseDataProvider("loadAllOrComments") + public void newFormattingContext_comments_with_userUuid_are_not_updatable_if_no_user_is_logged_in(Load load) { + IssueDto issue = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(); + String uuid = randomAlphabetic(30); + IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); + IssueChangeDto issueChangeUser2 = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto issueChangeUserUnknown = newComment(issue).setUserUuid(uuid); + insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2, issueChangeUserUnknown)); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); + + assertThat(formattingContext.isUpdatableComment(issueChangeUser1)).isFalse(); + assertThat(formattingContext.isUpdatableComment(issueChangeUser2)).isFalse(); + assertThat(formattingContext.isUpdatableComment(issueChangeUserUnknown)).isFalse(); + } + + @Test + @UseDataProvider("loadAllOrComments") + public void newFormattingContext_only_comments_of_logged_in_user_are_updatable(Load load) { + IssueDto issue = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(); + userSessionRule.logIn(user2); + String uuid = randomAlphabetic(30); + IssueChangeDto issueChangeUser1a = newComment(issue).setUserUuid(user1.getUuid()); + IssueChangeDto issueChangeUser1b = newComment(issue).setUserUuid(user1.getUuid()); + IssueChangeDto issueChangeUser2a = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto issueChangeUser2b = newComment(issue).setUserUuid(user2.getUuid()); + IssueChangeDto issueChangeUserUnknown = newComment(issue).setUserUuid(uuid); + insertInRandomOrder(Arrays.asList(issueChangeUser1a, issueChangeUser1b, issueChangeUser2a, issueChangeUser2b, issueChangeUserUnknown)); + + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); + + assertThat(formattingContext.isUpdatableComment(issueChangeUser1a)).isFalse(); + assertThat(formattingContext.isUpdatableComment(issueChangeUser1b)).isFalse(); + assertThat(formattingContext.isUpdatableComment(issueChangeUser2a)).isTrue(); + assertThat(formattingContext.isUpdatableComment(issueChangeUser2b)).isTrue(); + assertThat(formattingContext.isUpdatableComment(issueChangeUserUnknown)).isFalse(); + } + + @Test + @UseDataProvider("loadAllOrChangelog") + public void formatChangelog_returns_empty_if_context_has_no_changeLog_for_specified_IssueDto(Load load) { + IssueDto issue1 = dbTester.issues().insertIssue(); + IssueDto issue2 = dbTester.issues().insertIssue(); + List comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newComment(issue1).setKey("comment_" + i)) + .collect(Collectors.toList()); + List fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) + .mapToObj(i -> newFieldChange(issue1) + .setChangeData(new FieldDiffs() + .setDiff("f_change_" + i, null, null) + .toEncodedString())) + .collect(Collectors.toList()); + insertInRandomOrder(comments, fieldChanges); + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), Load.CHANGE_LOG); + + assertThat(underTest.formatChangelog(issue2, formattingContext)).isEmpty(); + assertThat(underTest.formatChangelog(issue1, formattingContext)).isNotEmpty(); + } + + @Test + @UseDataProvider("loadAllOrChangelog") + public void formatChangelog_returns_field_diff_details(Load load) { + IssueDto issue1 = dbTester.issues().insertIssue(); + int createdAt = 2_333_999; + IssueChangeDto issueChangeDto = dbTester.issues().insertChange(newFieldChange(issue1) + .setIssueChangeCreationDate(createdAt) + .setChangeData(new FieldDiffs() + .setDiff("f_change_1", "a", "b") + .setDiff("f_change_2", "c", null) + .setDiff("f_change_3", null, null) + .setDiff("f_change_4", null, "e") + .toEncodedString())); + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); + + List wsChangelogList = underTest.formatChangelog(issue1, formattingContext).collect(Collectors.toList()); + assertThat(wsChangelogList).hasSize(1); + Changelog wsChangelog = wsChangelogList.iterator().next(); + assertThat(wsChangelog.getCreationDate()).isEqualTo(formatDateTime(createdAt)); + assertThat(wsChangelog.getDiffsList()).hasSize(4); + assertThat(wsChangelog.getDiffsList().get(0).getKey()).isEqualTo("f_change_1"); + assertThat(wsChangelog.getDiffsList().get(0).getOldValue()).isEqualTo("a"); + assertThat(wsChangelog.getDiffsList().get(0).getNewValue()).isEqualTo("b"); + assertThat(wsChangelog.getDiffsList().get(1).getKey()).isEqualTo("f_change_2"); + assertThat(wsChangelog.getDiffsList().get(1).getOldValue()).isEqualTo("c"); + assertThat(wsChangelog.getDiffsList().get(1).hasNewValue()).isFalse(); + assertThat(wsChangelog.getDiffsList().get(2).getKey()).isEqualTo("f_change_3"); + assertThat(wsChangelog.getDiffsList().get(2).hasOldValue()).isFalse(); + assertThat(wsChangelog.getDiffsList().get(2).hasNewValue()).isFalse(); + assertThat(wsChangelog.getDiffsList().get(3).getKey()).isEqualTo("f_change_4"); + assertThat(wsChangelog.getDiffsList().get(3).hasOldValue()).isFalse(); + assertThat(wsChangelog.getDiffsList().get(3).getNewValue()).isEqualTo("e"); + } + + @Test + @UseDataProvider("loadAllOrChangelog") + public void formatChangelog_returns_user_details_if_exists(Load load) { + IssueDto issue1 = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(t -> t.setActive(false)); + String uuid = randomAlphabetic(22); + dbTester.issues().insertChange(newFieldChange(issue1) + .setUserUuid(user1.getUuid()) + .setChangeData(new FieldDiffs() + .setDiff("f_change_1", "a", "b") + .toEncodedString())); + dbTester.issues().insertChange(newFieldChange(issue1) + .setUserUuid(uuid) + .setChangeData(new FieldDiffs() + .setDiff("f_change_2", "a", "b") + .toEncodedString())); + dbTester.issues().insertChange(newFieldChange(issue1) + .setUserUuid(user2.getUuid()) + .setChangeData(new FieldDiffs() + .setDiff("f_change_3", "a", "b") + .toEncodedString())); + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); + + List wsChangelogList = underTest.formatChangelog(issue1, formattingContext).collect(Collectors.toList()); + assertThat(wsChangelogList) + .extracting(Changelog::hasUser, t -> t.getDiffsList().iterator().next().getKey()) + .containsExactlyInAnyOrder( + tuple(true, "f_change_1"), + tuple(false, "f_change_2"), + tuple(true, "f_change_3")); + assertThat(wsChangelogList.stream().filter(Changelog::hasUser)) + .extracting(Changelog::getUser, Changelog::getUserName, Changelog::getIsUserActive, Changelog::getAvatar) + .containsExactlyInAnyOrder( + tuple(user1.getLogin(), user1.getName(), true, avatarResolver.create(user1)), + tuple(user2.getLogin(), user2.getName(), false, avatarResolver.create(user2))); + } + + @Test + @UseDataProvider("loadAllOrChangelog") + public void formatChangelog_returns_no_avatar_if_email_is_not_set(Load load) { + IssueDto issue1 = dbTester.issues().insertIssue(); + UserDto user1 = dbTester.users().insertUser(t -> t.setEmail(null)); + dbTester.issues().insertChange(newFieldChange(issue1) + .setUserUuid(user1.getUuid()) + .setChangeData(new FieldDiffs() + .setDiff("f_change_1", "a", "b") + .toEncodedString())); + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); + + List wsChangelogList = underTest.formatChangelog(issue1, formattingContext).collect(Collectors.toList()); + assertThat(wsChangelogList).hasSize(1); + assertThat(wsChangelogList.iterator().next().hasAvatar()).isFalse(); + } + + @Test + @UseDataProvider("loadAllOrComments") + public void formatComments_returns_empty_if_context_has_no_comment_for_specified_IssueDto(Load load) { + IssueDto issue1 = dbTester.issues().insertIssue(); + IssueDto issue2 = dbTester.issues().insertIssue(); + IntStream.range(0, 1 + RANDOM.nextInt(22)) + .forEach(t -> dbTester.issues().insertChange(newComment(issue1))); + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); + + assertThat(underTest.formatComments(issue2, Comment.newBuilder(), formattingContext)).isEmpty(); + assertThat(underTest.formatComments(issue1, Comment.newBuilder(), formattingContext)).isNotEmpty(); + } + + @Test + @UseDataProvider("loadAllOrComments") + public void formatComments_returns_comment_markdown_and_html_when_available(Load load) { + IssueDto issue = dbTester.issues().insertIssue(); + IssueChangeDto withText = dbTester.issues().insertChange(newComment(issue).setChangeData("* foo")); + IssueChangeDto noText = dbTester.issues().insertChange(newComment(issue).setChangeData(null)); + FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); + + List comments = underTest.formatComments(issue, Comment.newBuilder(), formattingContext).collect(Collectors.toList()); + assertThat(comments) + .extracting(Comment::getKey, Comment::hasMarkdown, Comment::hasHtmlText) + .containsExactlyInAnyOrder( + tuple(withText.getKey(), true, true), + tuple(noText.getKey(), false, false) + ); + assertThat(comments.stream().filter(Comment::hasHtmlText)) + .extracting(Comment::getMarkdown, Comment::getHtmlText) + .containsOnly(tuple(withText.getChangeData(), Markdown.convertToHtml(withText.getChangeData()))); + } + + @DataProvider + public static Object[][] loadAllOrChangelog() { + return new Object[][] { + {Load.ALL}, + {Load.CHANGE_LOG} + }; + } + + @DataProvider + public static Object[][] loadAllOrComments() { + return new Object[][] { + {Load.ALL}, + {Load.COMMENTS} + }; + } + + private ComponentDto insertFile() { + return dbTester.components().insertComponent(ComponentTesting.newFileDto(ComponentTesting.newPublicProjectDto(OrganizationTesting.newOrganizationDto()))); + } + + private static IssueChangeDto newComment(IssueDto issue) { + return IssueTesting.newIssuechangeDto(issue).setChangeType(TYPE_COMMENT); + } + + private static IssueChangeDto newFieldChange(IssueDto issue) { + return IssueTesting.newIssuechangeDto(issue).setChangeType(TYPE_FIELD_CHANGE); + } + + @SafeVarargs + private final void insertInRandomOrder(Collection... changesLists) { + List all = new ArrayList<>(); + Arrays.stream(changesLists).forEach(all::addAll); + Collections.shuffle(all); + all.forEach(i -> dbTester.issues().insertChange(i)); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java index aa447b0402e..5a9f7cce012 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java @@ -38,7 +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.IssueChangeWSSupport; import org.sonar.server.issue.IssueFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -71,8 +71,8 @@ public class ChangelogActionTest { private ComponentDto project; private ComponentDto file; 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 IssueChangeWSSupport issueChangeSupport = new IssueChangeWSSupport(db.getDbClient(), new AvatarResolverImpl(), userSession); + private ChangelogAction underTest = new ChangelogAction(db.getDbClient(), issueFinder, userSession, issueChangeSupport); private WsActionTester tester = new WsActionTester(underTest); @Before diff --git a/sonar-ws/src/main/protobuf/ws-hotspots.proto b/sonar-ws/src/main/protobuf/ws-hotspots.proto index 1606042472f..51c3133b0ec 100644 --- a/sonar-ws/src/main/protobuf/ws-hotspots.proto +++ b/sonar-ws/src/main/protobuf/ws-hotspots.proto @@ -65,6 +65,7 @@ message ShowWsResponse { optional string updateDate = 12; optional sonarqube.ws.commons.TextRange textRange = 13; repeated sonarqube.ws.commons.Changelog changelog = 14; + repeated sonarqube.ws.commons.Comment comment = 15; } message Component {