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.

SearchAction.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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.measure.ws;
  21. import java.util.ArrayList;
  22. import java.util.Collection;
  23. import java.util.HashSet;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.function.Function;
  28. import java.util.stream.Collectors;
  29. import org.sonar.api.server.ws.Change;
  30. import org.sonar.api.server.ws.Request;
  31. import org.sonar.api.server.ws.Response;
  32. import org.sonar.api.server.ws.WebService;
  33. import org.sonar.api.web.UserRole;
  34. import org.sonar.db.DbClient;
  35. import org.sonar.db.DbSession;
  36. import org.sonar.db.component.ComponentDto;
  37. import org.sonar.db.measure.LiveMeasureDto;
  38. import org.sonar.db.metric.MetricDto;
  39. import org.sonar.db.metric.RemovedMetricConverter;
  40. import org.sonar.server.user.UserSession;
  41. import org.sonarqube.ws.Measures.Measure;
  42. import org.sonarqube.ws.Measures.SearchWsResponse;
  43. import static com.google.common.base.Preconditions.checkArgument;
  44. import static java.lang.String.format;
  45. import static java.util.Comparator.comparing;
  46. import static java.util.function.Function.identity;
  47. import static java.util.stream.Collectors.toMap;
  48. import static org.sonar.api.resources.Qualifiers.APP;
  49. import static org.sonar.api.resources.Qualifiers.PROJECT;
  50. import static org.sonar.api.resources.Qualifiers.SUBVIEW;
  51. import static org.sonar.api.resources.Qualifiers.VIEW;
  52. import static org.sonar.db.metric.RemovedMetricConverter.DEPRECATED_METRIC_REPLACEMENT;
  53. import static org.sonar.db.metric.RemovedMetricConverter.REMOVED_METRIC;
  54. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_KEYS;
  55. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PROJECT_KEYS;
  56. import static org.sonar.server.exceptions.BadRequestException.checkRequest;
  57. import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.updateMeasureBuilder;
  58. import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
  59. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  60. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_002;
  61. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  62. public class SearchAction implements MeasuresWsAction {
  63. private static final int MAX_NB_PROJECTS = 100;
  64. private static final List<String> ALLOWED_QUALIFIERS = List.of(PROJECT, APP, VIEW, SUBVIEW);
  65. private final UserSession userSession;
  66. private final DbClient dbClient;
  67. public SearchAction(UserSession userSession, DbClient dbClient) {
  68. this.userSession = userSession;
  69. this.dbClient = dbClient;
  70. }
  71. @Override
  72. public void define(WebService.NewController context) {
  73. WebService.NewAction action = context.createAction("search")
  74. .setInternal(true)
  75. .setDescription("Search for project measures ordered by project names.<br>" +
  76. "At most %d projects can be provided.<br>" +
  77. "Returns the projects with the 'Browse' permission.",
  78. MAX_NB_PROJECTS)
  79. .setSince("6.2")
  80. .setResponseExample(getClass().getResource("search-example.json"))
  81. .setHandler(this)
  82. .setChangelog(
  83. new Change("10.5", String.format("The metrics %s are now deprecated " +
  84. "without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.",
  85. MeasuresWsModule.getDeprecatedMetricsInSonarQube105())),
  86. new Change("10.5", "Added new accepted values for the 'metricKeys' param: 'new_maintainability_issues', 'new_reliability_issues', 'new_security_issues'"),
  87. new Change("10.4", String.format("The metrics %s are now deprecated " +
  88. "without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.",
  89. MeasuresWsModule.getDeprecatedMetricsInSonarQube104())),
  90. new Change("10.4", "Added new accepted values for the 'metricKeys' param: 'maintainability_issues', 'reliability_issues', 'security_issues'"),
  91. new Change("10.4", "The metrics 'open_issues', 'reopened_issues' and 'confirmed_issues' are now deprecated in the response. Consume 'violations' instead."),
  92. new Change("10.4", "The use of 'open_issues', 'reopened_issues' and 'confirmed_issues' values in 'metricKeys' param are now deprecated. Use 'violations' instead."),
  93. new Change("10.4", "The metric 'wont_fix_issues' is now deprecated in the response. Consume 'accepted_issues' instead."),
  94. new Change("10.4", "The use of 'wont_fix_issues' value in 'metricKeys' param is now deprecated. Use 'accepted_issues' instead."),
  95. new Change("10.4", "Added new accepted value for the 'metricKeys' param: 'accepted_issues'."),
  96. new Change("10.0", format("The use of the following metrics in 'metricKeys' parameter is not deprecated anymore: %s",
  97. MeasuresWsModule.getDeprecatedMetricsInSonarQube93())),
  98. new Change("9.3", format("The use of the following metrics in 'metricKeys' parameter is deprecated: %s",
  99. MeasuresWsModule.getDeprecatedMetricsInSonarQube93())));
  100. createMetricKeysParameter(action);
  101. action.createParam(PARAM_PROJECT_KEYS)
  102. .setDescription("Comma-separated list of project, view or sub-view keys")
  103. .setExampleValue(String.join(",", KEY_PROJECT_EXAMPLE_001, KEY_PROJECT_EXAMPLE_002))
  104. .setRequired(true);
  105. }
  106. @Override
  107. public void handle(Request httpRequest, Response httpResponse) throws Exception {
  108. try (DbSession dbSession = dbClient.openSession(false)) {
  109. SearchWsResponse response = new ResponseBuilder(httpRequest, dbSession).build();
  110. writeProtobuf(response, httpRequest, httpResponse);
  111. }
  112. }
  113. private class ResponseBuilder {
  114. private final DbSession dbSession;
  115. private final Request httpRequest;
  116. private SearchRequest request;
  117. private List<ComponentDto> projects;
  118. private List<MetricDto> metrics;
  119. private List<LiveMeasureDto> measures;
  120. ResponseBuilder(Request httpRequest, DbSession dbSession) {
  121. this.dbSession = dbSession;
  122. this.httpRequest = httpRequest;
  123. }
  124. SearchWsResponse build() {
  125. this.request = createRequest();
  126. this.projects = searchProjects();
  127. this.metrics = searchMetrics();
  128. this.measures = searchMeasures();
  129. return buildResponse();
  130. }
  131. private SearchRequest createRequest() {
  132. request = SearchRequest.builder()
  133. .setMetricKeys(httpRequest.mandatoryParamAsStrings(PARAM_METRIC_KEYS))
  134. .setProjectKeys(httpRequest.paramAsStrings(PARAM_PROJECT_KEYS))
  135. .build();
  136. return request;
  137. }
  138. private List<ComponentDto> searchProjects() {
  139. List<ComponentDto> componentDtos = searchByProjectKeys(dbSession, request.getProjectKeys());
  140. checkArgument(ALLOWED_QUALIFIERS.containsAll(componentDtos.stream().map(ComponentDto::qualifier).collect(Collectors.toSet())),
  141. "Only component of qualifiers %s are allowed", ALLOWED_QUALIFIERS);
  142. return getAuthorizedProjects(componentDtos);
  143. }
  144. private List<ComponentDto> searchByProjectKeys(DbSession dbSession, List<String> projectKeys) {
  145. return dbClient.componentDao().selectByKeys(dbSession, projectKeys);
  146. }
  147. private List<ComponentDto> getAuthorizedProjects(List<ComponentDto> componentDtos) {
  148. return userSession.keepAuthorizedComponents(UserRole.USER, componentDtos);
  149. }
  150. private List<MetricDto> searchMetrics() {
  151. Collection<String> metricKeysParamValue = RemovedMetricConverter.withRemovedMetricAlias(request.getMetricKeys());
  152. List<MetricDto> dbMetrics = dbClient.metricDao().selectByKeys(dbSession, metricKeysParamValue);
  153. List<String> metricKeys = dbMetrics.stream().map(MetricDto::getKey).toList();
  154. checkRequest(metricKeysParamValue.size() == dbMetrics.size(), "The following metrics are not found: %s",
  155. String.join(", ", difference(metricKeysParamValue, metricKeys)));
  156. return dbMetrics;
  157. }
  158. private List<String> difference(Collection<String> expected, Collection<String> actual) {
  159. Set<String> actualSet = new HashSet<>(actual);
  160. return expected.stream()
  161. .filter(value -> !actualSet.contains(value))
  162. .sorted(String::compareTo)
  163. .toList();
  164. }
  165. private List<LiveMeasureDto> searchMeasures() {
  166. return dbClient.liveMeasureDao().selectByComponentUuidsAndMetricUuids(dbSession,
  167. projects.stream().map(ComponentDto::uuid).toList(),
  168. metrics.stream().map(MetricDto::getUuid).toList());
  169. }
  170. private SearchWsResponse buildResponse() {
  171. List<Measure> wsMeasures = buildWsMeasures();
  172. return SearchWsResponse.newBuilder()
  173. .addAllMeasures(wsMeasures)
  174. .build();
  175. }
  176. private List<Measure> buildWsMeasures() {
  177. Map<String, ComponentDto> componentsByUuid = projects.stream().collect(toMap(ComponentDto::uuid, Function.identity()));
  178. Map<String, String> componentNamesByKey = projects.stream().collect(toMap(ComponentDto::getKey, ComponentDto::name));
  179. Map<String, MetricDto> metricsByUuid = metrics.stream().collect(toMap(MetricDto::getUuid, identity()));
  180. Function<LiveMeasureDto, MetricDto> dbMeasureToDbMetric = dbMeasure -> metricsByUuid.get(dbMeasure.getMetricUuid());
  181. Function<Measure, String> byMetricKey = Measure::getMetric;
  182. Function<Measure, String> byComponentName = wsMeasure -> componentNamesByKey.get(wsMeasure.getComponent());
  183. Measure.Builder measureBuilder = Measure.newBuilder();
  184. List<Measure> allMeasures = new ArrayList<>();
  185. for (LiveMeasureDto measure : measures) {
  186. updateMeasureBuilder(measureBuilder, dbMeasureToDbMetric.apply(measure), measure);
  187. measureBuilder.setComponent(componentsByUuid.get(measure.getComponentUuid()).getKey());
  188. Measure measureMsg = measureBuilder.build();
  189. addMeasureIncludingRenamedMetric(measureMsg, allMeasures, measureBuilder);
  190. measureBuilder.clear();
  191. }
  192. return allMeasures.stream()
  193. .sorted(comparing(byMetricKey).thenComparing(byComponentName))
  194. .toList();
  195. }
  196. private void addMeasureIncludingRenamedMetric(Measure measureMsg, List<Measure> allMeasures, Measure.Builder measureBuilder) {
  197. if (measureBuilder.getMetric().equals(DEPRECATED_METRIC_REPLACEMENT)) {
  198. if (request.getMetricKeys().contains(DEPRECATED_METRIC_REPLACEMENT)) {
  199. allMeasures.add(measureMsg);
  200. }
  201. if (request.getMetricKeys().contains(REMOVED_METRIC)) {
  202. allMeasures.add(measureBuilder.setMetric(REMOVED_METRIC).build());
  203. }
  204. } else {
  205. allMeasures.add(measureMsg);
  206. }
  207. }
  208. }
  209. private static class SearchRequest {
  210. private final List<String> metricKeys;
  211. private final List<String> projectKeys;
  212. public SearchRequest(Builder builder) {
  213. metricKeys = builder.metricKeys;
  214. projectKeys = builder.projectKeys;
  215. }
  216. public List<String> getMetricKeys() {
  217. return metricKeys;
  218. }
  219. public List<String> getProjectKeys() {
  220. return projectKeys;
  221. }
  222. public static Builder builder() {
  223. return new Builder();
  224. }
  225. }
  226. private static class Builder {
  227. private List<String> metricKeys;
  228. private List<String> projectKeys;
  229. private Builder() {
  230. // enforce method constructor
  231. }
  232. public Builder setMetricKeys(List<String> metricKeys) {
  233. this.metricKeys = metricKeys;
  234. return this;
  235. }
  236. public Builder setProjectKeys(List<String> projectKeys) {
  237. this.projectKeys = projectKeys;
  238. return this;
  239. }
  240. public SearchAction.SearchRequest build() {
  241. checkArgument(metricKeys != null && !metricKeys.isEmpty(), "Metric keys must be provided");
  242. checkArgument(projectKeys != null && !projectKeys.isEmpty(), "Project keys must be provided");
  243. int nbComponents = projectKeys.size();
  244. checkArgument(nbComponents <= MAX_NB_PROJECTS,
  245. "%s projects provided, more than maximum authorized (%s)", nbComponents, MAX_NB_PROJECTS);
  246. return new SearchAction.SearchRequest(this);
  247. }
  248. }
  249. }