From e678c88a913dd1eacb0c334ad87769a8d120c3b3 Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Tue, 12 Jan 2016 12:45:48 +0100 Subject: [PATCH] SONAR-7134 WS api/measures/component --- .../test/java/it/measure/MeasuresWsTest.java | 16 + .../server/component/ComponentFinder.java | 1 + .../server/measure/ws/ComponentAction.java | 251 +++++++++++++++ .../measure/ws/ComponentTreeAction.java | 19 +- .../measure/ws/ComponentTreeDataLoader.java | 39 +-- .../server/measure/ws/ComponentTreeSort.java | 1 - .../server/measure/ws/MeasuresWsModule.java | 4 +- .../ws/MeasuresWsParametersBuilder.java | 49 +++ .../measure/ws/MetricDtoWithBestValue.java | 82 +++++ .../measure/ws/SnapshotDtoToWsPeriods.java | 66 ++++ .../server/measure/ws/component-example.json | 119 +++++++ .../measure/ws/ComponentActionTest.java | 293 ++++++++++++++++++ .../measure/ws/ComponentTreeActionTest.java | 2 +- .../measure/ws/MeasuresWsModuleTest.java | 2 +- .../sonar/db/measure/MeasureDtoFunctions.java | 42 +++ .../sonar/db/metric/MetricDtoFunctions.java | 14 + .../ws/client/measure/ComponentWsRequest.java | 70 +++++ .../ws/client/measure/MeasuresService.java | 20 +- .../client/measure/MeasuresWsParameters.java | 19 +- sonar-ws/src/main/protobuf/ws-measures.proto | 7 + 20 files changed, 1055 insertions(+), 61 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsParametersBuilder.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/ws/SnapshotDtoToWsPeriods.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component-example.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java create mode 100644 sonar-db/src/main/java/org/sonar/db/measure/MeasureDtoFunctions.java create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentWsRequest.java diff --git a/it/it-tests/src/test/java/it/measure/MeasuresWsTest.java b/it/it-tests/src/test/java/it/measure/MeasuresWsTest.java index 9a694b012a4..60af1d3416e 100644 --- a/it/it-tests/src/test/java/it/measure/MeasuresWsTest.java +++ b/it/it-tests/src/test/java/it/measure/MeasuresWsTest.java @@ -30,8 +30,10 @@ import org.junit.ClassRule; import org.junit.Test; import org.sonarqube.ws.WsMeasures; import org.sonarqube.ws.WsMeasures.ComponentTreeWsResponse; +import org.sonarqube.ws.WsMeasures.ComponentWsResponse; import org.sonarqube.ws.client.WsClient; import org.sonarqube.ws.client.measure.ComponentTreeWsRequest; +import org.sonarqube.ws.client.measure.ComponentWsRequest; import util.ItUtils; import static com.google.common.collect.Lists.newArrayList; @@ -80,4 +82,18 @@ public class MeasuresWsTest { assertThat(components).hasSize(2).extracting("key").containsOnly("sample:src/main/xoo/sample", FILE_KEY); assertThat(components.get(0).getMeasures().getMeasuresList().get(0).getValue()).isEqualTo("13"); } + + @Test + public void component() { + ComponentWsResponse response = wsClient.measures().component(new ComponentWsRequest() + .setComponentKey("sample") + .setMetricKeys(singletonList("ncloc")) + .setAdditionalFields(newArrayList("metrics", "periods"))); + + WsMeasures.Component component = response.getComponent(); + assertThat(component.getKey()).isEqualTo("sample"); + assertThat(component.getMeasures().getMeasuresList()).isNotEmpty(); + assertThat(response.getMetrics().getMetricsList()).extracting("key").containsOnly("ncloc"); + + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java index 63be251b2fa..d2b84d00be2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java @@ -105,6 +105,7 @@ public class ComponentFinder { PROJECT_ID_AND_KEY("projectId", "projectKey"), UUID_AND_KEY("uuid", "key"), ID_AND_KEY("id", "key"), + COMPONENT_ID_AND_KEY("componentId", "componentKey"), BASE_COMPONENT_ID_AND_KEY("baseComponentId", "baseComponentKey"); private final String uuidParamName; diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java new file mode 100644 index 00000000000..b6a62d5e12e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java @@ -0,0 +1,251 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.ws; + +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.measure.MeasureDto; +import org.sonar.db.measure.MeasureDtoFunctions; +import org.sonar.db.metric.MetricDto; +import org.sonar.db.metric.MetricDtoFunctions; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.measure.ws.MetricDtoWithBestValue.MetricDtoToMetricDtoWithBestValueFunction; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.WsMeasures; +import org.sonarqube.ws.WsMeasures.ComponentWsResponse; +import org.sonarqube.ws.client.measure.ComponentWsRequest; + +import static com.google.common.base.Objects.firstNonNull; +import static com.google.common.collect.FluentIterable.from; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; +import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_KEY; +import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent; +import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter; +import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter; +import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric; +import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriods.snapshotsToPeriods; +import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException; +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonar.server.ws.WsUtils.checkRequest; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ACTION_COMPONENT; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ADDITIONAL_METRICS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ADDITIONAL_PERIODS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_COMPONENT_ID; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_COMPONENT_KEY; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS; + +public class ComponentAction implements MeasuresWsAction { + private static final Set QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE = ImmutableSortedSet.of(Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE); + + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final UserSession userSession; + + public ComponentAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_COMPONENT) + .setDescription(format("Return component with specified measures. The %s or the %s parameter must be provided.
" + + "Requires one of the following permissions:" + + "", + PARAM_COMPONENT_ID, PARAM_COMPONENT_KEY)) + .setResponseExample(getClass().getResource("component-example.json")) + .setSince("5.4") + .setHandler(this); + + action.createParam(PARAM_COMPONENT_ID) + .setDescription("Component id") + .setExampleValue(UUID_EXAMPLE_01); + + action.createParam(PARAM_COMPONENT_KEY) + .setDescription("Component key") + .setExampleValue(KEY_PROJECT_EXAMPLE_001); + + createMetricKeysParameter(action); + createAdditionalFieldsParameter(action); + } + + @Override + public void handle(Request request, Response response) throws Exception { + ComponentWsResponse componentWsResponse = doHandle(toComponentWsRequest(request)); + writeProtobuf(componentWsResponse, request, response); + } + + private ComponentWsResponse doHandle(ComponentWsRequest request) { + DbSession dbSession = dbClient.openSession(false); + try { + ComponentDto component = componentFinder.getByUuidOrKey(dbSession, request.getComponentId(), request.getComponentKey(), COMPONENT_ID_AND_KEY); + Optional refComponent = getReferenceComponent(dbSession, component); + checkPermissions(component); + SnapshotDto lastSnapshot = dbClient.snapshotDao().selectLastSnapshotByComponentId(dbSession, component.getId()); + List metrics = searchMetrics(dbSession, request); + List periods = snapshotsToPeriods(lastSnapshot); + List measures = searchMeasures(dbSession, component, lastSnapshot, metrics, periods); + + return buildResponse(request, component, refComponent, measures, metrics, periods); + } finally { + dbClient.closeSession(dbSession); + } + } + + private Optional getReferenceComponent(DbSession dbSession, ComponentDto component) { + if (component.getCopyResourceId() == null) { + return Optional.absent(); + } + + return dbClient.componentDao().selectById(dbSession, component.getCopyResourceId()); + } + + private static ComponentWsResponse buildResponse(ComponentWsRequest request, ComponentDto component, Optional refComponent, List measures, + List metrics, List periods) { + ComponentWsResponse.Builder response = ComponentWsResponse.newBuilder(); + Map metricsById = Maps.uniqueIndex(metrics, MetricDtoFunctions.toId()); + Map measuresByMetric = new HashMap<>(); + for (MeasureDto measure : measures) { + MetricDto metric = metricsById.get(measure.getMetricId()); + measuresByMetric.put(metric, measure); + } + Map referenceComponentUuidById = new HashMap<>(); + if (refComponent.isPresent()) { + referenceComponentUuidById.put(refComponent.get().getId(), refComponent.get().uuid()); + } + + response.setComponent(componentDtoToWsComponent(component, measuresByMetric, referenceComponentUuidById)); + + List additionalFields = request.getAdditionalFields(); + if (additionalFields != null) { + if (additionalFields.contains(ADDITIONAL_METRICS)) { + for (MetricDto metric : metrics) { + response.getMetricsBuilder().addMetrics(metricDtoToWsMetric(metric)); + } + } + if (additionalFields.contains(ADDITIONAL_PERIODS)) { + response.getPeriodsBuilder().addAllPeriods(periods); + } + } + + return response.build(); + } + + private List searchMetrics(DbSession dbSession, ComponentWsRequest request) { + List metrics = dbClient.metricDao().selectByKeys(dbSession, request.getMetricKeys()); + if (metrics.size() < request.getMetricKeys().size()) { + List foundMetricKeys = Lists.transform(metrics, MetricDtoFunctions.toKey()); + Set missingMetricKeys = Sets.difference( + new LinkedHashSet<>(request.getMetricKeys()), + new LinkedHashSet<>(foundMetricKeys)); + + throw new NotFoundException(format("The following metric keys are not found: %s", Joiner.on(", ").join(missingMetricKeys))); + } + + return metrics; + } + + private List searchMeasures(DbSession dbSession, ComponentDto component, @Nullable SnapshotDto snapshot, List metrics, List periods) { + if (snapshot == null) { + return emptyList(); + } + + List metricIds = Lists.transform(metrics, MetricDtoFunctions.toId()); + List measures = dbClient.measureDao().selectBySnapshotIdsAndMetricIds(dbSession, singletonList(snapshot.getId()), metricIds); + addBestValuesToMeasures(measures, component, metrics, periods); + + return measures; + } + + /** + * Conditions for best value measure: + *
    + *
  • component is a production file or test file
  • + *
  • metric is optimized for best value
  • + *
+ */ + private static void addBestValuesToMeasures(List measures, ComponentDto component, List metrics, List periods) { + if (!QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE.contains(component.qualifier())) { + return; + } + + List metricWithBestValueList = from(metrics) + .filter(MetricDtoFunctions.isOptimizedForBestValue()) + .transform(new MetricDtoToMetricDtoWithBestValueFunction(periods)) + .toList(); + Map measuresByMetricId = Maps.uniqueIndex(measures, MeasureDtoFunctions.toMetricId()); + + for (MetricDtoWithBestValue metricWithBestValue : metricWithBestValueList) { + if (measuresByMetricId.get(metricWithBestValue.getMetric().getId()) == null) { + measures.add(metricWithBestValue.getBestValue()); + } + } + } + + private static ComponentWsRequest toComponentWsRequest(Request request) { + ComponentWsRequest componentWsRequest = new ComponentWsRequest() + .setComponentId(request.param(PARAM_COMPONENT_ID)) + .setComponentKey(request.param(PARAM_COMPONENT_KEY)) + .setAdditionalFields(request.paramAsStrings(PARAM_ADDITIONAL_FIELDS)) + .setMetricKeys(request.mandatoryParamAsStrings(PARAM_METRIC_KEYS)); + checkRequest(!componentWsRequest.getMetricKeys().isEmpty(), "At least one metric key must be provided"); + return componentWsRequest; + } + + private void checkPermissions(ComponentDto baseComponent) { + String projectUuid = firstNonNull(baseComponent.projectUuid(), baseComponent.uuid()); + if (!userSession.hasPermission(GlobalPermissions.SYSTEM_ADMIN) && + !userSession.hasComponentUuidPermission(UserRole.ADMIN, projectUuid) && + !userSession.hasComponentUuidPermission(UserRole.USER, projectUuid)) { + throw insufficientPrivilegesException(); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java index ca904c05765..c1e08414784 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java @@ -38,6 +38,8 @@ import org.sonarqube.ws.client.measure.ComponentTreeWsRequest; import static java.lang.String.format; import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02; import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent; +import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter; +import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter; import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext; @@ -45,6 +47,8 @@ import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter; import static org.sonar.server.ws.WsUtils.checkRequest; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ACTION_COMPONENT_TREE; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ADDITIONAL_METRICS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ADDITIONAL_PERIODS; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_BASE_COMPONENT_ID; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_BASE_COMPONENT_KEY; @@ -80,9 +84,6 @@ public class ComponentTreeAction implements MeasuresWsAction { static final String QUALIFIER_SORT = "qualifier"; static final String METRIC_SORT = "metric"; static final Set SORTS = ImmutableSortedSet.of(NAME_SORT, PATH_SORT, QUALIFIER_SORT, METRIC_SORT); - static final String ADDITIONAL_METRICS = "metrics"; - static final String ADDITIONAL_PERIODS = "periods"; - static final Set ADDITIONAL_FIELDS = ImmutableSortedSet.of(ADDITIONAL_METRICS, ADDITIONAL_PERIODS); private final ComponentTreeDataLoader dataLoader; private final UserSession userSession; @@ -133,21 +134,13 @@ public class ComponentTreeAction implements MeasuresWsAction { .setDescription("Base component key.The search is based on this component.") .setExampleValue(KEY_PROJECT_EXAMPLE_001); - action.createParam(PARAM_METRIC_KEYS) - .setDescription("Metric keys") - .setRequired(true) - .setExampleValue("ncloc,complexity,violations"); - action.createParam(PARAM_METRIC_SORT) .setDescription( format("Metric key to sort by. The '%s' parameter must contain the '%s' value. It must be part of the '%s' parameter", Param.SORT, METRIC_SORT, PARAM_METRIC_KEYS)) .setExampleValue("ncloc"); - action.createParam(PARAM_ADDITIONAL_FIELDS) - .setDescription("Comma-separated list of additional fields that can be returned in the response.") - .setPossibleValues(ADDITIONAL_FIELDS) - .setExampleValue("periods,metrics"); - + createMetricKeysParameter(action); + createAdditionalFieldsParameter(action); createQualifiersParameter(action, newQualifierParameterContext(userSession, i18n, resourceTypes)); action.createParam(PARAM_STRATEGY) diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java index e73cb8cc1a5..79d26ae7666 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java @@ -212,7 +212,7 @@ public class ComponentTreeDataLoader { private static void addBestValuesToMeasures(Table measuresByComponentUuidAndMetric, List components, List metrics, List periods) { List metricDtosWithBestValueMeasure = from(metrics) - .filter(IsMetricOptimizedForBestValue.INSTANCE) + .filter(MetricDtoFunctions.isOptimizedForBestValue()) .transform(new MetricDtoToMetricDtoWithBestValue(periods)) .toList(); if (metricDtosWithBestValueMeasure.isEmpty()) { @@ -222,8 +222,8 @@ public class ComponentTreeDataLoader { List componentsEligibleForBestValue = from(components).filter(IsFileComponent.INSTANCE).toList(); for (ComponentDtoWithSnapshotId component : componentsEligibleForBestValue) { for (MetricDtoWithBestValue metricWithBestValue : metricDtosWithBestValueMeasure) { - if (measuresByComponentUuidAndMetric.get(component.uuid(), metricWithBestValue.metric) == null) { - measuresByComponentUuidAndMetric.put(component.uuid(), metricWithBestValue.metric, metricWithBestValue.bestValue); + if (measuresByComponentUuidAndMetric.get(component.uuid(), metricWithBestValue.getMetric()) == null) { + measuresByComponentUuidAndMetric.put(component.uuid(), metricWithBestValue.getMetric(), metricWithBestValue.getBestValue()); } } } @@ -349,15 +349,6 @@ public class ComponentTreeDataLoader { } } - private enum IsMetricOptimizedForBestValue implements Predicate { - INSTANCE; - - @Override - public boolean apply(@Nonnull MetricDto input) { - return input.isOptimizedBestValue() && input.getBestValue() != null; - } - } - private enum IsFileComponent implements Predicate { INSTANCE; @@ -380,30 +371,6 @@ public class ComponentTreeDataLoader { } } - private static class MetricDtoWithBestValue { - private static final String LOWER_CASE_NEW_METRIC_PREFIX = "new_"; - private final MetricDto metric; - - private final MeasureDto bestValue; - - private MetricDtoWithBestValue(MetricDto metric, List periodIndexes) { - this.metric = metric; - MeasureDto measure = new MeasureDto() - .setMetricId(metric.getId()) - .setMetricKey(metric.getKey()); - boolean isNewTypeMetric = metric.getKey().toLowerCase().startsWith(LOWER_CASE_NEW_METRIC_PREFIX); - if (isNewTypeMetric) { - for (Integer periodIndex : periodIndexes) { - measure.setVariation(periodIndex, 0.0d); - } - } else { - measure.setValue(metric.getBestValue()); - } - - this.bestValue = measure; - } - } - private enum WsPeriodToIndex implements Function { INSTANCE; diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java index 710d2196feb..50b7c1a0292 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java @@ -98,7 +98,6 @@ class ComponentTreeSort { /** * Order by measure value, taking the metric direction into account - * Metric direction is taken into account in {@link ComponentDtoWithSnapshotIdToNumericalMeasureValue} */ private static Ordering metricOrdering(ComponentTreeWsRequest wsRequest, List metrics, Table measuresByComponentUuidAndMetric) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java index 7743470557c..23d6b832814 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java @@ -28,7 +28,7 @@ public class MeasuresWsModule extends Module { add( ComponentTreeDataLoader.class, MeasuresWs.class, - ComponentTreeAction.class - ); + ComponentTreeAction.class, + ComponentAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsParametersBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsParametersBuilder.java new file mode 100644 index 00000000000..4107953931a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsParametersBuilder.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.ws; + +import org.sonar.api.server.ws.WebService.NewAction; +import org.sonar.api.server.ws.WebService.NewParam; + +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ADDITIONAL_FIELDS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS; + +class MeasuresWsParametersBuilder { + + private MeasuresWsParametersBuilder() { + // prevent instantiation + } + + static NewParam createAdditionalFieldsParameter(NewAction action) { + return action.createParam(PARAM_ADDITIONAL_FIELDS) + .setDescription("Comma-separated list of additional fields that can be returned in the response.") + .setPossibleValues(ADDITIONAL_FIELDS) + .setExampleValue("periods,metrics"); + } + + static NewParam createMetricKeysParameter(NewAction action) { + return action.createParam(PARAM_METRIC_KEYS) + .setDescription("Metric keys") + .setRequired(true) + .setExampleValue("ncloc,complexity,violations"); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java new file mode 100644 index 00000000000..7c235fbdb63 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.ws; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import java.util.List; +import javax.annotation.Nonnull; +import org.sonar.db.measure.MeasureDto; +import org.sonar.db.metric.MetricDto; +import org.sonarqube.ws.WsMeasures; + +class MetricDtoWithBestValue { + private static final String LOWER_CASE_NEW_METRIC_PREFIX = "new_"; + + private final MetricDto metric; + private final MeasureDto bestValue; + + MetricDtoWithBestValue(MetricDto metric, List periodIndexes) { + this.metric = metric; + MeasureDto measure = new MeasureDto() + .setMetricId(metric.getId()) + .setMetricKey(metric.getKey()); + boolean isNewTypeMetric = metric.getKey().toLowerCase().startsWith(LOWER_CASE_NEW_METRIC_PREFIX); + if (isNewTypeMetric) { + for (Integer periodIndex : periodIndexes) { + measure.setVariation(periodIndex, 0.0d); + } + } else { + measure.setValue(metric.getBestValue()); + } + + this.bestValue = measure; + } + + MetricDto getMetric() { + return metric; + } + + MeasureDto getBestValue() { + return bestValue; + } + + static class MetricDtoToMetricDtoWithBestValueFunction implements Function { + private final List periodIndexes; + + MetricDtoToMetricDtoWithBestValueFunction(List periods) { + this.periodIndexes = Lists.transform(periods, WsPeriodToIndex.INSTANCE); + } + + @Override + public MetricDtoWithBestValue apply(@Nonnull MetricDto input) { + return new MetricDtoWithBestValue(input, periodIndexes); + } + } + + private enum WsPeriodToIndex implements Function { + INSTANCE; + + @Override + public Integer apply(@Nonnull WsMeasures.Period input) { + return input.getIndex(); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SnapshotDtoToWsPeriods.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SnapshotDtoToWsPeriods.java new file mode 100644 index 00000000000..8418f3a4f96 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SnapshotDtoToWsPeriods.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.ws; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.db.component.SnapshotDto; +import org.sonarqube.ws.WsMeasures; + +import static java.util.Collections.emptyList; +import static org.sonar.api.utils.DateUtils.formatDateTime; + +class SnapshotDtoToWsPeriods { + private SnapshotDtoToWsPeriods() { + // prevent instantiation + } + + static List snapshotsToPeriods(@Nullable SnapshotDto snapshot) { + if (snapshot == null) { + return emptyList(); + } + + List periods = new ArrayList<>(); + for (int periodIndex = 1; periodIndex <= 5; periodIndex++) { + if (snapshot.getPeriodDate(periodIndex) != null) { + periods.add(snapshotDtoToWsPeriod(snapshot, periodIndex)); + } + } + + return periods; + } + + private static WsMeasures.Period snapshotDtoToWsPeriod(SnapshotDto snapshot, int periodIndex) { + WsMeasures.Period.Builder period = WsMeasures.Period.newBuilder(); + period.setIndex(periodIndex); + if (snapshot.getPeriodMode(periodIndex) != null) { + period.setMode(snapshot.getPeriodMode(periodIndex)); + } + if (snapshot.getPeriodModeParameter(periodIndex) != null) { + period.setParameter(snapshot.getPeriodModeParameter(periodIndex)); + } + if (snapshot.getPeriodDate(periodIndex) != null) { + period.setDate(formatDateTime(snapshot.getPeriodDate(periodIndex))); + } + + return period.build(); + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component-example.json b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component-example.json new file mode 100644 index 00000000000..f5f98554091 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component-example.json @@ -0,0 +1,119 @@ +{ + "component": { + "id": "AVIwDXE-bJbJqrw6wFv5", + "key": "MY_PROJECT:ElementImpl.java", + "name": "ElementImpl.java", + "qualifier": "FIL", + "path": "src/main/java/com/sonarsource/markdown/impl/ElementImpl.java", + "measures": [ + { + "metric": "complexity", + "value": "12", + "periods": [ + { + "index": 1, + "value": "2" + }, + { + "index": 2, + "value": "0" + }, + { + "index": 3, + "value": "0" + } + ] + }, + { + "metric": "new_violations", + "periods": [ + { + "index": 1, + "value": "25" + }, + { + "index": 2, + "value": "0" + }, + { + "index": 3, + "value": "25" + } + ] + }, + { + "metric": "ncloc", + "value": "114", + "periods": [ + { + "index": 1, + "value": "3" + }, + { + "index": 2, + "value": "-5" + }, + { + "index": 3, + "value": "5" + } + ] + } + ] + }, + "metrics": [ + { + "key": "complexity", + "name": "Complexity", + "description": "Cyclomatic complexity", + "domain": "Complexity", + "type": "INT", + "higherValuesAreBetter": false, + "qualitative": false, + "hidden": false, + "custom": false + }, + { + "key": "ncloc", + "name": "Lines of code", + "description": "Non Commenting Lines of Code", + "domain": "Size", + "type": "INT", + "higherValuesAreBetter": false, + "qualitative": false, + "hidden": false, + "custom": false + }, + { + "key": "new_violations", + "name": "New issues", + "description": "New Issues", + "domain": "Issues", + "type": "INT", + "higherValuesAreBetter": false, + "qualitative": true, + "hidden": false, + "custom": false + } + ], + "periods": [ + { + "index": 1, + "mode": "previous_version", + "date": "2016-01-11T10:49:50+0100", + "parameter": "1.0-SNAPSHOT" + }, + { + "index": 2, + "mode": "previous_analysis", + "date": "2016-01-11T10:50:06+0100", + "parameter": "2016-01-11" + }, + { + "index": 3, + "mode": "days", + "date": "2016-01-11T10:38:45+0100", + "parameter": "30" + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java new file mode 100644 index 00000000000..955ddcf0bcd --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java @@ -0,0 +1,293 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.ws; + +import com.google.common.base.Throwables; +import java.io.IOException; +import java.io.InputStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; +import org.sonar.test.DbTests; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.WsMeasures.ComponentWsResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newProjectCopy; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.component.ComponentTesting.newView; +import static org.sonar.db.component.SnapshotTesting.createForComponent; +import static org.sonar.db.measure.MeasureTesting.newMeasureDto; +import static org.sonar.db.metric.MetricTesting.newMetricDto; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_COMPONENT_ID; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS; + +@Category(DbTests.class) +public class ComponentActionTest { + private static final String PROJECT_UUID = "project-uuid"; + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + ComponentDbTester componentDb = new ComponentDbTester(db); + DbClient dbClient = db.getDbClient(); + DbSession dbSession = db.getSession(); + + WsActionTester ws = new WsActionTester(new ComponentAction(dbClient, new ComponentFinder(dbClient), userSession)); + + @Before + public void setUp() { + userSession.setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + } + + @Test + public void json_example() { + insertJsonExampleData(); + + String response = ws.newRequest() + .setParam(PARAM_COMPONENT_ID, "AVIwDXE-bJbJqrw6wFv5") + .setParam(PARAM_METRIC_KEYS, "ncloc, complexity, new_violations") + .setParam(PARAM_ADDITIONAL_FIELDS, "metrics,periods") + .execute() + .getInput(); + + assertJson(response).isSimilarTo(getClass().getResource("component-example.json")); + } + + @Test + public void provided_project() { + componentDb.insertComponent(newProjectDto(PROJECT_UUID)); + userSession.anonymous().addProjectUuidPermissions(UserRole.USER, PROJECT_UUID); + insertNclocMetric(); + + ComponentWsResponse response = newRequest(PROJECT_UUID, "ncloc"); + + assertThat(response.getMetrics().getMetricsCount()).isEqualTo(1); + assertThat(response.getPeriods().getPeriodsCount()).isEqualTo(0); + assertThat(response.getComponent().getId()).isEqualTo(PROJECT_UUID); + } + + @Test + public void without_additional_fields() { + componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid")); + insertNclocMetric(); + + String response = ws.newRequest() + .setParam(PARAM_COMPONENT_ID, "project-uuid") + .setParam(PARAM_METRIC_KEYS, "ncloc") + .execute().getInput(); + + assertThat(response) + .doesNotContain("periods") + .doesNotContain("metrics"); + } + + @Test + public void reference_uuid_in_the_response() { + ComponentDto project = newProjectDto("project-uuid"); + ComponentDto view = newView("view-uuid"); + componentDb.insertViewAndSnapshot(view); + componentDb.insertProjectAndSnapshot(project); + componentDb.insertProjectAndSnapshot(newProjectCopy("project-uuid-copy", project, view)); + insertNclocMetric(); + + ComponentWsResponse response = newRequest("project-uuid-copy", "ncloc"); + + assertThat(response.getComponent().getId()).isEqualTo("project-uuid-copy"); + assertThat(response.getComponent().getRefId()).isEqualTo("project-uuid"); + } + + @Test + public void fail_when_a_metric_is_not_found() { + componentDb.insertProjectAndSnapshot(newProjectDto(PROJECT_UUID)); + insertNclocMetric(); + insertComplexityMetric(); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("The following metric keys are not found: unknown-metric, another-unknown-metric"); + + newRequest(PROJECT_UUID, "ncloc, complexity, unknown-metric, another-unknown-metric"); + } + + @Test + public void fail_when_empty_metric_keys_parameter() { + componentDb.insertProjectAndSnapshot(newProjectDto(PROJECT_UUID)); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("At least one metric key must be provided"); + + newRequest(PROJECT_UUID, ""); + } + + @Test + public void fail_when_not_enough_permission() { + userSession.setGlobalPermissions(GlobalPermissions.QUALITY_PROFILE_ADMIN); + componentDb.insertProjectAndSnapshot(newProjectDto(PROJECT_UUID)); + insertNclocMetric(); + + expectedException.expect(ForbiddenException.class); + + newRequest(PROJECT_UUID, "ncloc"); + } + + private ComponentWsResponse newRequest(String componentUuid, String metricKeys) { + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_COMPONENT_ID, componentUuid) + .setParam(PARAM_METRIC_KEYS, metricKeys) + .setParam(PARAM_ADDITIONAL_FIELDS, "metrics,periods") + .execute() + .getInputStream(); + + try { + return ComponentWsResponse.parseFrom(responseStream); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static MetricDto newMetricDtoWithoutOptimization() { + return newMetricDto() + .setWorstValue(null) + .setOptimizedBestValue(false) + .setBestValue(null) + .setUserManaged(false); + } + + private MetricDto insertNclocMetric() { + MetricDto metric = dbClient.metricDao().insert(dbSession, newMetricDtoWithoutOptimization() + .setKey("ncloc") + .setShortName("Lines of code") + .setDescription("Non Commenting Lines of Code") + .setDomain("Size") + .setValueType("INT") + .setDirection(-1) + .setQualitative(false) + .setHidden(false) + .setUserManaged(false)); + db.commit(); + return metric; + } + + private MetricDto insertComplexityMetric() { + MetricDto metric = dbClient.metricDao().insert(dbSession, newMetricDtoWithoutOptimization() + .setKey("complexity") + .setShortName("Complexity") + .setDescription("Cyclomatic complexity") + .setDomain("Complexity") + .setValueType("INT") + .setDirection(-1) + .setQualitative(false) + .setHidden(false) + .setUserManaged(false)); + db.commit(); + return metric; + } + + private MetricDto insertNewViolationMetric() { + MetricDto metric = dbClient.metricDao().insert(dbSession, newMetricDtoWithoutOptimization() + .setKey("new_violations") + .setShortName("New issues") + .setDescription("New Issues") + .setDomain("Issues") + .setValueType("INT") + .setDirection(-1) + .setQualitative(true) + .setHidden(false) + .setUserManaged(false)); + db.commit(); + return metric; + } + + private void insertJsonExampleData() { + ComponentDto project = newProjectDto(PROJECT_UUID); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + + ComponentDto file = newFileDto(project) + .setUuid("AVIwDXE-bJbJqrw6wFv5") + .setKey("MY_PROJECT:ElementImpl.java") + .setName("ElementImpl.java") + .setQualifier(Qualifiers.FILE) + .setPath("src/main/java/com/sonarsource/markdown/impl/ElementImpl.java"); + componentDb.insertComponent(file); + SnapshotDto fileSnapshot = dbClient.snapshotDao().insert(dbSession, createForComponent(file, projectSnapshot) + .setPeriodDate(1, parseDateTime("2016-01-11T10:49:50+0100").getTime()) + .setPeriodMode(1, "previous_version") + .setPeriodParam(1, "1.0-SNAPSHOT") + .setPeriodDate(2, parseDateTime("2016-01-11T10:50:06+0100").getTime()) + .setPeriodMode(2, "previous_analysis") + .setPeriodParam(2, "2016-01-11") + .setPeriodDate(3, parseDateTime("2016-01-11T10:38:45+0100").getTime()) + .setPeriodMode(3, "days") + .setPeriodParam(3, "30")); + + MetricDto complexity = insertComplexityMetric(); + dbClient.measureDao().insert(dbSession, + newMeasureDto(complexity, fileSnapshot.getId()) + .setValue(12.0d) + .setVariation(1, 2.0d) + .setVariation(2, 0.0d) + .setVariation(3, 0.0d)); + + MetricDto ncloc = insertNclocMetric(); + dbClient.measureDao().insert(dbSession, + newMeasureDto(ncloc, fileSnapshot.getId()) + .setValue(114.0d) + .setVariation(1, 3.0d) + .setVariation(2, -5.0d) + .setVariation(3, 5.0d)); + + MetricDto newViolations = insertNewViolationMetric(); + dbClient.measureDao().insert(dbSession, + newMeasureDto(newViolations, fileSnapshot.getId()) + .setVariation(1, 25.0d) + .setVariation(2, 0.0d) + .setVariation(3, 25.0d)); + db.commit(); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java index f8d35db0e7c..852eebc4a1b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java @@ -67,10 +67,10 @@ import static org.sonar.db.component.ComponentTesting.newProjectDto; import static org.sonar.db.component.SnapshotTesting.newSnapshotForProject; import static org.sonar.db.measure.MeasureTesting.newMeasureDto; import static org.sonar.db.metric.MetricTesting.newMetricDto; -import static org.sonar.server.measure.ws.ComponentTreeAction.ADDITIONAL_PERIODS; import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_SORT; import static org.sonar.server.measure.ws.ComponentTreeAction.NAME_SORT; import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ADDITIONAL_PERIODS; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_BASE_COMPONENT_ID; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS; diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java index 3e8b7946f73..67e60760bba 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java @@ -29,6 +29,6 @@ public class MeasuresWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new MeasuresWsModule().configure(container); - assertThat(container.size()).isEqualTo(3 + 2); + assertThat(container.size()).isEqualTo(4 + 2); } } diff --git a/sonar-db/src/main/java/org/sonar/db/measure/MeasureDtoFunctions.java b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDtoFunctions.java new file mode 100644 index 00000000000..6a189b51c92 --- /dev/null +++ b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDtoFunctions.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.measure; + +import com.google.common.base.Function; +import javax.annotation.Nonnull; + +public class MeasureDtoFunctions { + private MeasureDtoFunctions() { + // prevent instantiation + } + + public static Function toMetricId() { + return ToMetricId.INSTANCE; + } + + private enum ToMetricId implements Function { + INSTANCE; + + @Override + public Integer apply(@Nonnull MeasureDto input) { + return input.getMetricId(); + } + } +} diff --git a/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java b/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java index c97aba3b698..f2111f7066f 100644 --- a/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java +++ b/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java @@ -20,6 +20,7 @@ package org.sonar.db.metric; import com.google.common.base.Function; +import com.google.common.base.Predicate; import javax.annotation.Nonnull; /** @@ -38,6 +39,10 @@ public class MetricDtoFunctions { return ToKey.INSTANCE; } + public static Predicate isOptimizedForBestValue() { + return IsMetricOptimizedForBestValue.INSTANCE; + } + private enum ToId implements Function { INSTANCE; @@ -55,4 +60,13 @@ public class MetricDtoFunctions { return input.getKey(); } } + + private enum IsMetricOptimizedForBestValue implements Predicate { + INSTANCE; + + @Override + public boolean apply(@Nonnull MetricDto input) { + return input.isOptimizedBestValue() && input.getBestValue() != null; + } + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentWsRequest.java new file mode 100644 index 00000000000..df6a800f9d5 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentWsRequest.java @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarqube.ws.client.measure; + +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +public class ComponentWsRequest { + private String componentId; + private String componentKey; + private List metricKeys; + private List additionalFields; + + @CheckForNull + public String getComponentId() { + return componentId; + } + + public ComponentWsRequest setComponentId(@Nullable String componentId) { + this.componentId = componentId; + return this; + } + + @CheckForNull + public String getComponentKey() { + return componentKey; + } + + public ComponentWsRequest setComponentKey(@Nullable String componentKey) { + this.componentKey = componentKey; + return this; + } + + public List getMetricKeys() { + return metricKeys; + } + + public ComponentWsRequest setMetricKeys(@Nullable List metricKeys) { + this.metricKeys = metricKeys; + return this; + } + + @CheckForNull + public List getAdditionalFields() { + return additionalFields; + } + + public ComponentWsRequest setAdditionalFields(@Nullable List additionalFields) { + this.additionalFields = additionalFields; + return this; + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java index 01a9bf7b59d..7fb0019e03f 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java @@ -19,16 +19,20 @@ */ package org.sonarqube.ws.client.measure; -import org.sonarqube.ws.WsMeasures; +import org.sonarqube.ws.WsMeasures.ComponentTreeWsResponse; +import org.sonarqube.ws.WsMeasures.ComponentWsResponse; import org.sonarqube.ws.client.BaseService; import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.WsConnector; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ACTION_COMPONENT; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ACTION_COMPONENT_TREE; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.CONTROLLER_MEASURES; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_BASE_COMPONENT_ID; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_BASE_COMPONENT_KEY; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_COMPONENT_ID; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_COMPONENT_KEY; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT; import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS; @@ -39,7 +43,7 @@ public class MeasuresService extends BaseService { super(wsConnector, CONTROLLER_MEASURES); } - public WsMeasures.ComponentTreeWsResponse componentTree(ComponentTreeWsRequest request) { + public ComponentTreeWsResponse componentTree(ComponentTreeWsRequest request) { GetRequest getRequest = new GetRequest(path(ACTION_COMPONENT_TREE)) .setParam(PARAM_BASE_COMPONENT_ID, request.getBaseComponentId()) .setParam(PARAM_BASE_COMPONENT_KEY, request.getBaseComponentKey()) @@ -54,6 +58,16 @@ public class MeasuresService extends BaseService { .setParam("asc", request.getAsc()) .setParam(PARAM_METRIC_SORT, request.getMetricSort()); - return call(getRequest, WsMeasures.ComponentTreeWsResponse.parser()); + return call(getRequest, ComponentTreeWsResponse.parser()); + } + + public ComponentWsResponse component(ComponentWsRequest request) { + GetRequest getRequest = new GetRequest(path(ACTION_COMPONENT)) + .setParam(PARAM_COMPONENT_ID, request.getComponentId()) + .setParam(PARAM_COMPONENT_KEY, request.getComponentKey()) + .setParam(PARAM_ADDITIONAL_FIELDS, inlineMultipleParamValue(request.getAdditionalFields())) + .setParam(PARAM_METRIC_KEYS, inlineMultipleParamValue(request.getMetricKeys())); + + return call(getRequest, ComponentWsResponse.parser()); } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java index 3f7c9adc52f..5874c4ab8e0 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java @@ -20,22 +20,33 @@ package org.sonarqube.ws.client.measure; -public class MeasuresWsParameters { - private MeasuresWsParameters() { - // static constants only - } +import com.google.common.collect.ImmutableSortedSet; +import java.util.Set; +public class MeasuresWsParameters { public static final String CONTROLLER_MEASURES = "api/measures"; // actions public static final String ACTION_COMPONENT_TREE = "component_tree"; + public static final String ACTION_COMPONENT = "component"; // parameters public static final String PARAM_BASE_COMPONENT_ID = "baseComponentId"; + public static final String PARAM_BASE_COMPONENT_KEY = "baseComponentKey"; public static final String PARAM_STRATEGY = "strategy"; public static final String PARAM_QUALIFIERS = "qualifiers"; public static final String PARAM_METRIC_KEYS = "metricKeys"; public static final String PARAM_METRIC_SORT = "metricSort"; public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields"; + public static final String PARAM_COMPONENT_ID = "componentId"; + public static final String PARAM_COMPONENT_KEY = "componentKey"; + public static final String ADDITIONAL_METRICS = "metrics"; + + public static final String ADDITIONAL_PERIODS = "periods"; + public static final Set ADDITIONAL_FIELDS = ImmutableSortedSet.of(ADDITIONAL_METRICS, ADDITIONAL_PERIODS); + + private MeasuresWsParameters() { + // static constants only + } } diff --git a/sonar-ws/src/main/protobuf/ws-measures.proto b/sonar-ws/src/main/protobuf/ws-measures.proto index 6014576568f..eec10749c8f 100644 --- a/sonar-ws/src/main/protobuf/ws-measures.proto +++ b/sonar-ws/src/main/protobuf/ws-measures.proto @@ -35,6 +35,13 @@ message ComponentTreeWsResponse { optional Periods periods = 5; } +// WS api/measures/component +message ComponentWsResponse { + optional Component component = 1; + optional Metrics metrics = 2; + optional Periods periods = 3; +} + message Component { optional string id = 1; optional string key = 2; -- 2.39.5