]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7134 WS api/measures/component
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Tue, 12 Jan 2016 11:45:48 +0000 (12:45 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 20 Jan 2016 11:45:58 +0000 (12:45 +0100)
20 files changed:
it/it-tests/src/test/java/it/measure/MeasuresWsTest.java
server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsParametersBuilder.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/SnapshotDtoToWsPeriods.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java
sonar-db/src/main/java/org/sonar/db/measure/MeasureDtoFunctions.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java
sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentWsRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java
sonar-ws/src/main/protobuf/ws-measures.proto

index 9a694b012a4bd1e78d583e3770ea9228be1fc098..60af1d3416e7064301f92b2c96fe087360cb341e 100644 (file)
@@ -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");
+
+  }
 }
index 63be251b2faf16f258e6c3e4f4a373dc427f3874..d2b84d00be212165de841a7566336128c13ebe51 100644 (file)
@@ -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 (file)
index 0000000..b6a62d5
--- /dev/null
@@ -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<String> 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.<br>" +
+        "Requires one of the following permissions:" +
+        "<ul>" +
+        "<li>'Administer System'</li>" +
+        "<li>'Administer' rights on the specified project</li>" +
+        "<li>'Browse' on the specified project</li>" +
+        "</ul>",
+        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<ComponentDto> refComponent = getReferenceComponent(dbSession, component);
+      checkPermissions(component);
+      SnapshotDto lastSnapshot = dbClient.snapshotDao().selectLastSnapshotByComponentId(dbSession, component.getId());
+      List<MetricDto> metrics = searchMetrics(dbSession, request);
+      List<WsMeasures.Period> periods = snapshotsToPeriods(lastSnapshot);
+      List<MeasureDto> measures = searchMeasures(dbSession, component, lastSnapshot, metrics, periods);
+
+      return buildResponse(request, component, refComponent, measures, metrics, periods);
+    } finally {
+      dbClient.closeSession(dbSession);
+    }
+  }
+
+  private Optional<ComponentDto> 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<ComponentDto> refComponent, List<MeasureDto> measures,
+    List<MetricDto> metrics, List<WsMeasures.Period> periods) {
+    ComponentWsResponse.Builder response = ComponentWsResponse.newBuilder();
+    Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDtoFunctions.toId());
+    Map<MetricDto, MeasureDto> measuresByMetric = new HashMap<>();
+    for (MeasureDto measure : measures) {
+      MetricDto metric = metricsById.get(measure.getMetricId());
+      measuresByMetric.put(metric, measure);
+    }
+    Map<Long, String> referenceComponentUuidById = new HashMap<>();
+    if (refComponent.isPresent()) {
+      referenceComponentUuidById.put(refComponent.get().getId(), refComponent.get().uuid());
+    }
+
+    response.setComponent(componentDtoToWsComponent(component, measuresByMetric, referenceComponentUuidById));
+
+    List<String> 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<MetricDto> searchMetrics(DbSession dbSession, ComponentWsRequest request) {
+    List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, request.getMetricKeys());
+    if (metrics.size() < request.getMetricKeys().size()) {
+      List<String> foundMetricKeys = Lists.transform(metrics, MetricDtoFunctions.toKey());
+      Set<String> 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<MeasureDto> searchMeasures(DbSession dbSession, ComponentDto component, @Nullable SnapshotDto snapshot, List<MetricDto> metrics, List<WsMeasures.Period> periods) {
+    if (snapshot == null) {
+      return emptyList();
+    }
+
+    List<Integer> metricIds = Lists.transform(metrics, MetricDtoFunctions.toId());
+    List<MeasureDto> measures = dbClient.measureDao().selectBySnapshotIdsAndMetricIds(dbSession, singletonList(snapshot.getId()), metricIds);
+    addBestValuesToMeasures(measures, component, metrics, periods);
+
+    return measures;
+  }
+
+  /**
+   * Conditions for best value measure:
+   * <ul>
+   * <li>component is a production file or test file</li>
+   * <li>metric is optimized for best value</li>
+   * </ul>
+   */
+  private static void addBestValuesToMeasures(List<MeasureDto> measures, ComponentDto component, List<MetricDto> metrics, List<WsMeasures.Period> periods) {
+    if (!QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE.contains(component.qualifier())) {
+      return;
+    }
+
+    List<MetricDtoWithBestValue> metricWithBestValueList = from(metrics)
+      .filter(MetricDtoFunctions.isOptimizedForBestValue())
+      .transform(new MetricDtoToMetricDtoWithBestValueFunction(periods))
+      .toList();
+    Map<Integer, MeasureDto> 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();
+    }
+  }
+}
index ca904c0576566bba205a0fb5e2b6500df362b44f..c1e084147842c2e38fb57399a1edec6baa4e56c1 100644 (file)
@@ -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<String> 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<String> 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)
index e73cb8cc1a5848e1dbd8f300a375316559067cb8..79d26ae7666ab47c2a4c38fae93b8939885eb71b 100644 (file)
@@ -212,7 +212,7 @@ public class ComponentTreeDataLoader {
   private static void addBestValuesToMeasures(Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric, List<ComponentDtoWithSnapshotId> components,
     List<MetricDto> metrics, List<WsMeasures.Period> periods) {
     List<MetricDtoWithBestValue> 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<ComponentDtoWithSnapshotId> 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<MetricDto> {
-    INSTANCE;
-
-    @Override
-    public boolean apply(@Nonnull MetricDto input) {
-      return input.isOptimizedBestValue() && input.getBestValue() != null;
-    }
-  }
-
   private enum IsFileComponent implements Predicate<ComponentDtoWithSnapshotId> {
     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<Integer> 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<WsMeasures.Period, Integer> {
     INSTANCE;
 
index 710d2196feb7615923f4d2c457bdd0b7b891aefd..50b7c1a02921fd09d6e07088c8aacf353e49b818 100644 (file)
@@ -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<ComponentDtoWithSnapshotId> metricOrdering(ComponentTreeWsRequest wsRequest, List<MetricDto> metrics,
     Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric) {
index 7743470557c5ffb0d6e584655fa871f12a2fec7f..23d6b83281443eb6d55103b1a72ba7ab5b3f0ad6 100644 (file)
@@ -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 (file)
index 0000000..4107953
--- /dev/null
@@ -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 (file)
index 0000000..7c235fb
--- /dev/null
@@ -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<Integer> 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<MetricDto, MetricDtoWithBestValue> {
+    private final List<Integer> periodIndexes;
+
+    MetricDtoToMetricDtoWithBestValueFunction(List<WsMeasures.Period> 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<WsMeasures.Period, Integer> {
+    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 (file)
index 0000000..8418f3a
--- /dev/null
@@ -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<WsMeasures.Period> snapshotsToPeriods(@Nullable SnapshotDto snapshot) {
+    if (snapshot == null) {
+      return emptyList();
+    }
+
+    List<WsMeasures.Period> 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 (file)
index 0000000..f5f9855
--- /dev/null
@@ -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 (file)
index 0000000..955ddcf
--- /dev/null
@@ -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();
+  }
+
+}
index f8d35db0e7cb5ea268dd9d89fae0ed54e7b2bdea..852eebc4a1b257a1c7dfbae2240e866d2d9a1935 100644 (file)
@@ -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;
index 3e8b7946f7393227da1b96ef075e1c32fd1d3217..67e60760bba160be222de5d3b1070132be8967be 100644 (file)
@@ -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 (file)
index 0000000..6a189b5
--- /dev/null
@@ -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<MeasureDto, Integer> toMetricId() {
+    return ToMetricId.INSTANCE;
+  }
+
+  private enum ToMetricId implements Function<MeasureDto, Integer> {
+    INSTANCE;
+
+    @Override
+    public Integer apply(@Nonnull MeasureDto input) {
+      return input.getMetricId();
+    }
+  }
+}
index c97aba3b698709c9ef2e4021067f39a440064989..f2111f7066f4772824925f0c88def00864853493 100644 (file)
@@ -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<MetricDto> isOptimizedForBestValue() {
+    return IsMetricOptimizedForBestValue.INSTANCE;
+  }
+
   private enum ToId implements Function<MetricDto, Integer> {
     INSTANCE;
 
@@ -55,4 +60,13 @@ public class MetricDtoFunctions {
       return input.getKey();
     }
   }
+
+  private enum IsMetricOptimizedForBestValue implements Predicate<MetricDto> {
+    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 (file)
index 0000000..df6a800
--- /dev/null
@@ -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<String> metricKeys;
+  private List<String> 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<String> getMetricKeys() {
+    return metricKeys;
+  }
+
+  public ComponentWsRequest setMetricKeys(@Nullable List<String> metricKeys) {
+    this.metricKeys = metricKeys;
+    return this;
+  }
+
+  @CheckForNull
+  public List<String> getAdditionalFields() {
+    return additionalFields;
+  }
+
+  public ComponentWsRequest setAdditionalFields(@Nullable List<String> additionalFields) {
+    this.additionalFields = additionalFields;
+    return this;
+  }
+}
index 01a9bf7b59d58c27aba8b9b7a4478ba8577eb6df..7fb0019e03fb97a8fa229fcf68bb38271e0ad8d4 100644 (file)
  */
 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());
   }
 }
index 3f7c9adc52fd6c5364bbac9bfeb745eb9ecd0615..5874c4ab8e08d33474f03ea381f38e127a878d94 100644 (file)
 
 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<String> ADDITIONAL_FIELDS = ImmutableSortedSet.of(ADDITIONAL_METRICS, ADDITIONAL_PERIODS);
+
+  private MeasuresWsParameters() {
+    // static constants only
+  }
 }
index 6014576568fdad09a458e5b8750b6d43046663de..eec10749c8fff130cb00856ffaa702b3f5d40750 100644 (file)
@@ -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;