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.

SearchHistoryAction.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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 com.google.common.collect.Sets;
  22. import java.util.Date;
  23. import java.util.HashSet;
  24. import java.util.List;
  25. import java.util.Optional;
  26. import java.util.Set;
  27. import java.util.function.Function;
  28. import java.util.stream.Collectors;
  29. import javax.annotation.CheckForNull;
  30. import javax.annotation.Nullable;
  31. import org.sonar.api.resources.Qualifiers;
  32. import org.sonar.api.resources.Scopes;
  33. import org.sonar.api.server.ws.Change;
  34. import org.sonar.api.server.ws.Request;
  35. import org.sonar.api.server.ws.Response;
  36. import org.sonar.api.server.ws.WebService;
  37. import org.sonar.api.server.ws.WebService.Param;
  38. import org.sonar.api.web.UserRole;
  39. import org.sonar.db.DbClient;
  40. import org.sonar.db.DbSession;
  41. import org.sonar.db.component.ComponentDto;
  42. import org.sonar.db.component.SnapshotDto;
  43. import org.sonar.db.component.SnapshotQuery;
  44. import org.sonar.db.component.SnapshotQuery.SORT_FIELD;
  45. import org.sonar.db.component.SnapshotQuery.SORT_ORDER;
  46. import org.sonar.db.measure.MeasureDto;
  47. import org.sonar.db.measure.PastMeasureQuery;
  48. import org.sonar.db.metric.MetricDto;
  49. import org.sonar.db.metric.RemovedMetricConverter;
  50. import org.sonar.server.component.ComponentFinder;
  51. import org.sonar.server.user.UserSession;
  52. import org.sonar.server.ws.KeyExamples;
  53. import org.sonarqube.ws.Measures.SearchHistoryResponse;
  54. import static java.lang.String.format;
  55. import static java.util.Optional.ofNullable;
  56. import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime;
  57. import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime;
  58. import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
  59. import static org.sonar.server.component.ws.MeasuresWsParameters.ACTION_SEARCH_HISTORY;
  60. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
  61. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_COMPONENT;
  62. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_FROM;
  63. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRICS;
  64. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
  65. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_TO;
  66. import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
  67. import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
  68. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  69. public class SearchHistoryAction implements MeasuresWsAction {
  70. private static final int MAX_PAGE_SIZE = 1_000;
  71. private static final int DEFAULT_PAGE_SIZE = 100;
  72. private final DbClient dbClient;
  73. private final ComponentFinder componentFinder;
  74. private final UserSession userSession;
  75. public SearchHistoryAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) {
  76. this.dbClient = dbClient;
  77. this.componentFinder = componentFinder;
  78. this.userSession = userSession;
  79. }
  80. @Override
  81. public void define(WebService.NewController context) {
  82. WebService.NewAction action = context.createAction(ACTION_SEARCH_HISTORY)
  83. .setDescription("Search measures history of a component.<br>" +
  84. "Measures are ordered chronologically.<br>" +
  85. "Pagination applies to the number of measures for each metric.<br>" +
  86. "Requires the following permission: 'Browse' on the specified component. <br>" +
  87. "For applications, it also requires 'Browse' permission on its child projects.")
  88. .setResponseExample(getClass().getResource("search_history-example.json"))
  89. .setSince("6.3")
  90. .setChangelog(
  91. new Change("10.5", String.format("The metrics %s are now deprecated " +
  92. "without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.",
  93. MeasuresWsModule.getDeprecatedMetricsInSonarQube105())),
  94. new Change("10.5", "Added new accepted values for the 'metricKeys' param: 'new_maintainability_issues', 'new_reliability_issues', 'new_security_issues'"),
  95. new Change("10.4", String.format("The metrics %s are now deprecated " +
  96. "without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.",
  97. MeasuresWsModule.getDeprecatedMetricsInSonarQube104())),
  98. new Change("10.4", "The metrics 'open_issues', 'reopened_issues' and 'confirmed_issues' are now deprecated in the response. Consume 'violations' instead."),
  99. new Change("10.4", "The use of 'open_issues', 'reopened_issues' and 'confirmed_issues' values in 'metricKeys' param are now deprecated. Use 'violations' instead."),
  100. new Change("10.4", "The metric 'wont_fix_issues' is now deprecated in the response. Consume 'accepted_issues' instead."),
  101. new Change("10.4", "The use of 'wont_fix_issues' value in 'metricKeys' param is now deprecated. Use 'accepted_issues' instead."),
  102. new Change("10.4", "Added new accepted value for the 'metricKeys' param: 'accepted_issues'."),
  103. new Change("10.0", format("The use of the following metrics in 'metricKeys' parameter is not deprecated anymore: %s",
  104. MeasuresWsModule.getDeprecatedMetricsInSonarQube93())),
  105. new Change("9.3", format("The use of the following metrics in 'metrics' parameter is deprecated: %s",
  106. MeasuresWsModule.getDeprecatedMetricsInSonarQube93())),
  107. new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT)))
  108. .setHandler(this);
  109. action.createParam(PARAM_COMPONENT)
  110. .setDescription("Component key")
  111. .setRequired(true)
  112. .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
  113. action.createParam(PARAM_BRANCH)
  114. .setDescription("Branch key. Not available in the community edition.")
  115. .setSince("6.6")
  116. .setExampleValue(KEY_BRANCH_EXAMPLE_001);
  117. action.createParam(PARAM_PULL_REQUEST)
  118. .setDescription("Pull request id. Not available in the community edition.")
  119. .setSince("7.1")
  120. .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
  121. action.createParam(PARAM_METRICS)
  122. .setDescription("Comma-separated list of metric keys")
  123. .setRequired(true)
  124. .setExampleValue("ncloc,coverage,new_violations");
  125. action.createParam(PARAM_FROM)
  126. .setDescription("Filter measures created after the given date (inclusive). <br>" +
  127. "Either a date (server timezone) or datetime can be provided")
  128. .setExampleValue("2017-10-19 or 2017-10-19T13:00:00+0200");
  129. action.createParam(PARAM_TO)
  130. .setDescription("Filter measures created before the given date (inclusive). <br>" +
  131. "Either a date (server timezone) or datetime can be provided")
  132. .setExampleValue("2017-10-19 or 2017-10-19T13:00:00+0200");
  133. action.addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
  134. }
  135. @Override
  136. public void handle(Request request, Response response) throws Exception {
  137. SearchHistoryResponse searchHistoryResponse = Optional.of(request)
  138. .map(SearchHistoryAction::toWsRequest)
  139. .map(search())
  140. .map(result -> new SearchHistoryResponseFactory(result).apply())
  141. .orElseThrow();
  142. writeProtobuf(searchHistoryResponse, request, response);
  143. }
  144. private static SearchHistoryRequest toWsRequest(Request request) {
  145. return SearchHistoryRequest.builder()
  146. .setComponent(request.mandatoryParam(PARAM_COMPONENT))
  147. .setBranch(request.param(PARAM_BRANCH))
  148. .setPullRequest(request.param(PARAM_PULL_REQUEST))
  149. .setMetrics(request.mandatoryParamAsStrings(PARAM_METRICS))
  150. .setFrom(request.param(PARAM_FROM))
  151. .setTo(request.param(PARAM_TO))
  152. .setPage(request.mandatoryParamAsInt(Param.PAGE))
  153. .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
  154. .build();
  155. }
  156. private Function<SearchHistoryRequest, SearchHistoryResult> search() {
  157. return request -> {
  158. try (DbSession dbSession = dbClient.openSession(false)) {
  159. ComponentDto component = searchComponent(request, dbSession);
  160. SearchHistoryResult result = new SearchHistoryResult(request.page, request.pageSize)
  161. .setComponent(component)
  162. .setAnalyses(searchAnalyses(dbSession, request, component))
  163. .setMetrics(searchMetrics(dbSession, request))
  164. .setRequestedMetrics(request.getMetrics());
  165. return result.setMeasures(searchMeasures(dbSession, request, result));
  166. }
  167. };
  168. }
  169. private ComponentDto searchComponent(SearchHistoryRequest request, DbSession dbSession) {
  170. ComponentDto component = loadComponent(dbSession, request);
  171. userSession.checkComponentPermission(UserRole.USER, component);
  172. if (Scopes.PROJECT.equals(component.scope()) && Qualifiers.APP.equals(component.qualifier())) {
  173. userSession.checkChildProjectsPermission(UserRole.USER, component);
  174. }
  175. return component;
  176. }
  177. private List<MeasureDto> searchMeasures(DbSession dbSession, SearchHistoryRequest request, SearchHistoryResult result) {
  178. Date from = parseStartingDateOrDateTime(request.getFrom());
  179. Date to = parseEndingDateOrDateTime(request.getTo());
  180. PastMeasureQuery dbQuery = new PastMeasureQuery(
  181. result.getComponent().uuid(),
  182. result.getMetrics().stream().map(MetricDto::getUuid).toList(),
  183. from == null ? null : from.getTime(),
  184. to == null ? null : (to.getTime() + 1_000L));
  185. return dbClient.measureDao().selectPastMeasures(dbSession, dbQuery);
  186. }
  187. private List<SnapshotDto> searchAnalyses(DbSession dbSession, SearchHistoryRequest request, ComponentDto component) {
  188. SnapshotQuery dbQuery = new SnapshotQuery()
  189. .setRootComponentUuid(component.branchUuid())
  190. .setStatus(STATUS_PROCESSED)
  191. .setSort(SORT_FIELD.BY_DATE, SORT_ORDER.ASC);
  192. ofNullable(request.getFrom()).ifPresent(from -> dbQuery.setCreatedAfter(parseStartingDateOrDateTime(from).getTime()));
  193. ofNullable(request.getTo()).ifPresent(to -> dbQuery.setCreatedBefore(parseEndingDateOrDateTime(to).getTime() + 1_000L));
  194. return dbClient.snapshotDao().selectAnalysesByQuery(dbSession, dbQuery);
  195. }
  196. private List<MetricDto> searchMetrics(DbSession dbSession, SearchHistoryRequest request) {
  197. List<String> upToDateRequestedMetrics = RemovedMetricConverter.withRemovedMetricAlias(request.getMetrics());
  198. List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, upToDateRequestedMetrics);
  199. if (upToDateRequestedMetrics.size() > metrics.size()) {
  200. Set<String> requestedMetrics = new HashSet<>(upToDateRequestedMetrics);
  201. Set<String> foundMetrics = metrics.stream().map(MetricDto::getKey).collect(Collectors.toSet());
  202. Set<String> unfoundMetrics = Sets.difference(requestedMetrics, foundMetrics).immutableCopy();
  203. throw new IllegalArgumentException(format("Metrics %s are not found", String.join(", ", unfoundMetrics)));
  204. }
  205. return metrics;
  206. }
  207. private ComponentDto loadComponent(DbSession dbSession, SearchHistoryRequest request) {
  208. String componentKey = request.getComponent();
  209. String branch = request.getBranch();
  210. String pullRequest = request.getPullRequest();
  211. return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
  212. }
  213. static class SearchHistoryRequest {
  214. private final String component;
  215. private final String branch;
  216. private final String pullRequest;
  217. private final List<String> metrics;
  218. private final String from;
  219. private final String to;
  220. private final int page;
  221. private final int pageSize;
  222. public SearchHistoryRequest(Builder builder) {
  223. this.component = builder.component;
  224. this.branch = builder.branch;
  225. this.pullRequest = builder.pullRequest;
  226. this.metrics = builder.metrics;
  227. this.from = builder.from;
  228. this.to = builder.to;
  229. this.page = builder.page;
  230. this.pageSize = builder.pageSize;
  231. }
  232. public String getComponent() {
  233. return component;
  234. }
  235. @CheckForNull
  236. public String getBranch() {
  237. return branch;
  238. }
  239. @CheckForNull
  240. public String getPullRequest() {
  241. return pullRequest;
  242. }
  243. public List<String> getMetrics() {
  244. return metrics;
  245. }
  246. @CheckForNull
  247. public String getFrom() {
  248. return from;
  249. }
  250. @CheckForNull
  251. public String getTo() {
  252. return to;
  253. }
  254. public int getPage() {
  255. return page;
  256. }
  257. public int getPageSize() {
  258. return pageSize;
  259. }
  260. public static Builder builder() {
  261. return new Builder();
  262. }
  263. }
  264. static class Builder {
  265. private String component;
  266. private String branch;
  267. private String pullRequest;
  268. private List<String> metrics;
  269. private String from;
  270. private String to;
  271. private int page = 1;
  272. private int pageSize = DEFAULT_PAGE_SIZE;
  273. private Builder() {
  274. // enforce build factory method
  275. }
  276. public Builder setComponent(String component) {
  277. this.component = component;
  278. return this;
  279. }
  280. public Builder setBranch(@Nullable String branch) {
  281. this.branch = branch;
  282. return this;
  283. }
  284. public Builder setPullRequest(@Nullable String pullRequest) {
  285. this.pullRequest = pullRequest;
  286. return this;
  287. }
  288. public Builder setMetrics(List<String> metrics) {
  289. this.metrics = metrics;
  290. return this;
  291. }
  292. public Builder setFrom(@Nullable String from) {
  293. this.from = from;
  294. return this;
  295. }
  296. public Builder setTo(@Nullable String to) {
  297. this.to = to;
  298. return this;
  299. }
  300. public Builder setPage(int page) {
  301. this.page = page;
  302. return this;
  303. }
  304. public Builder setPageSize(int pageSize) {
  305. this.pageSize = pageSize;
  306. return this;
  307. }
  308. public SearchHistoryRequest build() {
  309. checkArgument(component != null && !component.isEmpty(), "Component key is required");
  310. checkArgument(metrics != null && !metrics.isEmpty(), "Metric keys are required");
  311. checkArgument(pageSize <= MAX_PAGE_SIZE, "Page size (%d) must be lower than or equal to %d", pageSize, MAX_PAGE_SIZE);
  312. return new SearchHistoryRequest(this);
  313. }
  314. private static void checkArgument(boolean condition, String message, Object... args) {
  315. if (!condition) {
  316. throw new IllegalArgumentException(format(message, args));
  317. }
  318. }
  319. }
  320. }