You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SearchResponseFormat.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.issue.ws;
  21. import java.util.ArrayList;
  22. import java.util.Collection;
  23. import java.util.Date;
  24. import java.util.LinkedHashMap;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Optional;
  28. import java.util.Set;
  29. import java.util.stream.Collectors;
  30. import org.sonar.api.resources.Language;
  31. import org.sonar.api.resources.Languages;
  32. import org.sonar.api.rule.RuleKey;
  33. import org.sonar.api.rules.RuleType;
  34. import org.sonar.api.utils.DateUtils;
  35. import org.sonar.api.utils.Duration;
  36. import org.sonar.api.utils.Durations;
  37. import org.sonar.api.utils.Paging;
  38. import org.sonar.db.component.BranchDto;
  39. import org.sonar.db.component.BranchType;
  40. import org.sonar.db.component.ComponentDto;
  41. import org.sonar.db.issue.IssueChangeDto;
  42. import org.sonar.db.issue.IssueDto;
  43. import org.sonar.db.project.ProjectDto;
  44. import org.sonar.db.protobuf.DbIssues;
  45. import org.sonar.db.rule.RuleDto;
  46. import org.sonar.db.user.UserDto;
  47. import org.sonar.markdown.Markdown;
  48. import org.sonar.server.es.Facets;
  49. import org.sonar.server.issue.TextRangeResponseFormatter;
  50. import org.sonar.server.issue.index.IssueScope;
  51. import org.sonar.server.issue.workflow.Transition;
  52. import org.sonar.server.ws.MessageFormattingUtils;
  53. import org.sonarqube.ws.Common;
  54. import org.sonarqube.ws.Common.Comment;
  55. import org.sonarqube.ws.Common.User;
  56. import org.sonarqube.ws.Issues;
  57. import org.sonarqube.ws.Issues.Actions;
  58. import org.sonarqube.ws.Issues.Comments;
  59. import org.sonarqube.ws.Issues.Component;
  60. import org.sonarqube.ws.Issues.Issue;
  61. import org.sonarqube.ws.Issues.Operation;
  62. import org.sonarqube.ws.Issues.SearchWsResponse;
  63. import org.sonarqube.ws.Issues.Transitions;
  64. import org.sonarqube.ws.Issues.Users;
  65. import static com.google.common.base.MoreObjects.firstNonNull;
  66. import static com.google.common.base.Preconditions.checkState;
  67. import static com.google.common.base.Strings.emptyToNull;
  68. import static com.google.common.base.Strings.nullToEmpty;
  69. import static java.lang.String.format;
  70. import static java.util.Collections.emptyList;
  71. import static java.util.Objects.requireNonNull;
  72. import static java.util.Optional.ofNullable;
  73. import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE;
  74. import static org.sonar.api.rule.RuleKey.EXTERNAL_RULE_REPO_PREFIX;
  75. import static org.sonar.server.issue.index.IssueIndex.FACET_ASSIGNED_TO_ME;
  76. import static org.sonar.server.issue.index.IssueIndex.FACET_PROJECTS;
  77. import static org.sonar.server.issue.ws.SearchAdditionalField.ACTIONS;
  78. import static org.sonar.server.issue.ws.SearchAdditionalField.ALL_ADDITIONAL_FIELDS;
  79. import static org.sonar.server.issue.ws.SearchAdditionalField.COMMENTS;
  80. import static org.sonar.server.issue.ws.SearchAdditionalField.RULE_DESCRIPTION_CONTEXT_KEY;
  81. import static org.sonar.server.issue.ws.SearchAdditionalField.TRANSITIONS;
  82. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
  83. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;
  84. public class SearchResponseFormat {
  85. private final Durations durations;
  86. private final Languages languages;
  87. private final TextRangeResponseFormatter textRangeFormatter;
  88. private final UserResponseFormatter userFormatter;
  89. public SearchResponseFormat(Durations durations, Languages languages, TextRangeResponseFormatter textRangeFormatter, UserResponseFormatter userFormatter) {
  90. this.durations = durations;
  91. this.languages = languages;
  92. this.textRangeFormatter = textRangeFormatter;
  93. this.userFormatter = userFormatter;
  94. }
  95. SearchWsResponse formatSearch(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging, Facets facets) {
  96. SearchWsResponse.Builder response = SearchWsResponse.newBuilder();
  97. response.setPaging(formatPaging(paging));
  98. ofNullable(data.getEffortTotal()).ifPresent(response::setEffortTotal);
  99. response.addAllIssues(createIssues(fields, data));
  100. response.addAllComponents(formatComponents(data));
  101. formatFacets(data, facets, response);
  102. if (fields.contains(SearchAdditionalField.RULES)) {
  103. response.setRules(formatRules(data));
  104. }
  105. if (fields.contains(SearchAdditionalField.USERS)) {
  106. response.setUsers(formatUsers(data));
  107. }
  108. if (fields.contains(SearchAdditionalField.LANGUAGES)) {
  109. response.setLanguages(formatLanguages());
  110. }
  111. return response.build();
  112. }
  113. Issues.ListWsResponse formatList(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging) {
  114. Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder();
  115. response.setPaging(Common.Paging.newBuilder()
  116. .setPageIndex(paging.pageIndex())
  117. .setPageSize(data.getIssues().size()));
  118. response.addAllIssues(createIssues(fields, data));
  119. response.addAllComponents(formatComponents(data));
  120. return response.build();
  121. }
  122. Operation formatOperation(SearchResponseData data) {
  123. Operation.Builder response = Operation.newBuilder();
  124. if (data.getIssues().size() == 1) {
  125. IssueDto dto = data.getIssues().get(0);
  126. response.setIssue(createIssue(ALL_ADDITIONAL_FIELDS, data, dto));
  127. }
  128. response.addAllComponents(formatComponents(data));
  129. response.addAllRules(formatRules(data).getRulesList());
  130. response.addAllUsers(formatUsers(data).getUsersList());
  131. return response.build();
  132. }
  133. private static Common.Paging.Builder formatPaging(Paging paging) {
  134. return Common.Paging.newBuilder()
  135. .setPageIndex(paging.pageIndex())
  136. .setPageSize(paging.pageSize())
  137. .setTotal(paging.total());
  138. }
  139. private List<Issues.Issue> createIssues(Collection<SearchAdditionalField> fields, SearchResponseData data) {
  140. return data.getIssues().stream()
  141. .map(dto -> createIssue(fields, data, dto))
  142. .toList();
  143. }
  144. private Issue createIssue(Collection<SearchAdditionalField> fields, SearchResponseData data, IssueDto dto) {
  145. Issue.Builder issueBuilder = Issue.newBuilder();
  146. addMandatoryFieldsToIssueBuilder(issueBuilder, dto, data);
  147. addAdditionalFieldsToIssueBuilder(fields, data, dto, issueBuilder);
  148. return issueBuilder.build();
  149. }
  150. private void addMandatoryFieldsToIssueBuilder(Issue.Builder issueBuilder, IssueDto dto, SearchResponseData data) {
  151. issueBuilder.setKey(dto.getKey());
  152. issueBuilder.setType(Common.RuleType.forNumber(dto.getType()));
  153. ComponentDto component = data.getComponentByUuid(dto.getComponentUuid());
  154. issueBuilder.setComponent(component.getKey());
  155. setBranchOrPr(component, issueBuilder, data);
  156. ComponentDto branch = data.getComponentByUuid(dto.getProjectUuid());
  157. if (branch != null) {
  158. issueBuilder.setProject(branch.getKey());
  159. }
  160. issueBuilder.setRule(dto.getRuleKey().toString());
  161. if (dto.isExternal()) {
  162. issueBuilder.setExternalRuleEngine(engineNameFrom(dto.getRuleKey()));
  163. }
  164. if (dto.getType() != RuleType.SECURITY_HOTSPOT.getDbConstant()) {
  165. issueBuilder.setSeverity(Common.Severity.valueOf(dto.getSeverity()));
  166. }
  167. ofNullable(data.getUserByUuid(dto.getAssigneeUuid())).ifPresent(assignee -> issueBuilder.setAssignee(assignee.getLogin()));
  168. ofNullable(emptyToNull(dto.getResolution())).ifPresent(issueBuilder::setResolution);
  169. issueBuilder.setStatus(dto.getStatus());
  170. issueBuilder.setMessage(nullToEmpty(dto.getMessage()));
  171. issueBuilder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(dto.parseMessageFormattings()));
  172. issueBuilder.addAllTags(dto.getTags());
  173. issueBuilder.addAllCodeVariants(dto.getCodeVariants());
  174. Long effort = dto.getEffort();
  175. if (effort != null) {
  176. String effortValue = durations.encode(Duration.create(effort));
  177. issueBuilder.setDebt(effortValue);
  178. issueBuilder.setEffort(effortValue);
  179. }
  180. ofNullable(dto.getLine()).ifPresent(issueBuilder::setLine);
  181. ofNullable(emptyToNull(dto.getChecksum())).ifPresent(issueBuilder::setHash);
  182. completeIssueLocations(dto, issueBuilder, data);
  183. issueBuilder.setAuthor(nullToEmpty(dto.getAuthorLogin()));
  184. ofNullable(dto.getIssueCreationDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setCreationDate);
  185. ofNullable(dto.getIssueUpdateDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setUpdateDate);
  186. ofNullable(dto.getIssueCloseDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setCloseDate);
  187. Optional.of(dto.isQuickFixAvailable())
  188. .ifPresentOrElse(issueBuilder::setQuickFixAvailable, () -> issueBuilder.setQuickFixAvailable(false));
  189. issueBuilder.setScope(UNIT_TEST_FILE.equals(component.qualifier()) ? IssueScope.TEST.name() : IssueScope.MAIN.name());
  190. }
  191. private static void addAdditionalFieldsToIssueBuilder(Collection<SearchAdditionalField> fields, SearchResponseData data, IssueDto dto, Issue.Builder issueBuilder) {
  192. if (fields.contains(ACTIONS)) {
  193. issueBuilder.setActions(createIssueActions(data, dto));
  194. }
  195. if (fields.contains(TRANSITIONS)) {
  196. issueBuilder.setTransitions(createIssueTransition(data, dto));
  197. }
  198. if (fields.contains(COMMENTS)) {
  199. issueBuilder.setComments(createIssueComments(data, dto));
  200. }
  201. if (fields.contains(RULE_DESCRIPTION_CONTEXT_KEY)) {
  202. dto.getOptionalRuleDescriptionContextKey().ifPresent(issueBuilder::setRuleDescriptionContextKey);
  203. }
  204. }
  205. private static String engineNameFrom(RuleKey ruleKey) {
  206. checkState(ruleKey.repository().startsWith(EXTERNAL_RULE_REPO_PREFIX));
  207. return ruleKey.repository().replace(EXTERNAL_RULE_REPO_PREFIX, "");
  208. }
  209. private void completeIssueLocations(IssueDto dto, Issue.Builder issueBuilder, SearchResponseData data) {
  210. DbIssues.Locations locations = dto.parseLocations();
  211. if (locations == null) {
  212. return;
  213. }
  214. textRangeFormatter.formatTextRange(locations, issueBuilder::setTextRange);
  215. issueBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, issueBuilder.getComponent(), data.getComponentsByUuid()));
  216. }
  217. private static Transitions createIssueTransition(SearchResponseData data, IssueDto dto) {
  218. Transitions.Builder wsTransitions = Transitions.newBuilder();
  219. List<Transition> transitions = data.getTransitionsForIssueKey(dto.getKey());
  220. if (transitions != null) {
  221. for (Transition transition : transitions) {
  222. wsTransitions.addTransitions(transition.key());
  223. }
  224. }
  225. return wsTransitions.build();
  226. }
  227. private static Actions createIssueActions(SearchResponseData data, IssueDto dto) {
  228. Actions.Builder wsActions = Actions.newBuilder();
  229. List<String> actions = data.getActionsForIssueKey(dto.getKey());
  230. if (actions != null) {
  231. wsActions.addAllActions(actions);
  232. }
  233. return wsActions.build();
  234. }
  235. private static Comments createIssueComments(SearchResponseData data, IssueDto dto) {
  236. Comments.Builder wsComments = Comments.newBuilder();
  237. List<IssueChangeDto> comments = data.getCommentsForIssueKey(dto.getKey());
  238. if (comments != null) {
  239. Comment.Builder wsComment = Comment.newBuilder();
  240. for (IssueChangeDto comment : comments) {
  241. String markdown = comment.getChangeData();
  242. wsComment
  243. .clear()
  244. .setKey(comment.getKey())
  245. .setUpdatable(data.isUpdatableComment(comment.getKey()))
  246. .setCreatedAt(DateUtils.formatDateTime(new Date(comment.getIssueChangeCreationDate())));
  247. ofNullable(data.getUserByUuid(comment.getUserUuid())).ifPresent(user -> wsComment.setLogin(user.getLogin()));
  248. if (markdown != null) {
  249. wsComment
  250. .setHtmlText(Markdown.convertToHtml(markdown))
  251. .setMarkdown(markdown);
  252. }
  253. wsComments.addComments(wsComment);
  254. }
  255. }
  256. return wsComments.build();
  257. }
  258. private Common.Rules.Builder formatRules(SearchResponseData data) {
  259. Common.Rules.Builder wsRules = Common.Rules.newBuilder();
  260. List<RuleDto> rules = firstNonNull(data.getRules(), emptyList());
  261. for (RuleDto rule : rules) {
  262. wsRules.addRules(formatRule(rule));
  263. }
  264. return wsRules;
  265. }
  266. private Common.Rule.Builder formatRule(RuleDto rule) {
  267. Common.Rule.Builder builder = Common.Rule.newBuilder()
  268. .setKey(rule.getKey().toString())
  269. .setName(nullToEmpty(rule.getName()))
  270. .setStatus(Common.RuleStatus.valueOf(rule.getStatus().name()));
  271. builder.setLang(nullToEmpty(rule.getLanguage()));
  272. Language lang = languages.get(rule.getLanguage());
  273. if (lang != null) {
  274. builder.setLangName(lang.getName());
  275. }
  276. return builder;
  277. }
  278. private static List<Issues.Component> formatComponents(SearchResponseData data) {
  279. Collection<ComponentDto> components = data.getComponents();
  280. List<Issues.Component> result = new ArrayList<>();
  281. for (ComponentDto dto : components) {
  282. Component.Builder builder = Component.newBuilder()
  283. .setKey(dto.getKey())
  284. .setQualifier(dto.qualifier())
  285. .setName(nullToEmpty(dto.name()))
  286. .setLongName(nullToEmpty(dto.longName()))
  287. .setEnabled(dto.isEnabled());
  288. setBranchOrPr(dto, builder, data);
  289. ofNullable(emptyToNull(dto.path())).ifPresent(builder::setPath);
  290. result.add(builder.build());
  291. }
  292. return result;
  293. }
  294. private static void setBranchOrPr(ComponentDto componentDto, Component.Builder builder, SearchResponseData data) {
  295. String branchUuid = componentDto.getCopyComponentUuid() != null ? componentDto.getCopyComponentUuid() : componentDto.branchUuid();
  296. BranchDto branchDto = data.getBranch(branchUuid);
  297. if (branchDto.isMain()) {
  298. return;
  299. }
  300. if (branchDto.getBranchType() == BranchType.BRANCH) {
  301. builder.setBranch(branchDto.getKey());
  302. } else if (branchDto.getBranchType() == BranchType.PULL_REQUEST) {
  303. builder.setPullRequest(branchDto.getKey());
  304. }
  305. }
  306. private static void setBranchOrPr(ComponentDto componentDto, Issue.Builder builder, SearchResponseData data) {
  307. String branchUuid = componentDto.getCopyComponentUuid() != null ? componentDto.getCopyComponentUuid() : componentDto.branchUuid();
  308. BranchDto branchDto = data.getBranch(branchUuid);
  309. if (branchDto.isMain()) {
  310. return;
  311. }
  312. if (branchDto.getBranchType() == BranchType.BRANCH) {
  313. builder.setBranch(branchDto.getKey());
  314. } else if (branchDto.getBranchType() == BranchType.PULL_REQUEST) {
  315. builder.setPullRequest(branchDto.getKey());
  316. }
  317. }
  318. private Users.Builder formatUsers(SearchResponseData data) {
  319. Users.Builder wsUsers = Users.newBuilder();
  320. List<UserDto> users = data.getUsers();
  321. if (users != null) {
  322. User.Builder builder = User.newBuilder();
  323. for (UserDto user : users) {
  324. wsUsers.addUsers(userFormatter.formatUser(builder, user));
  325. }
  326. }
  327. return wsUsers;
  328. }
  329. private Issues.Languages.Builder formatLanguages() {
  330. Issues.Languages.Builder wsLangs = Issues.Languages.newBuilder();
  331. Issues.Language.Builder wsLang = Issues.Language.newBuilder();
  332. for (Language lang : languages.all()) {
  333. wsLang
  334. .clear()
  335. .setKey(lang.getKey())
  336. .setName(lang.getName());
  337. wsLangs.addLanguages(wsLang);
  338. }
  339. return wsLangs;
  340. }
  341. private static void formatFacets(SearchResponseData data, Facets facets, SearchWsResponse.Builder wsSearch) {
  342. Common.Facets.Builder wsFacets = Common.Facets.newBuilder();
  343. SearchAction.SUPPORTED_FACETS.stream()
  344. .filter(f -> !f.equals(FACET_PROJECTS))
  345. .filter(f -> !f.equals(FACET_ASSIGNED_TO_ME))
  346. .filter(f -> !f.equals(PARAM_ASSIGNEES))
  347. .filter(f -> !f.equals(PARAM_RULES))
  348. .forEach(f -> computeStandardFacet(wsFacets, facets, f));
  349. computeAssigneesFacet(wsFacets, facets, data);
  350. computeAssignedToMeFacet(wsFacets, facets, data);
  351. computeRulesFacet(wsFacets, facets, data);
  352. computeProjectsFacet(wsFacets, facets, data);
  353. wsSearch.setFacets(wsFacets.build());
  354. }
  355. private static void computeStandardFacet(Common.Facets.Builder wsFacets, Facets facets, String facetKey) {
  356. LinkedHashMap<String, Long> facet = facets.get(facetKey);
  357. if (facet == null) {
  358. return;
  359. }
  360. Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
  361. wsFacet.setProperty(facetKey);
  362. facet.forEach((value, count) -> wsFacet.addValuesBuilder()
  363. .setVal(value)
  364. .setCount(count)
  365. .build());
  366. wsFacet.build();
  367. }
  368. private static void computeAssigneesFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData data) {
  369. LinkedHashMap<String, Long> facet = facets.get(PARAM_ASSIGNEES);
  370. if (facet == null) {
  371. return;
  372. }
  373. Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
  374. wsFacet.setProperty(PARAM_ASSIGNEES);
  375. facet
  376. .forEach((userUuid, count) -> {
  377. UserDto user = data.getUserByUuid(userUuid);
  378. wsFacet.addValuesBuilder()
  379. .setVal(user == null ? "" : user.getLogin())
  380. .setCount(count)
  381. .build();
  382. });
  383. wsFacet.build();
  384. }
  385. private static void computeAssignedToMeFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData data) {
  386. LinkedHashMap<String, Long> facet = facets.get(FACET_ASSIGNED_TO_ME);
  387. if (facet == null) {
  388. return;
  389. }
  390. Map.Entry<String, Long> entry = facet.entrySet().iterator().next();
  391. UserDto user = data.getUserByUuid(entry.getKey());
  392. checkState(user != null, "User with uuid '%s' has not been found", entry.getKey());
  393. Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
  394. wsFacet.setProperty(FACET_ASSIGNED_TO_ME);
  395. wsFacet.addValuesBuilder()
  396. .setVal(user.getLogin())
  397. .setCount(entry.getValue())
  398. .build();
  399. }
  400. private static void computeRulesFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData data) {
  401. LinkedHashMap<String, Long> facet = facets.get(PARAM_RULES);
  402. if (facet == null) {
  403. return;
  404. }
  405. Map<String, RuleKey> ruleUuidsByRuleKeys = data.getRules().stream().collect(Collectors.toMap(RuleDto::getUuid, RuleDto::getKey));
  406. Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
  407. wsFacet.setProperty(PARAM_RULES);
  408. facet.forEach((ruleUuid, count) -> wsFacet.addValuesBuilder()
  409. .setVal(ruleUuidsByRuleKeys.get(ruleUuid).toString())
  410. .setCount(count)
  411. .build());
  412. wsFacet.build();
  413. }
  414. private static void computeProjectsFacet(Common.Facets.Builder wsFacets, Facets facets, SearchResponseData datas) {
  415. LinkedHashMap<String, Long> facet = facets.get(FACET_PROJECTS);
  416. if (facet == null) {
  417. return;
  418. }
  419. Common.Facet.Builder wsFacet = wsFacets.addFacetsBuilder();
  420. wsFacet.setProperty(FACET_PROJECTS);
  421. facet.forEach((uuid, count) -> {
  422. ProjectDto project = datas.getProject(uuid);
  423. requireNonNull(project, format("Project has not been found for uuid '%s'", uuid));
  424. wsFacet.addValuesBuilder()
  425. .setVal(project.getKey())
  426. .setCount(count)
  427. .build();
  428. });
  429. wsFacet.build();
  430. }
  431. }