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.

ComponentAction.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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.ImmutableSortedSet;
  22. import com.google.common.collect.Maps;
  23. import java.util.Collection;
  24. import java.util.HashMap;
  25. import java.util.HashSet;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.Optional;
  29. import java.util.Set;
  30. import java.util.stream.Collectors;
  31. import javax.annotation.CheckForNull;
  32. import javax.annotation.Nullable;
  33. import org.sonar.api.resources.Qualifiers;
  34. import org.sonar.api.server.ws.Change;
  35. import org.sonar.api.server.ws.Request;
  36. import org.sonar.api.server.ws.Response;
  37. import org.sonar.api.server.ws.WebService;
  38. import org.sonar.api.web.UserRole;
  39. import org.sonar.core.util.stream.MoreCollectors;
  40. import org.sonar.db.DbClient;
  41. import org.sonar.db.DbSession;
  42. import org.sonar.db.component.ComponentDto;
  43. import org.sonar.db.component.SnapshotDto;
  44. import org.sonar.db.measure.LiveMeasureDto;
  45. import org.sonar.db.metric.MetricDto;
  46. import org.sonar.db.metric.MetricDtoFunctions;
  47. import org.sonar.server.component.ComponentFinder;
  48. import org.sonar.server.exceptions.NotFoundException;
  49. import org.sonar.server.user.UserSession;
  50. import org.sonarqube.ws.Measures;
  51. import org.sonarqube.ws.Measures.ComponentWsResponse;
  52. import static java.lang.String.format;
  53. import static java.util.Collections.emptyMap;
  54. import static java.util.Collections.singletonList;
  55. import static java.util.Collections.singletonMap;
  56. import static org.sonar.server.component.ws.MeasuresWsParameters.ACTION_COMPONENT;
  57. import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_METRICS;
  58. import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_PERIOD;
  59. import static org.sonar.server.component.ws.MeasuresWsParameters.DEPRECATED_ADDITIONAL_PERIODS;
  60. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS;
  61. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
  62. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_COMPONENT;
  63. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_KEYS;
  64. import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
  65. import static org.sonar.server.exceptions.BadRequestException.checkRequest;
  66. import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
  67. import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter;
  68. import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
  69. import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric;
  70. import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriod.snapshotToWsPeriods;
  71. import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
  72. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  73. import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
  74. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  75. public class ComponentAction implements MeasuresWsAction {
  76. private static final Set<String> QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE = ImmutableSortedSet.of(Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE);
  77. private final DbClient dbClient;
  78. private final ComponentFinder componentFinder;
  79. private final UserSession userSession;
  80. public ComponentAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) {
  81. this.dbClient = dbClient;
  82. this.componentFinder = componentFinder;
  83. this.userSession = userSession;
  84. }
  85. @Override
  86. public void define(WebService.NewController context) {
  87. WebService.NewAction action = context.createAction(ACTION_COMPONENT)
  88. .setDescription("Return component with specified measures.<br>" +
  89. "Requires the following permission: 'Browse' on the project of specified component.")
  90. .setResponseExample(getClass().getResource("component-example.json"))
  91. .setSince("5.4")
  92. .setChangelog(
  93. new Change("8.8", "deprecated response field 'id' has been removed"),
  94. new Change("8.8", "deprecated response field 'refId' has been removed."),
  95. new Change("8.1", "the response field periods under measures field is deprecated. Use period instead."),
  96. new Change("8.1", "the response field periods is deprecated. Use period instead."),
  97. new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT)),
  98. new Change("6.6", "the response field 'id' is deprecated. Use 'key' instead."),
  99. new Change("6.6", "the response field 'refId' is deprecated. Use 'refKey' instead."))
  100. .setHandler(this);
  101. action.createParam(PARAM_COMPONENT)
  102. .setDescription("Component key")
  103. .setRequired(true)
  104. .setExampleValue(KEY_PROJECT_EXAMPLE_001);
  105. action.createParam(PARAM_BRANCH)
  106. .setDescription("Branch key. Not available in the community edition.")
  107. .setExampleValue(KEY_BRANCH_EXAMPLE_001)
  108. .setSince("6.6");
  109. action.createParam(PARAM_PULL_REQUEST)
  110. .setDescription("Pull request id. Not available in the community edition.")
  111. .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
  112. .setSince("7.1");
  113. createMetricKeysParameter(action);
  114. createAdditionalFieldsParameter(action);
  115. }
  116. @Override
  117. public void handle(Request request, Response response) throws Exception {
  118. ComponentWsResponse componentWsResponse = doHandle(toComponentWsRequest(request));
  119. writeProtobuf(componentWsResponse, request, response);
  120. }
  121. private ComponentWsResponse doHandle(ComponentRequest request) {
  122. try (DbSession dbSession = dbClient.openSession(false)) {
  123. String branch = request.getBranch();
  124. String pullRequest = request.getPullRequest();
  125. ComponentDto component = loadComponent(dbSession, request, branch, pullRequest);
  126. checkPermissions(component);
  127. SnapshotDto analysis = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, component.projectUuid()).orElse(null);
  128. boolean isPR = isPR(pullRequest);
  129. Set<String> metricKeysToRequest = new HashSet<>(request.metricKeys);
  130. if (isPR) {
  131. PrMeasureFix.addReplacementMetricKeys(metricKeysToRequest);
  132. }
  133. List<MetricDto> metrics = searchMetrics(dbSession, metricKeysToRequest);
  134. List<LiveMeasureDto> measures = searchMeasures(dbSession, component, metrics);
  135. Map<MetricDto, LiveMeasureDto> measuresByMetric = getMeasuresByMetric(measures, metrics);
  136. if (isPR) {
  137. Set<String> originalMetricKeys = new HashSet<>(request.metricKeys);
  138. PrMeasureFix.createReplacementMeasures(metrics, measuresByMetric, originalMetricKeys);
  139. PrMeasureFix.removeMetricsNotRequested(metrics, originalMetricKeys);
  140. }
  141. Optional<Measures.Period> period = snapshotToWsPeriods(analysis);
  142. Optional<ComponentDto> refComponent = getReferenceComponent(dbSession, component);
  143. return buildResponse(request, component, refComponent, measuresByMetric, metrics, period);
  144. }
  145. }
  146. public List<MetricDto> searchMetrics(DbSession dbSession, Collection<String> metricKeys) {
  147. List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, metricKeys);
  148. if (metrics.size() < metricKeys.size()) {
  149. Set<String> foundMetricKeys = metrics.stream().map(MetricDto::getKey).collect(Collectors.toSet());
  150. Set<String> missingMetricKeys = metricKeys.stream().filter(m -> !foundMetricKeys.contains(m)).collect(Collectors.toSet());
  151. throw new NotFoundException(format("The following metric keys are not found: %s", String.join(", ", missingMetricKeys)));
  152. }
  153. return metrics;
  154. }
  155. private List<LiveMeasureDto> searchMeasures(DbSession dbSession, ComponentDto component, Collection<MetricDto> metrics) {
  156. Set<String> metricUuids = metrics.stream().map(MetricDto::getUuid).collect(Collectors.toSet());
  157. List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricUuids(dbSession, singletonList(component.uuid()), metricUuids);
  158. addBestValuesToMeasures(measures, component, metrics);
  159. return measures;
  160. }
  161. private static Map<MetricDto, LiveMeasureDto> getMeasuresByMetric(List<LiveMeasureDto> measures, Collection<MetricDto> metrics) {
  162. Map<String, MetricDto> metricsByUuid = Maps.uniqueIndex(metrics, MetricDto::getUuid);
  163. Map<MetricDto, LiveMeasureDto> measuresByMetric = new HashMap<>();
  164. for (LiveMeasureDto measure : measures) {
  165. MetricDto metric = metricsByUuid.get(measure.getMetricUuid());
  166. measuresByMetric.put(metric, measure);
  167. }
  168. return measuresByMetric;
  169. }
  170. /**
  171. * Conditions for best value measure:
  172. * <ul>
  173. * <li>component is a production file or test file</li>
  174. * <li>metric is optimized for best value</li>
  175. * </ul>
  176. */
  177. private static void addBestValuesToMeasures(List<LiveMeasureDto> measures, ComponentDto component, Collection<MetricDto> metrics) {
  178. if (!QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE.contains(component.qualifier())) {
  179. return;
  180. }
  181. List<MetricDtoWithBestValue> metricWithBestValueList = metrics.stream()
  182. .filter(MetricDtoFunctions.isOptimizedForBestValue())
  183. .map(MetricDtoWithBestValue::new)
  184. .collect(MoreCollectors.toList(metrics.size()));
  185. Map<String, LiveMeasureDto> measuresByMetricUuid = Maps.uniqueIndex(measures, LiveMeasureDto::getMetricUuid);
  186. for (MetricDtoWithBestValue metricWithBestValue : metricWithBestValueList) {
  187. if (measuresByMetricUuid.get(metricWithBestValue.getMetric().getUuid()) == null) {
  188. measures.add(metricWithBestValue.getBestValue());
  189. }
  190. }
  191. }
  192. private static boolean isPR(@Nullable String pullRequest) {
  193. return pullRequest != null;
  194. }
  195. private ComponentDto loadComponent(DbSession dbSession, ComponentRequest request, @Nullable String branch, @Nullable String pullRequest) {
  196. String componentKey = request.getComponent();
  197. if (branch == null && pullRequest == null) {
  198. return componentFinder.getByKey(dbSession, componentKey);
  199. }
  200. checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_COMPONENT);
  201. return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
  202. }
  203. private Optional<ComponentDto> getReferenceComponent(DbSession dbSession, ComponentDto component) {
  204. if (component.getCopyResourceUuid() == null) {
  205. return Optional.empty();
  206. }
  207. return dbClient.componentDao().selectByUuid(dbSession, component.getCopyResourceUuid());
  208. }
  209. private static ComponentWsResponse buildResponse(ComponentRequest request, ComponentDto component, Optional<ComponentDto> refComponent,
  210. Map<MetricDto, LiveMeasureDto> measuresByMetric, Collection<MetricDto> metrics, Optional<Measures.Period> period) {
  211. ComponentWsResponse.Builder response = ComponentWsResponse.newBuilder();
  212. if (refComponent.isPresent()) {
  213. response.setComponent(componentDtoToWsComponent(component, measuresByMetric, singletonMap(refComponent.get().uuid(), refComponent.get())));
  214. } else {
  215. response.setComponent(componentDtoToWsComponent(component, measuresByMetric, emptyMap()));
  216. }
  217. List<String> additionalFields = request.getAdditionalFields();
  218. if (additionalFields != null) {
  219. if (additionalFields.contains(ADDITIONAL_METRICS)) {
  220. for (MetricDto metric : metrics) {
  221. response.getMetricsBuilder().addMetrics(metricDtoToWsMetric(metric));
  222. }
  223. }
  224. // backward compatibility
  225. if (additionalFields.contains(DEPRECATED_ADDITIONAL_PERIODS) && period.isPresent()) {
  226. response.getPeriodsBuilder().addPeriods(period.get());
  227. }
  228. if (additionalFields.contains(ADDITIONAL_PERIOD) && period.isPresent()) {
  229. response.setPeriod(period.get());
  230. }
  231. }
  232. return response.build();
  233. }
  234. private static ComponentRequest toComponentWsRequest(Request request) {
  235. ComponentRequest componentRequest = new ComponentRequest()
  236. .setComponent(request.mandatoryParam(PARAM_COMPONENT))
  237. .setBranch(request.param(PARAM_BRANCH))
  238. .setPullRequest(request.param(PARAM_PULL_REQUEST))
  239. .setAdditionalFields(request.paramAsStrings(PARAM_ADDITIONAL_FIELDS))
  240. .setMetricKeys(request.mandatoryParamAsStrings(PARAM_METRIC_KEYS));
  241. checkRequest(!componentRequest.getMetricKeys().isEmpty(), "At least one metric key must be provided");
  242. return componentRequest;
  243. }
  244. private void checkPermissions(ComponentDto baseComponent) {
  245. userSession.checkComponentPermission(UserRole.USER, baseComponent);
  246. }
  247. private static class ComponentRequest {
  248. private String component;
  249. private String branch;
  250. private String pullRequest;
  251. private List<String> metricKeys;
  252. private List<String> additionalFields;
  253. private String getComponent() {
  254. return component;
  255. }
  256. private ComponentRequest setComponent(@Nullable String component) {
  257. this.component = component;
  258. return this;
  259. }
  260. @CheckForNull
  261. private String getBranch() {
  262. return branch;
  263. }
  264. private ComponentRequest setBranch(@Nullable String branch) {
  265. this.branch = branch;
  266. return this;
  267. }
  268. @CheckForNull
  269. public String getPullRequest() {
  270. return pullRequest;
  271. }
  272. public ComponentRequest setPullRequest(@Nullable String pullRequest) {
  273. this.pullRequest = pullRequest;
  274. return this;
  275. }
  276. private List<String> getMetricKeys() {
  277. return metricKeys;
  278. }
  279. private ComponentRequest setMetricKeys(@Nullable List<String> metricKeys) {
  280. this.metricKeys = metricKeys;
  281. return this;
  282. }
  283. @CheckForNull
  284. private List<String> getAdditionalFields() {
  285. return additionalFields;
  286. }
  287. private ComponentRequest setAdditionalFields(@Nullable List<String> additionalFields) {
  288. this.additionalFields = additionalFields;
  289. return this;
  290. }
  291. }
  292. }