123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- /*
- * SonarQube
- * Copyright (C) 2009-2023 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 java.util.ArrayList;
- import java.util.Collection;
- import java.util.Date;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Optional;
- import java.util.Set;
- import java.util.stream.Collectors;
- import org.sonar.api.resources.Language;
- import org.sonar.api.resources.Languages;
- import org.sonar.api.rule.RuleKey;
- import org.sonar.api.rules.RuleType;
- import org.sonar.api.utils.DateUtils;
- import org.sonar.api.utils.Duration;
- import org.sonar.api.utils.Durations;
- import org.sonar.api.utils.Paging;
- import org.sonar.db.component.BranchDto;
- import org.sonar.db.component.BranchType;
- import org.sonar.db.component.ComponentDto;
- import org.sonar.db.issue.IssueChangeDto;
- import org.sonar.db.issue.IssueDto;
- import org.sonar.db.project.ProjectDto;
- import org.sonar.db.protobuf.DbIssues;
- import org.sonar.db.rule.RuleDto;
- import org.sonar.db.user.UserDto;
- import org.sonar.markdown.Markdown;
- import org.sonar.server.es.Facets;
- import org.sonar.server.issue.TextRangeResponseFormatter;
- import org.sonar.server.issue.index.IssueScope;
- import org.sonar.server.issue.workflow.Transition;
- import org.sonar.server.ws.MessageFormattingUtils;
- import org.sonarqube.ws.Common;
- import org.sonarqube.ws.Common.Comment;
- import org.sonarqube.ws.Common.User;
- import org.sonarqube.ws.Issues;
- import org.sonarqube.ws.Issues.Actions;
- import org.sonarqube.ws.Issues.Comments;
- import org.sonarqube.ws.Issues.Component;
- import org.sonarqube.ws.Issues.Issue;
- import org.sonarqube.ws.Issues.Operation;
- import org.sonarqube.ws.Issues.SearchWsResponse;
- import org.sonarqube.ws.Issues.Transitions;
- import org.sonarqube.ws.Issues.Users;
-
- import static com.google.common.base.MoreObjects.firstNonNull;
- import static com.google.common.base.Preconditions.checkState;
- import static com.google.common.base.Strings.emptyToNull;
- import static com.google.common.base.Strings.nullToEmpty;
- import static java.lang.String.format;
- import static java.util.Collections.emptyList;
- import static java.util.Objects.requireNonNull;
- import static java.util.Optional.ofNullable;
- import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE;
- import static org.sonar.api.rule.RuleKey.EXTERNAL_RULE_REPO_PREFIX;
- import static org.sonar.server.issue.index.IssueIndex.FACET_ASSIGNED_TO_ME;
- import static org.sonar.server.issue.index.IssueIndex.FACET_PROJECTS;
- import static org.sonar.server.issue.ws.SearchAdditionalField.ACTIONS;
- import static org.sonar.server.issue.ws.SearchAdditionalField.ALL_ADDITIONAL_FIELDS;
- import static org.sonar.server.issue.ws.SearchAdditionalField.COMMENTS;
- import static org.sonar.server.issue.ws.SearchAdditionalField.RULE_DESCRIPTION_CONTEXT_KEY;
- import static org.sonar.server.issue.ws.SearchAdditionalField.TRANSITIONS;
- import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
- import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;
-
- public class SearchResponseFormat {
-
- private final Durations durations;
- private final Languages languages;
- private final TextRangeResponseFormatter textRangeFormatter;
- private final UserResponseFormatter userFormatter;
-
- public SearchResponseFormat(Durations durations, Languages languages, TextRangeResponseFormatter textRangeFormatter, UserResponseFormatter userFormatter) {
- this.durations = durations;
- this.languages = languages;
- this.textRangeFormatter = textRangeFormatter;
- this.userFormatter = userFormatter;
- }
-
- SearchWsResponse formatSearch(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging, Facets facets) {
- SearchWsResponse.Builder response = SearchWsResponse.newBuilder();
-
- response.setPaging(formatPaging(paging));
- ofNullable(data.getEffortTotal()).ifPresent(response::setEffortTotal);
- response.addAllIssues(createIssues(fields, data));
- response.addAllComponents(formatComponents(data));
- formatFacets(data, facets, response);
- if (fields.contains(SearchAdditionalField.RULES)) {
- response.setRules(formatRules(data));
- }
- if (fields.contains(SearchAdditionalField.USERS)) {
- response.setUsers(formatUsers(data));
- }
- if (fields.contains(SearchAdditionalField.LANGUAGES)) {
- response.setLanguages(formatLanguages());
- }
- return response.build();
- }
-
- Issues.ListWsResponse formatList(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging) {
- Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder();
-
- response.setPaging(Common.Paging.newBuilder()
- .setPageIndex(paging.pageIndex())
- .setPageSize(data.getIssues().size()));
- response.addAllIssues(createIssues(fields, data));
- response.addAllComponents(formatComponents(data));
- return response.build();
- }
-
- Operation formatOperation(SearchResponseData data) {
- Operation.Builder response = Operation.newBuilder();
-
- if (data.getIssues().size() == 1) {
- IssueDto dto = data.getIssues().get(0);
- response.setIssue(createIssue(ALL_ADDITIONAL_FIELDS, data, dto));
- }
- response.addAllComponents(formatComponents(data));
- response.addAllRules(formatRules(data).getRulesList());
- response.addAllUsers(formatUsers(data).getUsersList());
- return response.build();
- }
-
- private static Common.Paging.Builder formatPaging(Paging paging) {
- return Common.Paging.newBuilder()
- .setPageIndex(paging.pageIndex())
- .setPageSize(paging.pageSize())
- .setTotal(paging.total());
- }
-
- private List<Issues.Issue> createIssues(Collection<SearchAdditionalField> fields, SearchResponseData data) {
- return data.getIssues().stream()
- .map(dto -> createIssue(fields, data, dto))
- .toList();
- }
-
- private Issue createIssue(Collection<SearchAdditionalField> fields, SearchResponseData data, IssueDto dto) {
- Issue.Builder issueBuilder = Issue.newBuilder();
- addMandatoryFieldsToIssueBuilder(issueBuilder, dto, data);
- addAdditionalFieldsToIssueBuilder(fields, data, dto, issueBuilder);
- return issueBuilder.build();
- }
-
- private void addMandatoryFieldsToIssueBuilder(Issue.Builder issueBuilder, IssueDto dto, SearchResponseData data) {
- issueBuilder.setKey(dto.getKey());
- issueBuilder.setType(Common.RuleType.forNumber(dto.getType()));
-
- ComponentDto component = data.getComponentByUuid(dto.getComponentUuid());
- issueBuilder.setComponent(component.getKey());
- setBranchOrPr(component, issueBuilder, data);
- ComponentDto branch = data.getComponentByUuid(dto.getProjectUuid());
- if (branch != null) {
- issueBuilder.setProject(branch.getKey());
- }
- issueBuilder.setRule(dto.getRuleKey().toString());
- if (dto.isExternal()) {
- issueBuilder.setExternalRuleEngine(engineNameFrom(dto.getRuleKey()));
- }
- if (dto.getType() != RuleType.SECURITY_HOTSPOT.getDbConstant()) {
- issueBuilder.setSeverity(Common.Severity.valueOf(dto.getSeverity()));
- }
- ofNullable(data.getUserByUuid(dto.getAssigneeUuid())).ifPresent(assignee -> issueBuilder.setAssignee(assignee.getLogin()));
- ofNullable(emptyToNull(dto.getResolution())).ifPresent(issueBuilder::setResolution);
- issueBuilder.setStatus(dto.getStatus());
- issueBuilder.setMessage(nullToEmpty(dto.getMessage()));
- issueBuilder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(dto.parseMessageFormattings()));
- issueBuilder.addAllTags(dto.getTags());
- issueBuilder.addAllCodeVariants(dto.getCodeVariants());
- Long effort = dto.getEffort();
- if (effort != null) {
- String effortValue = durations.encode(Duration.create(effort));
- issueBuilder.setDebt(effortValue);
- issueBuilder.setEffort(effortValue);
- }
- ofNullable(dto.getLine()).ifPresent(issueBuilder::setLine);
- ofNullable(emptyToNull(dto.getChecksum())).ifPresent(issueBuilder::setHash);
- completeIssueLocations(dto, issueBuilder, data);
-
- issueBuilder.setAuthor(nullToEmpty(dto.getAuthorLogin()));
- ofNullable(dto.getIssueCreationDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setCreationDate);
- ofNullable(dto.getIssueUpdateDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setUpdateDate);
- ofNullable(dto.getIssueCloseDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setCloseDate);
-
- Optional.of(dto.isQuickFixAvailable())
- .ifPresentOrElse(issueBuilder::setQuickFixAvailable, () -> issueBuilder.setQuickFixAvailable(false));
-
- issueBuilder.setScope(UNIT_TEST_FILE.equals(component.qualifier()) ? IssueScope.TEST.name() : IssueScope.MAIN.name());
- }
-
- private static void addAdditionalFieldsToIssueBuilder(Collection<SearchAdditionalField> fields, SearchResponseData data, IssueDto dto, Issue.Builder issueBuilder) {
- if (fields.contains(ACTIONS)) {
- issueBuilder.setActions(createIssueActions(data, dto));
- }
- if (fields.contains(TRANSITIONS)) {
- issueBuilder.setTransitions(createIssueTransition(data, dto));
- }
- if (fields.contains(COMMENTS)) {
- issueBuilder.setComments(createIssueComments(data, dto));
- }
- if (fields.contains(RULE_DESCRIPTION_CONTEXT_KEY)) {
- dto.getOptionalRuleDescriptionContextKey().ifPresent(issueBuilder::setRuleDescriptionContextKey);
- }
- }
-
- private static String engineNameFrom(RuleKey ruleKey) {
- checkState(ruleKey.repository().startsWith(EXTERNAL_RULE_REPO_PREFIX));
- return ruleKey.repository().replace(EXTERNAL_RULE_REPO_PREFIX, "");
- }
-
- private void completeIssueLocations(IssueDto dto, Issue.Builder issueBuilder, SearchResponseData data) {
- DbIssues.Locations locations = dto.parseLocations();
- if (locations == null) {
- return;
- }
- textRangeFormatter.formatTextRange(locations, issueBuilder::setTextRange);
- issueBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, issueBuilder.getComponent(), data.getComponentsByUuid()));
- }
-
- private static Transitions createIssueTransition(SearchResponseData data, IssueDto dto) {
- Transitions.Builder wsTransitions = Transitions.newBuilder();
- List<Transition> transitions = data.getTransitionsForIssueKey(dto.getKey());
- if (transitions != null) {
- for (Transition transition : transitions) {
- wsTransitions.addTransitions(transition.key());
- }
- }
- return wsTransitions.build();
- }
-
- private static Actions createIssueActions(SearchResponseData data, IssueDto dto) {
- Actions.Builder wsActions = Actions.newBuilder();
- List<String> actions = data.getActionsForIssueKey(dto.getKey());
- if (actions != null) {
- wsActions.addAllActions(actions);
- }
- return wsActions.build();
- }
-
- private static Comments createIssueComments(SearchResponseData data, IssueDto dto) {
- Comments.Builder wsComments = Comments.newBuilder();
- List<IssueChangeDto> comments = data.getCommentsForIssueKey(dto.getKey());
- if (comments != null) {
- Comment.Builder wsComment = Comment.newBuilder();
- for (IssueChangeDto comment : comments) {
- String markdown = comment.getChangeData();
- wsComment
- .clear()
- .setKey(comment.getKey())
- .setUpdatable(data.isUpdatableComment(comment.getKey()))
- .setCreatedAt(DateUtils.formatDateTime(new Date(comment.getIssueChangeCreationDate())));
- ofNullable(data.getUserByUuid(comment.getUserUuid())).ifPresent(user -> wsComment.setLogin(user.getLogin()));
- if (markdown != null) {
- wsComment
- .setHtmlText(Markdown.convertToHtml(markdown))
- .setMarkdown(markdown);
- }
- wsComments.addComments(wsComment);
- }
- }
- return wsComments.build();
- }
-
- private Common.Rules.Builder formatRules(SearchResponseData data) {
- Common.Rules.Builder wsRules = Common.Rules.newBuilder();
- List<RuleDto> rules = firstNonNull(data.getRules(), emptyList());
- for (RuleDto rule : rules) {
- wsRules.addRules(formatRule(rule));
- }
- return wsRules;
- }
-
- private Common.Rule.Builder formatRule(RuleDto rule) {
- Common.Rule.Builder builder = Common.Rule.newBuilder()
- .setKey(rule.getKey().toString())
- .setName(nullToEmpty(rule.getName()))
- .setStatus(Common.RuleStatus.valueOf(rule.getStatus().name()));
-
- builder.setLang(nullToEmpty(rule.getLanguage()));
- Language lang = languages.get(rule.getLanguage());
- if (lang != null) {
- builder.setLangName(lang.getName());
- }
- return builder;
- }
-
- private static List<Issues.Component> formatComponents(SearchResponseData data) {
- Collection<ComponentDto> components = data.getComponents();
- List<Issues.Component> result = new ArrayList<>();
- for (ComponentDto dto : components) {
- Component.Builder builder = Component.newBuilder()
- .setKey(dto.getKey())
- .setQualifier(dto.qualifier())
- .setName(nullToEmpty(dto.name()))
- .setLongName(nullToEmpty(dto.longName()))
- .setEnabled(dto.isEnabled());
- setBranchOrPr(dto, builder, data);
- ofNullable(emptyToNull(dto.path())).ifPresent(builder::setPath);
-
- result.add(builder.build());
- }
- return result;
- }
-
- private static void setBranchOrPr(ComponentDto componentDto, Component.Builder builder, SearchResponseData data) {
- String branchUuid = componentDto.getCopyComponentUuid() != null ? componentDto.getCopyComponentUuid() : componentDto.branchUuid();
- BranchDto branchDto = data.getBranch(branchUuid);
- if (branchDto.isMain()) {
- return;
- }
- if (branchDto.getBranchType() == BranchType.BRANCH) {
- builder.setBranch(branchDto.getKey());
- } else if (branchDto.getBranchType() == BranchType.PULL_REQUEST) {
- builder.setPullRequest(branchDto.getKey());
- }
- }
-
- private static void setBranchOrPr(ComponentDto componentDto, Issue.Builder builder, SearchResponseData data) {
- String branchUuid = componentDto.getCopyComponentUuid() != null ? componentDto.getCopyComponentUuid() : componentDto.branchUuid();
- BranchDto branchDto = data.getBranch(branchUuid);
- if (branchDto.isMain()) {
- return;
- }
- if (branchDto.getBranchType() == BranchType.BRANCH) {
- builder.setBranch(branchDto.getKey());
- } else if (branchDto.getBranchType() == BranchType.PULL_REQUEST) {
- builder.setPullRequest(branchDto.getKey());
- }
- }
-
- private Users.Builder formatUsers(SearchResponseData data) {
- Users.Builder wsUsers = Users.newBuilder();
- List<UserDto> users = data.getUsers();
- if (users != null) {
- User.Builder builder = User.newBuilder();
- for (UserDto user : users) {
- wsUsers.addUsers(userFormatter.formatUser(builder, user));
- }
- }
- return wsUsers;
- }
-
- private Issues.Languages.Builder formatLanguages() {
- Issues.Languages.Builder wsLangs = Issues.Languages.newBuilder();
- Issues.Language.Builder wsLang = Issues.Language.newBuilder();
- for (Language lang : languages.all()) {
- wsLang
- .clear()
- .setKey(lang.getKey())
- .setName(lang.getName());
- wsLangs.addLanguages(wsLang);
- }
- return wsLangs;
- }
-
- private static void formatFacets(SearchResponseData data, Facets facets, SearchWsResponse.Builder wsSearch) {
- Common.Facets.Builder wsFacets = Common.Facets.newBuilder();
- SearchAction.SUPPORTED_FACETS.stream()
- .filter(f -> !f.equals(FACET_PROJECTS))
- .filter(f -> !f.equals(FACET_ASSIGNED_TO_ME))
- .filter(f -> !f.equals(PARAM_ASSIGNEES))
- .filter(f -> !f.equals(PARAM_RULES))
- .forEach(f -> computeStandardFacet(wsFacets, facets, f));
- computeAssigneesFacet(wsFacets, facets, data);
- computeAssignedToMeFacet(wsFacets, facets, data);
- computeRulesFacet(wsFacets, facets, data);
- computeProjectsFacet(wsFacets, facets, data);
- wsSearch.setFacets(wsFacets.build());
- }
-
- private static void computeStandardFacet(Common.Facets.Builder wsFacets, Facets facets, String facetKey) {
- LinkedHashMap<String, Long> facet = facets.get(facetKey);
- if (facet == null) {
- return;
- }
- Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
- wsFacet.setProperty(facetKey);
- facet.forEach((value, count) -> wsFacet.addValuesBuilder()
- .setVal(value)
- .setCount(count)
- .build());
- wsFacet.build();
- }
-
- private static void computeAssigneesFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData data) {
- LinkedHashMap<String, Long> facet = facets.get(PARAM_ASSIGNEES);
- if (facet == null) {
- return;
- }
- Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
- wsFacet.setProperty(PARAM_ASSIGNEES);
- facet
- .forEach((userUuid, count) -> {
- UserDto user = data.getUserByUuid(userUuid);
- wsFacet.addValuesBuilder()
- .setVal(user == null ? "" : user.getLogin())
- .setCount(count)
- .build();
- });
- wsFacet.build();
- }
-
- private static void computeAssignedToMeFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData data) {
- LinkedHashMap<String, Long> facet = facets.get(FACET_ASSIGNED_TO_ME);
- if (facet == null) {
- return;
- }
- Map.Entry<String, Long> entry = facet.entrySet().iterator().next();
- UserDto user = data.getUserByUuid(entry.getKey());
- checkState(user != null, "User with uuid '%s' has not been found", entry.getKey());
-
- Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
- wsFacet.setProperty(FACET_ASSIGNED_TO_ME);
- wsFacet.addValuesBuilder()
- .setVal(user.getLogin())
- .setCount(entry.getValue())
- .build();
- }
-
- private static void computeRulesFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData data) {
- LinkedHashMap<String, Long> facet = facets.get(PARAM_RULES);
- if (facet == null) {
- return;
- }
-
- Map<String, RuleKey> ruleUuidsByRuleKeys = data.getRules().stream().collect(Collectors.toMap(RuleDto::getUuid, RuleDto::getKey));
- Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
- wsFacet.setProperty(PARAM_RULES);
- facet.forEach((ruleUuid, count) -> wsFacet.addValuesBuilder()
- .setVal(ruleUuidsByRuleKeys.get(ruleUuid).toString())
- .setCount(count)
- .build());
- wsFacet.build();
- }
-
- private static void computeProjectsFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData datas) {
- LinkedHashMap<String, Long> facet = facets.get(FACET_PROJECTS);
- if (facet == null) {
- return;
- }
- Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
- wsFacet.setProperty(FACET_PROJECTS);
- facet.forEach((uuid, count) -> {
- ProjectDto project = datas.getProject(uuid);
- requireNonNull(project, format("Project has not been found for uuid '%s'", uuid));
- wsFacet.addValuesBuilder()
- .setVal(project.getKey())
- .setCount(count)
- .build();
- });
- wsFacet.build();
- }
-
- }
|