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.

ComponentTreeAction.java 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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.base.Joiner;
  22. import com.google.common.collect.FluentIterable;
  23. import com.google.common.collect.HashBasedTable;
  24. import com.google.common.collect.ImmutableMap;
  25. import com.google.common.collect.ImmutableSet;
  26. import com.google.common.collect.ImmutableSortedSet;
  27. import com.google.common.collect.Lists;
  28. import com.google.common.collect.Maps;
  29. import com.google.common.collect.Sets;
  30. import com.google.common.collect.Table;
  31. import java.util.ArrayList;
  32. import java.util.Collection;
  33. import java.util.Collections;
  34. import java.util.HashSet;
  35. import java.util.LinkedHashSet;
  36. import java.util.List;
  37. import java.util.Map;
  38. import java.util.Objects;
  39. import java.util.Optional;
  40. import java.util.Set;
  41. import java.util.function.Function;
  42. import java.util.stream.Stream;
  43. import javax.annotation.CheckForNull;
  44. import javax.annotation.Nonnull;
  45. import javax.annotation.Nullable;
  46. import org.sonar.api.resources.Qualifiers;
  47. import org.sonar.api.resources.ResourceTypes;
  48. import org.sonar.api.server.ws.Change;
  49. import org.sonar.api.server.ws.Request;
  50. import org.sonar.api.server.ws.Response;
  51. import org.sonar.api.server.ws.WebService;
  52. import org.sonar.api.server.ws.WebService.Param;
  53. import org.sonar.api.utils.Paging;
  54. import org.sonar.api.web.UserRole;
  55. import org.sonar.core.i18n.I18n;
  56. import org.sonar.core.util.stream.MoreCollectors;
  57. import org.sonar.db.DbClient;
  58. import org.sonar.db.DbSession;
  59. import org.sonar.db.component.BranchType;
  60. import org.sonar.db.component.ComponentDto;
  61. import org.sonar.db.component.ComponentTreeQuery;
  62. import org.sonar.db.component.ComponentTreeQuery.Strategy;
  63. import org.sonar.db.component.SnapshotDto;
  64. import org.sonar.db.measure.LiveMeasureDto;
  65. import org.sonar.db.measure.MeasureTreeQuery;
  66. import org.sonar.db.metric.MetricDto;
  67. import org.sonar.db.metric.MetricDtoFunctions;
  68. import org.sonar.server.component.ComponentFinder;
  69. import org.sonar.server.exceptions.NotFoundException;
  70. import org.sonar.server.user.UserSession;
  71. import org.sonarqube.ws.Measures;
  72. import org.sonarqube.ws.Measures.ComponentTreeWsResponse;
  73. import static com.google.common.base.Preconditions.checkArgument;
  74. import static com.google.common.base.Preconditions.checkState;
  75. import static java.lang.String.format;
  76. import static java.util.Collections.emptyList;
  77. import static java.util.Collections.emptyMap;
  78. import static org.sonar.api.measures.Metric.ValueType.DATA;
  79. import static org.sonar.api.measures.Metric.ValueType.DISTRIB;
  80. import static org.sonar.api.utils.Paging.offset;
  81. import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02;
  82. import static org.sonar.db.component.ComponentTreeQuery.Strategy.CHILDREN;
  83. import static org.sonar.db.component.ComponentTreeQuery.Strategy.LEAVES;
  84. import static org.sonar.server.component.ComponentFinder.ParamNames.BASE_COMPONENT_ID_AND_KEY;
  85. import static org.sonar.server.component.ws.MeasuresWsParameters.ACTION_COMPONENT_TREE;
  86. import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_METRICS;
  87. import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_PERIODS;
  88. import static org.sonar.server.component.ws.MeasuresWsParameters.DEPRECATED_PARAM_BASE_COMPONENT_ID;
  89. import static org.sonar.server.component.ws.MeasuresWsParameters.DEPRECATED_PARAM_BASE_COMPONENT_KEY;
  90. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS;
  91. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
  92. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_COMPONENT;
  93. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_KEYS;
  94. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_PERIOD_SORT;
  95. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_SORT;
  96. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_SORT_FILTER;
  97. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
  98. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_QUALIFIERS;
  99. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_STRATEGY;
  100. import static org.sonar.server.exceptions.BadRequestException.checkRequest;
  101. import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
  102. import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.updateMeasureBuilder;
  103. import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter;
  104. import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
  105. import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric;
  106. import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriod.snapshotToWsPeriods;
  107. import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
  108. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  109. import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
  110. import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext;
  111. import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter;
  112. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  113. /**
  114. * <p>Navigate through components based on different strategy with specified measures.
  115. * To limit the number of rows in database, a best value algorithm exists in database.</p>
  116. * A measure is not stored in database if:
  117. * <ul>
  118. * <li>the component is a file (production or test)</li>
  119. * <li>optimization algorithm is enabled on the metric</li>
  120. * <li>the measure computed equals the metric best value</li>
  121. * <li>the period values are all equal to 0</li>
  122. * </ul>
  123. * To recreate a best value 2 different cases:
  124. * <ul>
  125. * <li>Metric starts with 'new_' (ex: new_violations): the best value measure doesn't have a value and period values are all equal to 0</li>
  126. * <li>Other metrics: the best value measure has a value of 0 and no period value</li>
  127. * </ul>
  128. */
  129. public class ComponentTreeAction implements MeasuresWsAction {
  130. private static final int MAX_SIZE = 500;
  131. private static final int QUERY_MINIMUM_LENGTH = 3;
  132. // tree exploration strategies
  133. static final String ALL_STRATEGY = "all";
  134. static final String CHILDREN_STRATEGY = "children";
  135. static final String LEAVES_STRATEGY = "leaves";
  136. static final Map<String, Strategy> STRATEGIES = ImmutableMap.of(
  137. ALL_STRATEGY, LEAVES,
  138. CHILDREN_STRATEGY, CHILDREN,
  139. LEAVES_STRATEGY, LEAVES);
  140. // sort
  141. static final String NAME_SORT = "name";
  142. static final String PATH_SORT = "path";
  143. static final String QUALIFIER_SORT = "qualifier";
  144. static final String METRIC_SORT = "metric";
  145. static final String METRIC_PERIOD_SORT = "metricPeriod";
  146. static final Set<String> SORTS = ImmutableSortedSet.of(NAME_SORT, PATH_SORT, QUALIFIER_SORT, METRIC_SORT, METRIC_PERIOD_SORT);
  147. static final String ALL_METRIC_SORT_FILTER = "all";
  148. static final String WITH_MEASURES_ONLY_METRIC_SORT_FILTER = "withMeasuresOnly";
  149. static final Set<String> METRIC_SORT_FILTERS = ImmutableSortedSet.of(ALL_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER);
  150. static final Set<String> FORBIDDEN_METRIC_TYPES = ImmutableSet.of(DISTRIB.name(), DATA.name());
  151. private static final int MAX_METRIC_KEYS = 15;
  152. private static final Joiner COMMA_JOINER = Joiner.on(", ");
  153. private static final Set<String> QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE = ImmutableSet.of(Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE);
  154. private final DbClient dbClient;
  155. private final ComponentFinder componentFinder;
  156. private final UserSession userSession;
  157. private final I18n i18n;
  158. private final ResourceTypes resourceTypes;
  159. public ComponentTreeAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, I18n i18n,
  160. ResourceTypes resourceTypes) {
  161. this.dbClient = dbClient;
  162. this.componentFinder = componentFinder;
  163. this.userSession = userSession;
  164. this.i18n = i18n;
  165. this.resourceTypes = resourceTypes;
  166. }
  167. @Override
  168. public void define(WebService.NewController context) {
  169. WebService.NewAction action = context.createAction(ACTION_COMPONENT_TREE)
  170. .setDescription(format("Navigate through components based on the chosen strategy with specified measures. The %s or the %s parameter must be provided.<br>" +
  171. "Requires the following permission: 'Browse' on the specified project.<br>" +
  172. "When limiting search with the %s parameter, directories are not returned.",
  173. DEPRECATED_PARAM_BASE_COMPONENT_ID, PARAM_COMPONENT, Param.TEXT_QUERY))
  174. .setResponseExample(getClass().getResource("component_tree-example.json"))
  175. .setSince("5.4")
  176. .setHandler(this)
  177. .addPagingParams(100, MAX_SIZE)
  178. .setChangelog(
  179. new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT)),
  180. new Change("7.2", "field 'bestValue' is added to the response"),
  181. new Change("6.3", format("Number of metric keys is limited to %s", MAX_METRIC_KEYS)),
  182. new Change("6.6", "the response field id is deprecated. Use key instead."),
  183. new Change("6.6", "the response field refId is deprecated. Use refKey instead."));
  184. action.createSortParams(SORTS, NAME_SORT, true)
  185. .setDescription("Comma-separated list of sort fields")
  186. .setExampleValue(NAME_SORT + "," + PATH_SORT);
  187. action.createParam(Param.TEXT_QUERY)
  188. .setDescription("Limit search to: <ul>" +
  189. "<li>component names that contain the supplied string</li>" +
  190. "<li>component keys that are exactly the same as the supplied string</li>" +
  191. "</ul>")
  192. .setMinimumLength(QUERY_MINIMUM_LENGTH)
  193. .setExampleValue("FILE_NAM");
  194. action.createParam(DEPRECATED_PARAM_BASE_COMPONENT_ID)
  195. .setDescription("Base component id. The search is based on this component.")
  196. .setExampleValue(UUID_EXAMPLE_02)
  197. .setDeprecatedSince("6.6");
  198. action.createParam(PARAM_COMPONENT)
  199. .setDescription("Component key. The search is based on this component.")
  200. .setExampleValue(KEY_PROJECT_EXAMPLE_001)
  201. .setDeprecatedKey(DEPRECATED_PARAM_BASE_COMPONENT_KEY, "6.6");
  202. action.createParam(PARAM_BRANCH)
  203. .setDescription("Branch key")
  204. .setExampleValue(KEY_BRANCH_EXAMPLE_001)
  205. .setInternal(true)
  206. .setSince("6.6");
  207. action.createParam(PARAM_PULL_REQUEST)
  208. .setDescription("Pull request id")
  209. .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
  210. .setInternal(true)
  211. .setSince("7.1");
  212. action.createParam(PARAM_METRIC_SORT)
  213. .setDescription(
  214. format("Metric key to sort by. The '%s' parameter must contain the '%s' or '%s' value. It must be part of the '%s' parameter", Param.SORT, METRIC_SORT, METRIC_PERIOD_SORT,
  215. PARAM_METRIC_KEYS))
  216. .setExampleValue("ncloc");
  217. action.createParam(PARAM_METRIC_PERIOD_SORT)
  218. .setDescription(format("Sort measures by leak period or not ?. The '%s' parameter must contain the '%s' value.", Param.SORT, METRIC_PERIOD_SORT))
  219. .setSince("5.5")
  220. .setPossibleValues(1);
  221. action.createParam(PARAM_METRIC_SORT_FILTER)
  222. .setDescription(format("Filter components. Sort must be on a metric. Possible values are: " +
  223. "<ul>" +
  224. "<li>%s: return all components</li>" +
  225. "<li>%s: filter out components that do not have a measure on the sorted metric</li>" +
  226. "</ul>", ALL_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER))
  227. .setDefaultValue(ALL_METRIC_SORT_FILTER)
  228. .setPossibleValues(METRIC_SORT_FILTERS);
  229. createMetricKeysParameter(action)
  230. .setDescription("Comma-separated list of metric keys. Types %s are not allowed.", COMMA_JOINER.join(FORBIDDEN_METRIC_TYPES))
  231. .setMaxValuesAllowed(MAX_METRIC_KEYS);
  232. createAdditionalFieldsParameter(action);
  233. createQualifiersParameter(action, newQualifierParameterContext(i18n, resourceTypes));
  234. action.createParam(PARAM_STRATEGY)
  235. .setDescription("Strategy to search for base component descendants:" +
  236. "<ul>" +
  237. "<li>children: return the children components of the base component. Grandchildren components are not returned</li>" +
  238. "<li>all: return all the descendants components of the base component. Grandchildren are returned.</li>" +
  239. "<li>leaves: return all the descendant components (files, in general) which don't have other children. They are the leaves of the component tree.</li>" +
  240. "</ul>")
  241. .setPossibleValues(STRATEGIES.keySet())
  242. .setDefaultValue(ALL_STRATEGY);
  243. }
  244. @Override
  245. public void handle(Request request, Response response) throws Exception {
  246. ComponentTreeWsResponse componentTreeWsResponse = doHandle(toComponentTreeWsRequest(request));
  247. writeProtobuf(componentTreeWsResponse, request, response);
  248. }
  249. private ComponentTreeWsResponse doHandle(ComponentTreeRequest request) {
  250. ComponentTreeData data = load(request);
  251. if (data.getComponents() == null) {
  252. return emptyResponse(data.getBaseComponent(), request);
  253. }
  254. return buildResponse(
  255. request,
  256. data,
  257. Paging.forPageIndex(
  258. request.getPage())
  259. .withPageSize(request.getPageSize())
  260. .andTotal(data.getComponentCount()));
  261. }
  262. private static ComponentTreeWsResponse buildResponse(ComponentTreeRequest request, ComponentTreeData data, Paging paging) {
  263. ComponentTreeWsResponse.Builder response = ComponentTreeWsResponse.newBuilder();
  264. response.getPagingBuilder()
  265. .setPageIndex(paging.pageIndex())
  266. .setPageSize(paging.pageSize())
  267. .setTotal(paging.total())
  268. .build();
  269. response.setBaseComponent(
  270. toWsComponent(
  271. data.getBaseComponent(),
  272. data.getMeasuresByComponentUuidAndMetric().row(data.getBaseComponent().uuid()),
  273. data.getReferenceComponentsByUuid()));
  274. for (ComponentDto componentDto : data.getComponents()) {
  275. response.addComponents(toWsComponent(
  276. componentDto,
  277. data.getMeasuresByComponentUuidAndMetric().row(componentDto.uuid()),
  278. data.getReferenceComponentsByUuid()));
  279. }
  280. if (areMetricsInResponse(request)) {
  281. Measures.Metrics.Builder metricsBuilder = response.getMetricsBuilder();
  282. for (MetricDto metricDto : data.getMetrics()) {
  283. metricsBuilder.addMetrics(metricDtoToWsMetric(metricDto));
  284. }
  285. }
  286. if (arePeriodsInResponse(request) && data.getPeriod() != null) {
  287. response.getPeriodsBuilder().addPeriods(data.getPeriod());
  288. }
  289. return response.build();
  290. }
  291. private static boolean areMetricsInResponse(ComponentTreeRequest request) {
  292. List<String> additionalFields = request.getAdditionalFields();
  293. return additionalFields != null && additionalFields.contains(ADDITIONAL_METRICS);
  294. }
  295. private static boolean arePeriodsInResponse(ComponentTreeRequest request) {
  296. List<String> additionalFields = request.getAdditionalFields();
  297. return additionalFields != null && additionalFields.contains(ADDITIONAL_PERIODS);
  298. }
  299. private static ComponentTreeWsResponse emptyResponse(@Nullable ComponentDto baseComponent, ComponentTreeRequest request) {
  300. ComponentTreeWsResponse.Builder response = ComponentTreeWsResponse.newBuilder();
  301. response.getPagingBuilder()
  302. .setPageIndex(request.getPage())
  303. .setPageSize(request.getPageSize())
  304. .setTotal(0);
  305. if (baseComponent != null) {
  306. response.setBaseComponent(componentDtoToWsComponent(baseComponent));
  307. }
  308. return response.build();
  309. }
  310. private static ComponentTreeRequest toComponentTreeWsRequest(Request request) {
  311. List<String> metricKeys = request.mandatoryParamAsStrings(PARAM_METRIC_KEYS);
  312. checkArgument(metricKeys.size() <= MAX_METRIC_KEYS, "Number of metrics keys is limited to %s, got %s", MAX_METRIC_KEYS, metricKeys.size());
  313. ComponentTreeRequest componentTreeRequest = new ComponentTreeRequest()
  314. .setBaseComponentId(request.param(DEPRECATED_PARAM_BASE_COMPONENT_ID))
  315. .setComponent(request.param(PARAM_COMPONENT))
  316. .setBranch(request.param(PARAM_BRANCH))
  317. .setPullRequest(request.param(PARAM_PULL_REQUEST))
  318. .setMetricKeys(metricKeys)
  319. .setStrategy(request.mandatoryParam(PARAM_STRATEGY))
  320. .setQualifiers(request.paramAsStrings(PARAM_QUALIFIERS))
  321. .setAdditionalFields(request.paramAsStrings(PARAM_ADDITIONAL_FIELDS))
  322. .setSort(request.paramAsStrings(Param.SORT))
  323. .setAsc(request.paramAsBoolean(Param.ASCENDING))
  324. .setMetricSort(request.param(PARAM_METRIC_SORT))
  325. .setMetricSortFilter(request.mandatoryParam(PARAM_METRIC_SORT_FILTER))
  326. .setMetricPeriodSort(request.paramAsInt(PARAM_METRIC_PERIOD_SORT))
  327. .setPage(request.mandatoryParamAsInt(Param.PAGE))
  328. .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
  329. .setQuery(request.param(Param.TEXT_QUERY));
  330. String metricSortValue = componentTreeRequest.getMetricSort();
  331. checkRequest(!componentTreeRequest.getMetricKeys().isEmpty(), "The '%s' parameter must contain at least one metric key", PARAM_METRIC_KEYS);
  332. List<String> sorts = Optional.ofNullable(componentTreeRequest.getSort()).orElse(emptyList());
  333. checkRequest(metricSortValue == null ^ sorts.contains(METRIC_SORT) ^ sorts.contains(METRIC_PERIOD_SORT),
  334. "To sort by a metric, the '%s' parameter must contain '%s' or '%s', and a metric key must be provided in the '%s' parameter",
  335. Param.SORT, METRIC_SORT, METRIC_PERIOD_SORT, PARAM_METRIC_SORT);
  336. checkRequest(metricSortValue == null ^ componentTreeRequest.getMetricKeys().contains(metricSortValue),
  337. "To sort by the '%s' metric, it must be in the list of metric keys in the '%s' parameter", metricSortValue, PARAM_METRIC_KEYS);
  338. checkRequest(componentTreeRequest.getMetricPeriodSort() == null ^ sorts.contains(METRIC_PERIOD_SORT),
  339. "To sort by a metric period, the '%s' parameter must contain '%s' and the '%s' must be provided.", Param.SORT, METRIC_PERIOD_SORT, PARAM_METRIC_PERIOD_SORT);
  340. checkRequest(ALL_METRIC_SORT_FILTER.equals(componentTreeRequest.getMetricSortFilter()) || metricSortValue != null,
  341. "To filter components based on the sort metric, the '%s' parameter must contain '%s' or '%s' and the '%s' parameter must be provided",
  342. Param.SORT, METRIC_SORT, METRIC_PERIOD_SORT, PARAM_METRIC_SORT);
  343. return componentTreeRequest;
  344. }
  345. private static Measures.Component.Builder toWsComponent(ComponentDto component, Map<MetricDto, ComponentTreeData.Measure> measures,
  346. Map<String, ComponentDto> referenceComponentsByUuid) {
  347. Measures.Component.Builder wsComponent = componentDtoToWsComponent(component);
  348. ComponentDto referenceComponent = referenceComponentsByUuid.get(component.getCopyResourceUuid());
  349. if (referenceComponent != null) {
  350. wsComponent.setRefId(referenceComponent.uuid());
  351. wsComponent.setRefKey(referenceComponent.getKey());
  352. }
  353. Measures.Measure.Builder measureBuilder = Measures.Measure.newBuilder();
  354. for (Map.Entry<MetricDto, ComponentTreeData.Measure> entry : measures.entrySet()) {
  355. ComponentTreeData.Measure measure = entry.getValue();
  356. updateMeasureBuilder(measureBuilder, entry.getKey(), measure.getValue(), measure.getData(), measure.getVariation());
  357. wsComponent.addMeasures(measureBuilder);
  358. measureBuilder.clear();
  359. }
  360. return wsComponent;
  361. }
  362. private ComponentTreeData load(ComponentTreeRequest wsRequest) {
  363. try (DbSession dbSession = dbClient.openSession(false)) {
  364. ComponentDto baseComponent = loadComponent(dbSession, wsRequest);
  365. checkPermissions(baseComponent);
  366. Optional<SnapshotDto> baseSnapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, baseComponent.projectUuid());
  367. if (!baseSnapshot.isPresent()) {
  368. return ComponentTreeData.builder()
  369. .setBaseComponent(baseComponent)
  370. .build();
  371. }
  372. Set<String> requestedMetricKeys = new HashSet<>(wsRequest.getMetricKeys());
  373. Set<String> metricKeysToSearch = new HashSet<>(requestedMetricKeys);
  374. boolean isSLBorPR = isSLBorPR(dbSession, baseComponent, wsRequest.getBranch(), wsRequest.getPullRequest());
  375. if (isSLBorPR) {
  376. SLBorPRMeasureFix.addReplacementMetricKeys(metricKeysToSearch);
  377. }
  378. ComponentTreeQuery componentTreeQuery = toComponentTreeQuery(wsRequest, baseComponent);
  379. List<ComponentDto> components = searchComponents(dbSession, componentTreeQuery);
  380. List<MetricDto> metrics = searchMetrics(dbSession, metricKeysToSearch);
  381. Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric = searchMeasuresByComponentUuidAndMetric(dbSession, baseComponent, componentTreeQuery,
  382. components,
  383. metrics);
  384. if (isSLBorPR) {
  385. SLBorPRMeasureFix.removeMetricsNotRequested(metrics, requestedMetricKeys);
  386. SLBorPRMeasureFix.createReplacementMeasures(metrics, measuresByComponentUuidAndMetric, requestedMetricKeys);
  387. }
  388. components = filterComponents(components, measuresByComponentUuidAndMetric, metrics, wsRequest);
  389. components = sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
  390. int componentCount = components.size();
  391. components = paginateComponents(components, wsRequest);
  392. return ComponentTreeData.builder()
  393. .setBaseComponent(baseComponent)
  394. .setComponentsFromDb(components)
  395. .setComponentCount(componentCount)
  396. .setMeasuresByComponentUuidAndMetric(measuresByComponentUuidAndMetric)
  397. .setMetrics(metrics)
  398. .setPeriod(snapshotToWsPeriods(baseSnapshot.get()).orElse(null))
  399. .setReferenceComponentsByUuid(searchReferenceComponentsById(dbSession, components))
  400. .build();
  401. }
  402. }
  403. private boolean isSLBorPR(DbSession dbSession, ComponentDto component, @Nullable String branch, @Nullable String pullRequest) {
  404. if (branch != null) {
  405. return dbClient.branchDao().selectByUuid(dbSession, component.projectUuid())
  406. .map(b -> b.getBranchType() == BranchType.SHORT).orElse(false);
  407. }
  408. return pullRequest != null;
  409. }
  410. private ComponentDto loadComponent(DbSession dbSession, ComponentTreeRequest request) {
  411. String componentId = request.getBaseComponentId();
  412. String componentKey = request.getComponent();
  413. String branch = request.getBranch();
  414. String pullRequest = request.getPullRequest();
  415. checkArgument(componentId == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'",
  416. DEPRECATED_PARAM_BASE_COMPONENT_ID, PARAM_BRANCH, PARAM_PULL_REQUEST);
  417. if (branch == null && pullRequest == null) {
  418. return componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, BASE_COMPONENT_ID_AND_KEY);
  419. }
  420. checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_COMPONENT);
  421. return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
  422. }
  423. private Map<String, ComponentDto> searchReferenceComponentsById(DbSession dbSession, List<ComponentDto> components) {
  424. List<String> referenceComponentUUids = components.stream()
  425. .map(ComponentDto::getCopyResourceUuid)
  426. .filter(Objects::nonNull)
  427. .collect(MoreCollectors.toList(components.size()));
  428. if (referenceComponentUUids.isEmpty()) {
  429. return emptyMap();
  430. }
  431. return FluentIterable.from(dbClient.componentDao().selectByUuids(dbSession, referenceComponentUUids))
  432. .uniqueIndex(ComponentDto::uuid);
  433. }
  434. private List<ComponentDto> searchComponents(DbSession dbSession, ComponentTreeQuery componentTreeQuery) {
  435. Collection<String> qualifiers = componentTreeQuery.getQualifiers();
  436. if (qualifiers != null && qualifiers.isEmpty()) {
  437. return Collections.emptyList();
  438. }
  439. return dbClient.componentDao().selectDescendants(dbSession, componentTreeQuery);
  440. }
  441. private List<MetricDto> searchMetrics(DbSession dbSession, Set<String> metricKeys) {
  442. List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, metricKeys);
  443. if (metrics.size() < metricKeys.size()) {
  444. List<String> foundMetricKeys = Lists.transform(metrics, MetricDto::getKey);
  445. Set<String> missingMetricKeys = Sets.difference(
  446. new LinkedHashSet<>(metricKeys),
  447. new LinkedHashSet<>(foundMetricKeys));
  448. throw new NotFoundException(format("The following metric keys are not found: %s", COMMA_JOINER.join(missingMetricKeys)));
  449. }
  450. String forbiddenMetrics = metrics.stream()
  451. .filter(metric -> ComponentTreeAction.FORBIDDEN_METRIC_TYPES.contains(metric.getValueType()))
  452. .map(MetricDto::getKey)
  453. .sorted()
  454. .collect(MoreCollectors.join(COMMA_JOINER));
  455. checkArgument(forbiddenMetrics.isEmpty(), "Metrics %s can't be requested in this web service. Please use api/measures/component", forbiddenMetrics);
  456. return metrics;
  457. }
  458. private Table<String, MetricDto, ComponentTreeData.Measure> searchMeasuresByComponentUuidAndMetric(DbSession dbSession, ComponentDto baseComponent,
  459. ComponentTreeQuery componentTreeQuery, List<ComponentDto> components, List<MetricDto> metrics) {
  460. Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId);
  461. MeasureTreeQuery measureQuery = MeasureTreeQuery.builder()
  462. .setStrategy(MeasureTreeQuery.Strategy.valueOf(componentTreeQuery.getStrategy().name()))
  463. .setNameOrKeyQuery(componentTreeQuery.getNameOrKeyQuery())
  464. .setQualifiers(componentTreeQuery.getQualifiers())
  465. .setMetricIds(new ArrayList<>(metricsById.keySet()))
  466. .build();
  467. Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric = HashBasedTable.create(components.size(), metrics.size());
  468. dbClient.liveMeasureDao().selectTreeByQuery(dbSession, baseComponent, measureQuery, result -> {
  469. LiveMeasureDto measureDto = result.getResultObject();
  470. measuresByComponentUuidAndMetric.put(
  471. measureDto.getComponentUuid(),
  472. metricsById.get(measureDto.getMetricId()),
  473. ComponentTreeData.Measure.createFromMeasureDto(measureDto));
  474. });
  475. addBestValuesToMeasures(measuresByComponentUuidAndMetric, components, metrics);
  476. return measuresByComponentUuidAndMetric;
  477. }
  478. /**
  479. * Conditions for best value measure:
  480. * <ul>
  481. * <li>component is a production file or test file</li>
  482. * <li>metric is optimized for best value</li>
  483. * </ul>
  484. */
  485. private static void addBestValuesToMeasures(Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric, List<ComponentDto> components,
  486. List<MetricDto> metrics) {
  487. List<MetricDtoWithBestValue> metricDtosWithBestValueMeasure = metrics.stream()
  488. .filter(MetricDtoFunctions.isOptimizedForBestValue())
  489. .map(new MetricDtoToMetricDtoWithBestValue())
  490. .collect(MoreCollectors.toList(metrics.size()));
  491. if (metricDtosWithBestValueMeasure.isEmpty()) {
  492. return;
  493. }
  494. Stream<ComponentDto> componentsEligibleForBestValue = components.stream().filter(ComponentTreeAction::isFileComponent);
  495. componentsEligibleForBestValue.forEach(component -> {
  496. for (MetricDtoWithBestValue metricWithBestValue : metricDtosWithBestValueMeasure) {
  497. if (measuresByComponentUuidAndMetric.get(component.uuid(), metricWithBestValue.getMetric()) == null) {
  498. measuresByComponentUuidAndMetric.put(component.uuid(), metricWithBestValue.getMetric(),
  499. ComponentTreeData.Measure.createFromMeasureDto(metricWithBestValue.getBestValue()));
  500. }
  501. }
  502. });
  503. }
  504. private static List<ComponentDto> filterComponents(List<ComponentDto> components,
  505. Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric, List<MetricDto> metrics, ComponentTreeRequest wsRequest) {
  506. if (!componentWithMeasuresOnly(wsRequest)) {
  507. return components;
  508. }
  509. String metricKeyToSort = wsRequest.getMetricSort();
  510. Optional<MetricDto> metricToSort = metrics.stream().filter(m -> metricKeyToSort.equals(m.getKey())).findFirst();
  511. checkState(metricToSort.isPresent(), "Metric '%s' not found", metricKeyToSort, wsRequest.getMetricKeys());
  512. return components
  513. .stream()
  514. .filter(new HasMeasure(measuresByComponentUuidAndMetric, metricToSort.get(), wsRequest.getMetricPeriodSort()))
  515. .collect(MoreCollectors.toList(components.size()));
  516. }
  517. private static boolean componentWithMeasuresOnly(ComponentTreeRequest wsRequest) {
  518. return WITH_MEASURES_ONLY_METRIC_SORT_FILTER.equals(wsRequest.getMetricSortFilter());
  519. }
  520. private static List<ComponentDto> sortComponents(List<ComponentDto> components, ComponentTreeRequest wsRequest, List<MetricDto> metrics,
  521. Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
  522. return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
  523. }
  524. private static List<ComponentDto> paginateComponents(List<ComponentDto> components, ComponentTreeRequest wsRequest) {
  525. return components.stream()
  526. .skip(offset(wsRequest.getPage(), wsRequest.getPageSize()))
  527. .limit(wsRequest.getPageSize())
  528. .collect(MoreCollectors.toList(wsRequest.getPageSize()));
  529. }
  530. @CheckForNull
  531. private List<String> childrenQualifiers(ComponentTreeRequest request, String baseQualifier) {
  532. List<String> requestQualifiers = request.getQualifiers();
  533. List<String> childrenQualifiers = null;
  534. if (LEAVES_STRATEGY.equals(request.getStrategy())) {
  535. childrenQualifiers = resourceTypes.getLeavesQualifiers(baseQualifier);
  536. }
  537. if (requestQualifiers == null) {
  538. return childrenQualifiers;
  539. }
  540. if (childrenQualifiers == null) {
  541. return requestQualifiers;
  542. }
  543. Sets.SetView<String> qualifiersIntersection = Sets.intersection(new HashSet<>(childrenQualifiers), new HashSet<Object>(requestQualifiers));
  544. return new ArrayList<>(qualifiersIntersection);
  545. }
  546. private ComponentTreeQuery toComponentTreeQuery(ComponentTreeRequest wsRequest, ComponentDto baseComponent) {
  547. List<String> childrenQualifiers = childrenQualifiers(wsRequest, baseComponent.qualifier());
  548. ComponentTreeQuery.Builder componentTreeQueryBuilder = ComponentTreeQuery.builder()
  549. .setBaseUuid(baseComponent.uuid())
  550. .setStrategy(STRATEGIES.get(wsRequest.getStrategy()));
  551. if (wsRequest.getQuery() != null) {
  552. componentTreeQueryBuilder.setNameOrKeyQuery(wsRequest.getQuery());
  553. }
  554. if (childrenQualifiers != null) {
  555. componentTreeQueryBuilder.setQualifiers(childrenQualifiers);
  556. }
  557. return componentTreeQueryBuilder.build();
  558. }
  559. private void checkPermissions(ComponentDto baseComponent) {
  560. userSession.checkComponentPermission(UserRole.USER, baseComponent);
  561. }
  562. public static boolean isFileComponent(@Nonnull ComponentDto input) {
  563. return QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE.contains(input.qualifier());
  564. }
  565. private static class MetricDtoToMetricDtoWithBestValue implements Function<MetricDto, MetricDtoWithBestValue> {
  566. @Override
  567. public MetricDtoWithBestValue apply(@Nonnull MetricDto input) {
  568. return new MetricDtoWithBestValue(input);
  569. }
  570. }
  571. }