org.sonar.server.issue.ws.SearchResponseLoader Maven / Gradle / Ivy
/*
* SonarQube
* Copyright (C) 2009-2018 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.ws;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.api.rule.RuleKey;
import org.sonar.core.issue.DefaultIssue;
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.protobuf.DbIssues;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.Facets;
import org.sonar.server.issue.TransitionService;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.client.issue.IssuesWsParameters;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.copyOf;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.difference;
import static java.util.Collections.emptyList;
import static java.util.stream.Stream.concat;
import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonar.server.issue.AssignAction.ASSIGN_KEY;
import static org.sonar.server.issue.CommentAction.COMMENT_KEY;
import static org.sonar.server.issue.SetSeverityAction.SET_SEVERITY_KEY;
import static org.sonar.server.issue.SetTypeAction.SET_TYPE_KEY;
import static org.sonar.server.issue.ws.SearchAdditionalField.ACTIONS;
import static org.sonar.server.issue.ws.SearchAdditionalField.COMMENTS;
import static org.sonar.server.issue.ws.SearchAdditionalField.RULES;
import static org.sonar.server.issue.ws.SearchAdditionalField.TRANSITIONS;
import static org.sonar.server.issue.ws.SearchAdditionalField.USERS;
/**
* Loads all the information required for the response of api/issues/search.
*/
public class SearchResponseLoader {
private final UserSession userSession;
private final DbClient dbClient;
private final TransitionService transitionService;
public SearchResponseLoader(UserSession userSession, DbClient dbClient, TransitionService transitionService) {
this.userSession = userSession;
this.dbClient = dbClient;
this.transitionService = transitionService;
}
/**
* The issue keys are given by the multi-criteria search in Elasticsearch index.
*/
public SearchResponseData load(Collector collector, @Nullable Facets facets) {
try (DbSession dbSession = dbClient.openSession(false)) {
SearchResponseData result = new SearchResponseData(dbClient.issueDao().selectByOrderedKeys(dbSession, collector.getIssueKeys()));
collector.collect(result.getIssues());
loadRules(collector, dbSession, result);
// order is important - loading of comments complete the list of users: loadComments() is
// before loadUsers()
loadComments(collector, dbSession, result);
loadUsers(collector, dbSession, result);
loadComponents(collector, dbSession, result);
loadOrganizations(dbSession, result);
loadActionsAndTransitions(collector, result);
completeTotalEffortFromFacet(facets, result);
return result;
}
}
/**
* The issue keys are given by the multi-criteria search in Elasticsearch index.
*
* Same as {@link #load(Collector, Facets)} but will only retrieve from DB data which is not already provided by the
* specified preloaded {@link SearchResponseData}.
* The returned {@link SearchResponseData} is not the one specified as argument.
*
*/
public SearchResponseData load(SearchResponseData preloadedResponseData, Collector collector, @Nullable Facets facets) {
try (DbSession dbSession = dbClient.openSession(false)) {
SearchResponseData result = new SearchResponseData(loadIssues(preloadedResponseData, collector, dbSession));
collector.collect(result.getIssues());
loadRules(preloadedResponseData, collector, dbSession, result);
// order is important - loading of comments complete the list of users: loadComments() is
// before loadUsers()
loadComments(collector, dbSession, result);
loadUsers(preloadedResponseData, collector, dbSession, result);
loadComponents(preloadedResponseData, collector, dbSession, result);
loadOrganizations(dbSession, result);
loadActionsAndTransitions(collector, result);
completeTotalEffortFromFacet(facets, result);
return result;
}
}
private List loadIssues(SearchResponseData preloadedResponseData, Collector collector, DbSession dbSession) {
List preloadedIssues = preloadedResponseData.getIssues();
Set preloadedIssueKeys = preloadedIssues.stream().map(IssueDto::getKey).collect(MoreCollectors.toSet(preloadedIssues.size()));
Set issueKeysToLoad = copyOf(difference(ImmutableSet.copyOf(collector.getIssueKeys()), preloadedIssueKeys));
if (issueKeysToLoad.isEmpty()) {
return preloadedIssues;
}
List loadedIssues = dbClient.issueDao().selectByKeys(dbSession, issueKeysToLoad);
return concat(preloadedIssues.stream(), loadedIssues.stream())
.collect(toList(preloadedIssues.size() + loadedIssues.size()));
}
private void loadUsers(SearchResponseData preloadedResponseData, Collector collector, DbSession dbSession, SearchResponseData result) {
if (collector.contains(USERS)) {
List preloadedUsers = firstNonNull(preloadedResponseData.getUsers(), emptyList());
Set preloadedLogins = preloadedUsers.stream().map(UserDto::getLogin).collect(MoreCollectors.toSet(preloadedUsers.size()));
Set requestedLogins = collector.get(USERS);
Set loginsToLoad = copyOf(difference(requestedLogins, preloadedLogins));
if (loginsToLoad.isEmpty()) {
result.setUsers(preloadedUsers);
} else {
List loadedUsers = dbClient.userDao().selectByLogins(dbSession, loginsToLoad);
result.setUsers(concat(preloadedUsers.stream(), loadedUsers.stream()).collect(toList(preloadedUsers.size() + loadedUsers.size())));
}
}
}
private void loadComponents(SearchResponseData preloadedResponseData, Collector collector, DbSession dbSession, SearchResponseData result) {
Collection preloadedComponents = preloadedResponseData.getComponents();
Set preloadedComponentUuids = preloadedComponents.stream().map(ComponentDto::uuid).collect(MoreCollectors.toSet(preloadedComponents.size()));
Set componentUuidsToLoad = copyOf(difference(collector.getComponentUuids(), preloadedComponentUuids));
result.addComponents(preloadedComponents);
if (!componentUuidsToLoad.isEmpty()) {
result.addComponents(dbClient.componentDao().selectByUuids(dbSession, componentUuidsToLoad));
}
// always load components and projects, because some issue fields still relate to component ids/keys.
// They should be dropped but are kept for backward-compatibility (see SearchResponseFormat)
result.addComponents(dbClient.componentDao().selectSubProjectsByComponentUuids(dbSession, collector.getComponentUuids()));
addProjectUuids(collector, dbSession, result);
}
private void addProjectUuids(Collector collector, DbSession dbSession, SearchResponseData result) {
Collection loadedComponents = result.getComponents();
for (ComponentDto component : loadedComponents) {
collector.addProjectUuid(component.projectUuid());
}
Set loadedProjectUuids = loadedComponents.stream().filter(cpt -> cpt.uuid().equals(cpt.projectUuid())).map(ComponentDto::uuid).collect(MoreCollectors.toSet());
Set projectUuidsToLoad = copyOf(difference(collector.getProjectUuids(), loadedProjectUuids));
if (!projectUuidsToLoad.isEmpty()) {
List projects = dbClient.componentDao().selectByUuids(dbSession, collector.getProjectUuids());
result.addComponents(projects);
}
}
private void loadRules(SearchResponseData preloadedResponseData, Collector collector, DbSession dbSession, SearchResponseData result) {
if (collector.contains(RULES)) {
List preloadedRules = firstNonNull(preloadedResponseData.getRules(), emptyList());
Set preloaedRuleKeys = preloadedRules.stream().map(RuleDefinitionDto::getKey).collect(MoreCollectors.toSet());
Set ruleKeysToLoad = copyOf(difference(collector.get(RULES), preloaedRuleKeys));
if (ruleKeysToLoad.isEmpty()) {
result.setRules(preloadedResponseData.getRules());
} else {
List loadedRules = dbClient.ruleDao().selectDefinitionByKeys(dbSession, ruleKeysToLoad);
result.setRules(concat(preloadedRules.stream(), loadedRules.stream()).collect(toList(preloadedRules.size() + loadedRules.size())));
}
}
}
private void loadUsers(Collector collector, DbSession dbSession, SearchResponseData result) {
if (collector.contains(USERS)) {
result.setUsers(dbClient.userDao().selectByLogins(dbSession, collector.getList(USERS)));
}
}
private void loadComments(Collector collector, DbSession dbSession, SearchResponseData result) {
if (collector.contains(COMMENTS)) {
List comments = dbClient.issueChangeDao().selectByTypeAndIssueKeys(dbSession, collector.getIssueKeys(), IssueChangeDto.TYPE_COMMENT);
result.setComments(comments);
for (IssueChangeDto comment : comments) {
collector.add(USERS, comment.getUserLogin());
if (canEditOrDelete(comment)) {
result.addUpdatableComment(comment.getKey());
}
}
}
}
private boolean canEditOrDelete(IssueChangeDto dto) {
return userSession.isLoggedIn() && userSession.getLogin().equals(dto.getUserLogin());
}
private void loadRules(Collector collector, DbSession dbSession, SearchResponseData result) {
if (collector.contains(RULES)) {
result.setRules(dbClient.ruleDao().selectDefinitionByKeys(dbSession, collector.getList(RULES)));
}
}
private void loadComponents(Collector collector, DbSession dbSession, SearchResponseData result) {
// always load components and projects, because some issue fields still relate to component ids/keys.
// They should be dropped but are kept for backward-compatibility (see SearchResponseFormat)
result.addComponents(dbClient.componentDao().selectByUuids(dbSession, collector.getComponentUuids()));
result.addComponents(dbClient.componentDao().selectSubProjectsByComponentUuids(dbSession, collector.getComponentUuids()));
addProjectUuids(collector, dbSession, result);
}
private void loadOrganizations(DbSession dbSession, SearchResponseData result) {
Collection components = result.getComponents();
dbClient.organizationDao().selectByUuids(
dbSession,
components.stream().map(ComponentDto::getOrganizationUuid).collect(MoreCollectors.toSet()))
.forEach(result::addOrganization);
}
private void loadActionsAndTransitions(Collector collector, SearchResponseData result) {
if (collector.contains(ACTIONS) || collector.contains(TRANSITIONS)) {
Map componentsByProjectUuid =
result.getComponents()
.stream()
.filter(ComponentDto::isRootProject)
.collect(MoreCollectors.uniqueIndex(ComponentDto::projectUuid));
for (IssueDto dto : result.getIssues()) {
// so that IssueDto can be used.
if (collector.contains(ACTIONS)) {
ComponentDto project = componentsByProjectUuid.get(dto.getProjectUuid());
result.addActions(dto.getKey(), listAvailableActions(dto, project));
}
if (collector.contains(TRANSITIONS)) {
// TODO workflow and action engines must not depend on org.sonar.api.issue.Issue but on a generic interface
DefaultIssue issue = dto.toDefaultIssue();
result.addTransitions(issue.key(), transitionService.listTransitions(issue));
}
}
}
}
private List listAvailableActions(IssueDto issue, ComponentDto project) {
List availableActions = newArrayList();
String login = userSession.getLogin();
if (login == null) {
return Collections.emptyList();
}
availableActions.add(COMMENT_KEY);
if (issue.getResolution() != null) {
return availableActions;
}
availableActions.add(ASSIGN_KEY);
availableActions.add("set_tags");
if (userSession.hasComponentPermission(ISSUE_ADMIN, project)) {
availableActions.add(SET_TYPE_KEY);
availableActions.add(SET_SEVERITY_KEY);
}
return availableActions;
}
private static void completeTotalEffortFromFacet(@Nullable Facets facets, SearchResponseData result) {
if (facets != null) {
Map effortFacet = facets.get(IssuesWsParameters.FACET_MODE_EFFORT);
if (effortFacet != null) {
result.setEffortTotal(effortFacet.get(Facets.TOTAL));
}
}
}
/**
* Collects the keys of all the data to be loaded (users, rules, ...)
*/
public static class Collector {
private final Set fields;
private final SetMultimap fieldValues = MultimapBuilder.enumKeys(SearchAdditionalField.class).hashSetValues().build();
private final Set componentUuids = new HashSet<>();
private final Set projectUuids = new HashSet<>();
private final List issueKeys;
public Collector(Set fields, List issueKeys) {
this.fields = fields;
this.issueKeys = issueKeys;
}
void collect(List issues) {
for (IssueDto issue : issues) {
componentUuids.add(issue.getComponentUuid());
projectUuids.add(issue.getProjectUuid());
add(RULES, issue.getRuleKey());
add(USERS, issue.getAssignee());
collectComponentsFromIssueLocations(issue);
}
}
private void collectComponentsFromIssueLocations(IssueDto issue) {
DbIssues.Locations locations = issue.parseLocations();
if (locations != null) {
for (DbIssues.Flow flow : locations.getFlowList()) {
for (DbIssues.Location location : flow.getLocationList()) {
if (location.hasComponentId()) {
componentUuids.add(location.getComponentId());
}
}
}
}
}
public void add(SearchAdditionalField key, @Nullable Object value) {
if (value != null) {
fieldValues.put(key, value);
}
}
public void addComponentUuids(@Nullable Collection uuids) {
if (uuids != null) {
this.componentUuids.addAll(uuids);
}
}
public void addProjectUuid(String uuid) {
this.projectUuids.add(uuid);
}
public void addProjectUuids(@Nullable Collection uuids) {
if (uuids != null) {
this.projectUuids.addAll(uuids);
}
}
public void addAll(SearchAdditionalField key, @Nullable Iterable values) {
if (values != null) {
for (Object value : values) {
add(key, value);
}
}
}
Set get(SearchAdditionalField key) {
return (Set) fieldValues.get(key);
}
List getList(SearchAdditionalField key) {
return newArrayList(get(key));
}
boolean contains(SearchAdditionalField field) {
return fields.contains(field);
}
public List getIssueKeys() {
return issueKeys;
}
public Set getComponentUuids() {
return componentUuids;
}
public Set getProjectUuids() {
return projectUuids;
}
}
}