@@ -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) |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -19,61 +19,40 @@ | |||
*/ | |||
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()); | |||
} | |||
} |
@@ -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, |
@@ -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()); | |||
} | |||
} | |||
} |
@@ -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() { |
@@ -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); | |||
} | |||
} | |||
@@ -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 { |