/* * SonarQube * Copyright (C) 2009-2017 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.MultimapBuilder; import com.google.common.collect.SetMultimap; import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; 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.server.es.Facets; import org.sonar.server.issue.ActionFinder; import org.sonar.server.issue.TransitionService; import org.sonar.server.user.UserSession; import org.sonarqube.ws.client.issue.IssuesWsParameters; import static com.google.common.collect.Lists.newArrayList; 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 ActionFinder actionService; private final TransitionService transitionService; public SearchResponseLoader(UserSession userSession, DbClient dbClient, ActionFinder actionService, TransitionService transitionService) { this.userSession = userSession; this.dbClient = dbClient; this.actionService = actionService; 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; } } private void loadUsers(Collector collector, DbSession dbSession, SearchResponseData result) { if (collector.contains(USERS)) { result.setUsers(dbClient.userDao().selectByLogins(dbSession, collector.<String>get(USERS))); } } private void loadComments(Collector collector, DbSession dbSession, SearchResponseData result) { if (collector.contains(COMMENTS)) { List<IssueChangeDto> 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.get(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())); for (ComponentDto component : result.getComponents()) { collector.addProjectUuid(component.projectUuid()); } List<ComponentDto> projects = dbClient.componentDao().selectByUuids(dbSession, collector.getProjectUuids()); result.addComponents(projects); } private void loadOrganizations(DbSession dbSession, SearchResponseData result) { Collection<ComponentDto> components = result.getComponents(); if (components == null) { return; } 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)) { for (IssueDto dto : result.getIssues()) { // so that IssueDto can be used. if (collector.contains(ACTIONS)) { result.addActions(dto.getKey(), actionService.listAvailableActions(dto)); } 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 static void completeTotalEffortFromFacet(@Nullable Facets facets, SearchResponseData result) { if (facets != null) { Map<String, Long> 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 EnumSet<SearchAdditionalField> fields; private final SetMultimap<SearchAdditionalField, Object> fieldValues = MultimapBuilder.enumKeys(SearchAdditionalField.class).hashSetValues().build(); private final Set<String> componentUuids = new HashSet<>(); private final Set<String> projectUuids = new HashSet<>(); private final List<String> issueKeys; public Collector(EnumSet<SearchAdditionalField> fields, List<String> issueKeys) { this.fields = fields; this.issueKeys = issueKeys; } void collect(List<IssueDto> 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<String> uuids) { if (uuids != null) { this.componentUuids.addAll(uuids); } } public void addProjectUuid(String uuid) { this.projectUuids.add(uuid); } public void addProjectUuids(@Nullable Collection<String> 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); } } } <T> List<T> get(SearchAdditionalField key) { return newArrayList((Set<T>) fieldValues.get(key)); } boolean contains(SearchAdditionalField field) { return fields.contains(field); } public List<String> getIssueKeys() { return issueKeys; } public Set<String> getComponentUuids() { return componentUuids; } public Set<String> getProjectUuids() { return projectUuids; } } }