]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7135 WS api/measures/component_tree navigate through components and display... 707/head
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 6 Jan 2016 16:46:38 +0000 (17:46 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 13 Jan 2016 13:13:35 +0000 (14:13 +0100)
53 files changed:
it/it-tests/src/test/java/it/measure/MeasuresWsTest.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentDtoToWsComponent.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeData.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureValueFormatter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWs.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoToWsMetric.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/component/ComponentFinderTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsTest.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/MyBatis.java
sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java
sonar-db/src/main/java/org/sonar/db/component/ComponentDtoFunctions.java
sonar-db/src/main/java/org/sonar/db/component/ComponentDtoWithSnapshotId.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java
sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java
sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java
sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java
sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java [new file with mode: 0644]
sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml
sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml
sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java
sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java
sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java
sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java
sonar-db/src/test/java/org/sonar/db/measure/MeasureDaoTest.java
sonar-db/src/test/java/org/sonar/db/measure/MeasureTesting.java
sonar-db/src/test/java/org/sonar/db/metric/MetricTesting.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java
sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentTreeWsRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/measure/package-info.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-commons.proto
sonar-ws/src/main/protobuf/ws-measures.proto [new file with mode: 0644]
sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java
sonar-ws/src/test/java/org/sonarqube/ws/client/measure/MeasuresServiceTest.java [new file with mode: 0644]

diff --git a/it/it-tests/src/test/java/it/measure/MeasuresWsTest.java b/it/it-tests/src/test/java/it/measure/MeasuresWsTest.java
new file mode 100644 (file)
index 0000000..b62e390
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube Integration Tests :: Tests
+ * 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 it.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarRunner;
+import it.Category4Suite;
+import java.util.List;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.ComponentTreeWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.measure.ComponentTreeWsRequest;
+import util.ItUtils;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.setServerProperty;
+
+public class MeasuresWsTest {
+  @ClassRule
+  public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+  private static final String FILE_KEY = "sample:src/main/xoo/sample/Sample.xoo";
+  WsClient wsClient;
+
+  @BeforeClass
+  public static void initPeriods() throws Exception {
+    setServerProperty(orchestrator, "sonar.timemachine.period1", "previous_analysis");
+    setServerProperty(orchestrator, "sonar.timemachine.period2", "30");
+    setServerProperty(orchestrator, "sonar.timemachine.period3", "previous_version");
+  }
+
+  @AfterClass
+  public static void resetPeriods() throws Exception {
+    ItUtils.resetPeriods(orchestrator);
+  }
+
+  @Before
+  public void inspectProject() {
+    orchestrator.resetData();
+    orchestrator.executeBuild(SonarRunner.create(projectDir("shared/xoo-sample")));
+
+    wsClient = ItUtils.newAdminWsClient(orchestrator);
+  }
+
+  @Test
+  public void component_tree() {
+    ComponentTreeWsResponse response = wsClient.measures().componentTree(new ComponentTreeWsRequest()
+      .setBaseComponentKey("sample")
+      .setMetricKeys(singletonList("ncloc"))
+      .setAdditionalFields(newArrayList("metrics", "periods")));
+
+    assertThat(response).isNotNull();
+    assertThat(response.getMetrics().getMetricsList()).extracting("key").containsOnly("ncloc");
+    List<WsMeasures.Component> components = response.getComponentsList();
+    assertThat(components).hasSize(2).extracting("key").containsOnly("sample:src/main/xoo/sample", FILE_KEY);
+    assertThat(components.get(0).getMeasuresList().get(0).getValue()).isEqualTo("13");
+  }
+}
index 93978a428508544ffad6caafd8182d4c1c736f9e..105c343b5065ff6fd79c09b7f093b6a1bc29a723 100644 (file)
@@ -48,8 +48,11 @@ public class ComponentFinder {
     checkArgument(componentUuid != null ^ componentKey != null, MSG_COMPONENT_ID_OR_KEY_TEMPLATE, parameterNames.getUuidParam(), parameterNames.getKeyParam());
 
     if (componentUuid != null) {
+      checkArgument(!componentUuid.isEmpty(), "The '%s' parameter must not be empty", parameterNames.getUuidParam());
       return getByUuid(dbSession, componentUuid);
     }
+
+    checkArgument(!componentKey.isEmpty(), "The '%s' parameter must not be empty", parameterNames.getKeyParam());
     return getByKey(dbSession, componentKey);
   }
 
index 33caeefffed0bc3ec8d0c4d4d2401cf3f98ca2cd..62aeefb9fbec5d971004377cc4ebdf2bbb210985 100644 (file)
@@ -35,6 +35,7 @@ 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.ComponentDtoWithSnapshotId;
 import org.sonar.db.component.ComponentTreeQuery;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.server.component.ComponentFinder;
@@ -66,7 +67,8 @@ public class TreeAction implements ComponentsWsAction {
   private static final String LEAVES_STRATEGY = "leaves";
   private static final Set<String> STRATEGIES = ImmutableSortedSet.of(ALL_STRATEGY, CHILDREN_STRATEGY, LEAVES_STRATEGY);
   private static final String NAME_SORT = "name";
-  private static final Set<String> SORTS = ImmutableSortedSet.of(NAME_SORT, "path", "qualifier");
+  private static final String PATH_SORT = "path";
+  private static final Set<String> SORTS = ImmutableSortedSet.of(NAME_SORT, PATH_SORT, "qualifier");
 
   private final DbClient dbClient;
   private final ComponentFinder componentFinder;
@@ -98,9 +100,12 @@ public class TreeAction implements ComponentsWsAction {
       .setResponseExample(getClass().getResource("tree-example.json"))
       .setHandler(this)
       .addSearchQuery("sonar", "component names", "component keys")
-      .addMultiSortsParams(newHashSet(SORTS), NAME_SORT, true)
       .addPagingParams(100, MAX_SIZE);
 
+    action.createSortParams(newHashSet(SORTS), NAME_SORT, true)
+      .setDescription("Comma-separated list of sort fields")
+      .setExampleValue(NAME_SORT + ", " + PATH_SORT);
+
     action.createParam(PARAM_BASE_COMPONENT_ID)
       .setDescription("Base component id. The search is based on this component. It is not included in the response.")
       .setExampleValue(UUID_EXAMPLE_02);
@@ -139,7 +144,7 @@ public class TreeAction implements ComponentsWsAction {
       }
 
       ComponentTreeQuery query = toComponentTreeQuery(treeWsRequest, baseSnapshot);
-      List<ComponentDto> components;
+      List<ComponentDtoWithSnapshotId> components;
       int total;
       switch (treeWsRequest.getStrategy()) {
         case CHILDREN_STRATEGY:
@@ -171,7 +176,7 @@ public class TreeAction implements ComponentsWsAction {
     }
   }
 
-  private static TreeWsResponse buildResponse(List<ComponentDto> components, Paging paging) {
+  private static TreeWsResponse buildResponse(List<ComponentDtoWithSnapshotId> components, Paging paging) {
     TreeWsResponse.Builder response = TreeWsResponse.newBuilder();
     response.getPagingBuilder()
       .setPageIndex(paging.pageIndex())
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentDtoToWsComponent.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentDtoToWsComponent.java
new file mode 100644 (file)
index 0000000..f2d35bd
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube :: Server
+ * 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.Map;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonarqube.ws.WsMeasures;
+
+import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.measureDtoToWsMeasure;
+
+class ComponentDtoToWsComponent {
+  private ComponentDtoToWsComponent() {
+    // static methods only
+  }
+
+  static WsMeasures.Component.Builder componentDtoToWsComponent(ComponentDto component, Map<MetricDto, MeasureDto> measuresByMetric,
+    Map<Long, String> referenceComponentUuidsById) {
+    WsMeasures.Component.Builder wsComponent = componentDtoToWsComponent(component);
+
+    if (!referenceComponentUuidsById.isEmpty() && referenceComponentUuidsById.get(component.getCopyResourceId()) != null) {
+      wsComponent.setRefId(referenceComponentUuidsById.get(component.getCopyResourceId()));
+    }
+
+    for (Map.Entry<MetricDto, MeasureDto> entry : measuresByMetric.entrySet()) {
+      wsComponent.getMeasuresBuilder().addMeasures(measureDtoToWsMeasure(entry.getKey(), entry.getValue()));
+    }
+
+    return wsComponent;
+  }
+
+  static WsMeasures.Component.Builder componentDtoToWsComponent(ComponentDto component) {
+    WsMeasures.Component.Builder wsComponent = WsMeasures.Component.newBuilder()
+      .setId(component.uuid())
+      .setKey(component.key())
+      .setName(component.name())
+      .setQualifier(component.qualifier());
+    if (component.path() != null) {
+      wsComponent.setPath(component.path());
+    }
+    if (component.description() != null) {
+      wsComponent.setDescription(component.description());
+    }
+
+    return wsComponent;
+  }
+}
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
new file mode 100644 (file)
index 0000000..0dbc596
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+ * SonarQube :: Server
+ * 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.collect.ImmutableSortedSet;
+import java.util.Set;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.resources.ResourceTypes;
+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.server.ws.WebService.Param;
+import org.sonar.api.utils.Paging;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.ComponentTreeWsResponse;
+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.MetricDtoToWsMetric.metricDtoToWsMetric;
+import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext;
+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.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_METRIC_KEYS;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_STRATEGY;
+
+public class ComponentTreeAction implements MeasuresWsAction {
+  private static final int MAX_SIZE = 500;
+  static final String ALL_STRATEGY = "all";
+  static final String CHILDREN_STRATEGY = "children";
+  static final String LEAVES_STRATEGY = "leaves";
+  static final Set<String> STRATEGIES = ImmutableSortedSet.of(ALL_STRATEGY, CHILDREN_STRATEGY, LEAVES_STRATEGY);
+  static final String NAME_SORT = "name";
+  static final String PATH_SORT = "path";
+  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;
+  private final I18n i18n;
+  private final ResourceTypes resourceTypes;
+
+  public ComponentTreeAction(ComponentTreeDataLoader dataLoader, UserSession userSession, I18n i18n,
+    ResourceTypes resourceTypes) {
+    this.dataLoader = dataLoader;
+    this.userSession = userSession;
+    this.i18n = i18n;
+    this.resourceTypes = resourceTypes;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction(ACTION_COMPONENT_TREE)
+      .setDescription(format("Navigate through components based on the chosen strategy 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>" +
+        "When limiting search with the %s parameter, directories are not returned.",
+        PARAM_BASE_COMPONENT_ID, PARAM_BASE_COMPONENT_KEY, Param.TEXT_QUERY))
+      .setResponseExample(getClass().getResource("component_tree-example.json"))
+      .setSince("5.4")
+      .setHandler(this)
+      .addPagingParams(100, MAX_SIZE);
+
+    action.createSortParams(SORTS, NAME_SORT, true)
+      .setDescription("Comma-separated list of sort fields")
+      .setExampleValue(NAME_SORT + ", " + PATH_SORT);
+
+    action.createParam(Param.TEXT_QUERY)
+      .setDescription("Limit search to: <ul>" +
+        "<li>component names that contain the supplied string</li>" +
+        "<li>component keys that are exactly the same as the supplied string</li>" +
+        "</ul>")
+      .setExampleValue("FILE_NAM");
+
+    action.createParam(PARAM_BASE_COMPONENT_ID)
+      .setDescription("Base component id. The search is based on this component. It is not included in the response.")
+      .setExampleValue(UUID_EXAMPLE_02);
+
+    action.createParam(PARAM_BASE_COMPONENT_KEY)
+      .setDescription("Base component key.The search is based on this component. It is not included in the response.")
+      .setExampleValue("org.apache.hbas:hbase");
+
+    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");
+
+    createQualifiersParameter(action, newQualifierParameterContext(userSession, i18n, resourceTypes));
+
+    action.createParam(PARAM_STRATEGY)
+      .setDescription("Strategy to search for base component descendants:" +
+        "<ul>" +
+        "<li>children: return the children components of the base component. Grandchildren components are not returned</li>" +
+        "<li>all: return all the descendants components of the base component. Grandchildren are returned. Base component is not returned.</li>" +
+        "<li>leaves: return all the descendant components (files, in general) which don't have other children. They are the leaves of the component tree.</li>" +
+        "</ul>")
+      .setPossibleValues(STRATEGIES)
+      .setDefaultValue(ALL_STRATEGY);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    ComponentTreeWsResponse componentTreeWsResponse = doHandle(toComponentTreeWsRequest(request));
+    writeProtobuf(componentTreeWsResponse, request, response);
+  }
+
+  private ComponentTreeWsResponse doHandle(ComponentTreeWsRequest request) {
+    ComponentTreeData data = dataLoader.load(request);
+    if (data.getComponents()==null) {
+      return emptyResponse(data.getBaseComponent(), request);
+    }
+
+    return buildResponse(
+      request,
+      data,
+      Paging.forPageIndex(
+        request.getPage())
+        .withPageSize(request.getPageSize())
+        .andTotal(data.getComponentCount()));
+  }
+
+  private static ComponentTreeWsResponse buildResponse(ComponentTreeWsRequest request, ComponentTreeData data, Paging paging) {
+    ComponentTreeWsResponse.Builder response = ComponentTreeWsResponse.newBuilder();
+    response.getPagingBuilder()
+      .setPageIndex(paging.pageIndex())
+      .setPageSize(paging.pageSize())
+      .setTotal(paging.total())
+      .build();
+
+    response.setBaseComponent(componentDtoToWsComponent(data.getBaseComponent()));
+
+    for (ComponentDto componentDto : data.getComponents()) {
+      response.addComponents(componentDtoToWsComponent(
+        componentDto,
+        data.getMeasuresByComponentUuidAndMetric().row(componentDto.uuid()),
+        data.getReferenceComponentUuidsById()));
+    }
+
+    if (areMetricsInResponse(request)) {
+      WsMeasures.Metrics.Builder metricsBuilder = response.getMetricsBuilder();
+      for (MetricDto metricDto : data.getMetrics()) {
+        metricsBuilder.addMetrics(metricDtoToWsMetric(metricDto));
+      }
+    }
+
+    if (arePeriodsInResponse(request)) {
+      response.getPeriodsBuilder().addAllPeriods(data.getPeriods());
+    }
+
+    return response.build();
+  }
+
+  private static boolean areMetricsInResponse(ComponentTreeWsRequest request) {
+    return request.getAdditionalFields() != null && request.getAdditionalFields().contains(ADDITIONAL_METRICS);
+  }
+
+  private static boolean arePeriodsInResponse(ComponentTreeWsRequest request) {
+    return request.getAdditionalFields() != null && request.getAdditionalFields().contains(ADDITIONAL_PERIODS);
+  }
+
+  private static ComponentTreeWsResponse emptyResponse(ComponentDto baseComponent, ComponentTreeWsRequest request) {
+    ComponentTreeWsResponse.Builder response = ComponentTreeWsResponse.newBuilder();
+    response.getPagingBuilder()
+      .setPageIndex(request.getPage())
+      .setPageSize(request.getPageSize())
+      .setTotal(0);
+    response.setBaseComponent(componentDtoToWsComponent(baseComponent));
+    return response.build();
+  }
+
+  private static ComponentTreeWsRequest toComponentTreeWsRequest(Request request) {
+    ComponentTreeWsRequest componentTreeWsRequest = new ComponentTreeWsRequest()
+      .setBaseComponentId(request.param(PARAM_BASE_COMPONENT_ID))
+      .setBaseComponentKey(request.param(PARAM_BASE_COMPONENT_KEY))
+      .setMetricKeys(request.mandatoryParamAsStrings(PARAM_METRIC_KEYS))
+      .setStrategy(request.mandatoryParam(PARAM_STRATEGY))
+      .setQualifiers(request.paramAsStrings(PARAM_QUALIFIERS))
+      .setAdditionalFields(request.paramAsStrings(PARAM_ADDITIONAL_FIELDS))
+      .setSort(request.paramAsStrings(Param.SORT))
+      .setAsc(request.paramAsBoolean(Param.ASCENDING))
+      .setMetricSort(request.param(PARAM_METRIC_SORT))
+      .setPage(request.mandatoryParamAsInt(Param.PAGE))
+      .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
+      .setQuery(request.param(Param.TEXT_QUERY));
+    String metricSortValue = componentTreeWsRequest.getMetricSort();
+    checkRequest(!componentTreeWsRequest.getMetricKeys().isEmpty(), "The '%s' parameter must contain at least one metric key", PARAM_METRIC_KEYS);
+    checkRequest(metricSortValue == null ^ componentTreeWsRequest.getSort().contains(METRIC_SORT),
+      "To sort by a metric, the '%s' parameter must contain '%s' and a metric key must be provided in the '%s' parameter",
+      Param.SORT, METRIC_SORT, PARAM_METRIC_SORT);
+    checkRequest(metricSortValue == null ^ componentTreeWsRequest.getMetricKeys().contains(metricSortValue),
+      "To sort by the '%s' metric, it must be in the list of metric keys in the '%s' parameter", metricSortValue, PARAM_METRIC_KEYS);
+    return componentTreeWsRequest;
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeData.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeData.java
new file mode 100644 (file)
index 0000000..6df0022
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * SonarQube :: Server
+ * 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.collect.Table;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentDtoWithSnapshotId;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonarqube.ws.WsMeasures;
+
+import static java.util.Objects.requireNonNull;
+
+class ComponentTreeData {
+  private final ComponentDto baseComponent;
+  private final List<ComponentDtoWithSnapshotId> components;
+  private final int componentCount;
+  private final Map<Long, String> referenceComponentUuidsById;
+  private final List<MetricDto> metrics;
+  private final List<WsMeasures.Period> periods;
+  private final Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric;
+
+  private ComponentTreeData(Builder builder) {
+    this.baseComponent = builder.baseComponent;
+    this.components = builder.componentsFromDb;
+    this.componentCount = builder.componentCount;
+    this.referenceComponentUuidsById = builder.referenceComponentUuidsById;
+    this.metrics = builder.metrics;
+    this.measuresByComponentUuidAndMetric = builder.measuresByComponentUuidAndMetric;
+    this.periods = builder.periods;
+  }
+
+  public ComponentDto getBaseComponent() {
+    return baseComponent;
+  }
+
+  @CheckForNull
+  List<ComponentDtoWithSnapshotId> getComponents() {
+    return components;
+  }
+
+  @CheckForNull
+  int getComponentCount() {
+    return componentCount;
+  }
+
+  @CheckForNull
+  public Map<Long, String> getReferenceComponentUuidsById() {
+    return referenceComponentUuidsById;
+  }
+
+  @CheckForNull
+  List<MetricDto> getMetrics() {
+    return metrics;
+  }
+
+  @CheckForNull
+  List<WsMeasures.Period> getPeriods() {
+    return periods;
+  }
+
+  @CheckForNull
+  Table<String, MetricDto, MeasureDto> getMeasuresByComponentUuidAndMetric() {
+    return measuresByComponentUuidAndMetric;
+  }
+
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static class Builder {
+    private ComponentDto baseComponent;
+    private List<ComponentDtoWithSnapshotId> componentsFromDb;
+    private Map<Long, String> referenceComponentUuidsById;
+    private int componentCount;
+    private List<MetricDto> metrics;
+    private List<WsMeasures.Period> periods;
+    private Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric;
+
+    private Builder() {
+      // private constructor
+    }
+
+    public Builder setBaseComponent(ComponentDto baseComponent) {
+      this.baseComponent = baseComponent;
+      return this;
+    }
+
+    public Builder setComponentsFromDb(List<ComponentDtoWithSnapshotId> componentsFromDbQuery) {
+      this.componentsFromDb = componentsFromDbQuery;
+      return this;
+    }
+
+    public Builder setComponentCount(int componentCount) {
+      this.componentCount = componentCount;
+      return this;
+    }
+
+    public Builder setMetrics(List<MetricDto> metrics) {
+      this.metrics = metrics;
+      return this;
+    }
+
+    public Builder setPeriods(List<WsMeasures.Period> periods) {
+      this.periods = periods;
+      return this;
+    }
+
+    public Builder setMeasuresByComponentUuidAndMetric(Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric) {
+      this.measuresByComponentUuidAndMetric = measuresByComponentUuidAndMetric;
+      return this;
+    }
+
+    public Builder setReferenceComponentUuidsById(Map<Long, String> referenceComponentsById) {
+      this.referenceComponentUuidsById = referenceComponentsById;
+      return this;
+    }
+
+    public ComponentTreeData build() {
+      requireNonNull(baseComponent);
+      return new ComponentTreeData(this);
+    }
+  }
+}
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
new file mode 100644 (file)
index 0000000..bc84c43
--- /dev/null
@@ -0,0 +1,436 @@
+/*
+ * SonarQube :: Server
+ * 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.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.utils.Paging;
+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.ComponentDtoWithSnapshotId;
+import org.sonar.db.component.ComponentTreeQuery;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.measure.MeasureDto;
+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.user.UserSession;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.client.measure.ComponentTreeWsRequest;
+
+import static com.google.common.base.Objects.firstNonNull;
+import static com.google.common.collect.FluentIterable.from;
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Sets.newHashSet;
+import static java.lang.String.format;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.server.component.ComponentFinder.ParamNames.BASE_COMPONENT_ID_AND_KEY;
+import static org.sonar.server.measure.ws.ComponentTreeAction.ALL_STRATEGY;
+import static org.sonar.server.measure.ws.ComponentTreeAction.CHILDREN_STRATEGY;
+import static org.sonar.server.measure.ws.ComponentTreeAction.LEAVES_STRATEGY;
+import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.NAME_SORT;
+import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
+
+public class ComponentTreeDataLoader {
+  private static final Set<String> QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE = newHashSet(Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE);
+
+  private final DbClient dbClient;
+  private final ComponentFinder componentFinder;
+  private final UserSession userSession;
+  private final ResourceTypes resourceTypes;
+
+  public ComponentTreeDataLoader(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, ResourceTypes resourceTypes) {
+    this.dbClient = dbClient;
+    this.componentFinder = componentFinder;
+    this.userSession = userSession;
+    this.resourceTypes = resourceTypes;
+  }
+
+  @CheckForNull
+  ComponentTreeData load(ComponentTreeWsRequest wsRequest) {
+    DbSession dbSession = dbClient.openSession(false);
+    try {
+      ComponentDto baseComponent = componentFinder.getByUuidOrKey(dbSession, wsRequest.getBaseComponentId(), wsRequest.getBaseComponentKey(), BASE_COMPONENT_ID_AND_KEY);
+      checkPermissions(baseComponent);
+      SnapshotDto baseSnapshot = dbClient.snapshotDao().selectLastSnapshotByComponentId(dbSession, baseComponent.getId());
+      if (baseSnapshot == null) {
+        return ComponentTreeData.builder()
+          .setBaseComponent(baseComponent)
+          .build();
+      }
+
+      ComponentTreeQuery dbQuery = toComponentTreeQuery(wsRequest, baseSnapshot);
+      ComponentDtosAndTotal componentDtosAndTotal = searchComponents(dbSession, dbQuery, wsRequest);
+      List<ComponentDtoWithSnapshotId> components = componentDtosAndTotal.componentDtos;
+      int componentCount = componentDtosAndTotal.total;
+      List<MetricDto> metrics = searchMetrics(dbSession, wsRequest);
+      List<WsMeasures.Period> periods = periodsFromSnapshot(baseSnapshot);
+      Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric = searchMeasuresByComponentUuidAndMetric(dbSession, components, metrics, periods);
+
+      components = sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
+      components = paginateComponents(components, componentCount, wsRequest);
+      Map<Long, String> referenceComponentUuidsById = searchReferenceComponentUuidsById(dbSession, components);
+
+      return ComponentTreeData.builder()
+        .setBaseComponent(baseComponent)
+        .setComponentsFromDb(components)
+        .setComponentCount(componentCount)
+        .setMeasuresByComponentUuidAndMetric(measuresByComponentUuidAndMetric)
+        .setMetrics(metrics)
+        .setPeriods(periods)
+        .setReferenceComponentUuidsById(referenceComponentUuidsById)
+        .build();
+    } finally {
+      dbClient.closeSession(dbSession);
+    }
+  }
+
+  private Map<Long, String> searchReferenceComponentUuidsById(DbSession dbSession, List<ComponentDtoWithSnapshotId> components) {
+    List<Long> referenceComponentIds = from(components)
+      .transform(ComponentDtoWithSnapshotIdToCopyResourceIdFunction.INSTANCE)
+      .filter(Predicates.<Long>notNull())
+      .toList();
+    if (referenceComponentIds.isEmpty()) {
+      return emptyMap();
+    }
+
+    List<ComponentDto> referenceComponents = dbClient.componentDao().selectByIds(dbSession, referenceComponentIds);
+    Map<Long, String> referenceComponentUuidsById = new HashMap<>();
+    for (ComponentDto referenceComponent : referenceComponents) {
+      referenceComponentUuidsById.put(referenceComponent.getId(), referenceComponent.uuid());
+    }
+
+    return referenceComponentUuidsById;
+  }
+
+  private ComponentDtosAndTotal searchComponents(DbSession dbSession, ComponentTreeQuery dbQuery, ComponentTreeWsRequest wsRequest) {
+    switch (wsRequest.getStrategy()) {
+      case CHILDREN_STRATEGY:
+        return new ComponentDtosAndTotal(
+          dbClient.componentDao().selectDirectChildren(dbSession, dbQuery),
+          dbClient.componentDao().countDirectChildren(dbSession, dbQuery));
+      case LEAVES_STRATEGY:
+      case ALL_STRATEGY:
+        return new ComponentDtosAndTotal(
+          dbClient.componentDao().selectAllChildren(dbSession, dbQuery),
+          dbClient.componentDao().countAllChildren(dbSession, dbQuery));
+      default:
+        throw new IllegalStateException("Unknown component tree strategy");
+    }
+  }
+
+  private List<MetricDto> searchMetrics(DbSession dbSession, ComponentTreeWsRequest request) {
+    List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, request.getMetricKeys());
+    if (metrics.size() < request.getMetricKeys().size()) {
+      List<String> foundMetricKeys = Lists.transform(metrics, new Function<MetricDto, String>() {
+        @Override
+        public String apply(@Nonnull MetricDto input) {
+          return input.getKey();
+        }
+      });
+      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 Table<String, MetricDto, MeasureDto> searchMeasuresByComponentUuidAndMetric(DbSession dbSession, List<ComponentDtoWithSnapshotId> components, List<MetricDto> metrics,
+    List<WsMeasures.Period> periods) {
+    Map<Long, ComponentDtoWithSnapshotId> componentsBySnapshotId = Maps.uniqueIndex(components, ComponentDtoWithSnapshotIdToSnapshotIdFunction.INSTANCE);
+
+    Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDtoFunctions.toId());
+    List<MeasureDto> measureDtos = dbClient.measureDao().selectBySnapshotIdsAndMetricIds(dbSession,
+      new ArrayList<>(componentsBySnapshotId.keySet()),
+      new ArrayList<>(metricsById.keySet()));
+
+    Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric = HashBasedTable.create(components.size(), metrics.size());
+    for (MeasureDto measureDto : measureDtos) {
+      measuresByComponentUuidAndMetric.put(
+        componentsBySnapshotId.get(measureDto.getSnapshotId()).uuid(),
+        metricsById.get(measureDto.getMetricId()),
+        measureDto);
+    }
+
+    addBestValuesToMeasures(measuresByComponentUuidAndMetric, components, metrics, periods);
+
+    return measuresByComponentUuidAndMetric;
+  }
+
+  /**
+   * 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(Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric, List<ComponentDtoWithSnapshotId> components,
+    List<MetricDto> metrics, List<WsMeasures.Period> periods) {
+    List<ComponentDtoWithSnapshotId> componentsEligibleForBestValue = from(components).filter(IsFileComponent.INSTANCE).toList();
+    List<MetricDtoWithBestValue> metricDtosWithBestValueMeasure = from(metrics)
+      .filter(IsMetricOptimizedForBestValue.INSTANCE)
+      .transform(new MetricDtoToMetricDtoWithBestValue(periods))
+      .toList();
+    if (metricDtosWithBestValueMeasure.isEmpty()) {
+      return;
+    }
+
+    for (ComponentDtoWithSnapshotId component : componentsEligibleForBestValue) {
+      for (MetricDtoWithBestValue metricWithBestValue : metricDtosWithBestValueMeasure) {
+        if (measuresByComponentUuidAndMetric.get(component.uuid(), metricWithBestValue.metric) == null) {
+          measuresByComponentUuidAndMetric.put(component.uuid(), metricWithBestValue.metric, metricWithBestValue.bestValue);
+        }
+      }
+    }
+  }
+
+  private static List<WsMeasures.Period> periodsFromSnapshot(SnapshotDto baseSnapshot) {
+    List<WsMeasures.Period> periods = new ArrayList<>();
+    for (int periodIndex = 1; periodIndex <= 5; periodIndex++) {
+      if (baseSnapshot.getPeriodDate(periodIndex) != null) {
+        periods.add(snapshotDtoToWsPeriod(baseSnapshot, 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();
+  }
+
+  private static List<ComponentDtoWithSnapshotId> sortComponents(List<ComponentDtoWithSnapshotId> components, ComponentTreeWsRequest wsRequest, List<MetricDto> metrics,
+    Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric) {
+    if (!wsRequest.getSort().contains(METRIC_SORT)) {
+      return components;
+    }
+
+    return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
+  }
+
+  private static List<ComponentDtoWithSnapshotId> paginateComponents(List<ComponentDtoWithSnapshotId> components, int componentCount, ComponentTreeWsRequest wsRequest) {
+    if (!wsRequest.getSort().contains(METRIC_SORT)) {
+      return components;
+    }
+
+    Paging paging = Paging.forPageIndex(wsRequest.getPage())
+      .withPageSize(wsRequest.getPageSize())
+      .andTotal(componentCount);
+
+    return from(components)
+      .skip(paging.offset())
+      .limit(paging.pageSize())
+      .toList();
+  }
+
+  @CheckForNull
+  private List<String> childrenQualifiers(ComponentTreeWsRequest request, String baseQualifier) {
+    List<String> requestQualifiers = request.getQualifiers();
+    List<String> childrenQualifiers = null;
+    if (LEAVES_STRATEGY.equals(request.getStrategy())) {
+      childrenQualifiers = resourceTypes.getLeavesQualifiers(baseQualifier);
+    }
+
+    if (requestQualifiers == null) {
+      return childrenQualifiers;
+    }
+
+    if (childrenQualifiers == null) {
+      return requestQualifiers;
+    }
+
+    // intersection of request and children qualifiers
+    childrenQualifiers.retainAll(requestQualifiers);
+
+    return childrenQualifiers;
+  }
+
+  private ComponentTreeQuery toComponentTreeQuery(ComponentTreeWsRequest wsRequest, SnapshotDto baseSnapshot) {
+    List<String> childrenQualifiers = childrenQualifiers(wsRequest, baseSnapshot.getQualifier());
+
+    List<String> sortsWithoutMetricSort = newArrayList(Iterables.filter(wsRequest.getSort(), IsNotMetricSort.INSTANCE));
+    sortsWithoutMetricSort = sortsWithoutMetricSort.isEmpty() ? singletonList(NAME_SORT) : sortsWithoutMetricSort;
+
+    ComponentTreeQuery.Builder dbQuery = ComponentTreeQuery.builder()
+      .setBaseSnapshot(baseSnapshot)
+      .setPage(wsRequest.getPage())
+      .setPageSize(wsRequest.getPageSize())
+      .setSortFields(sortsWithoutMetricSort)
+      .setAsc(wsRequest.getAsc());
+
+    if (wsRequest.getQuery() != null) {
+      dbQuery.setNameOrKeyQuery(wsRequest.getQuery());
+    }
+    if (childrenQualifiers != null) {
+      dbQuery.setQualifiers(childrenQualifiers);
+    }
+    // load all components if we must sort by metric value
+    if (wsRequest.getSort().contains(METRIC_SORT)) {
+      dbQuery.setPage(1);
+      dbQuery.setPageSize(Integer.MAX_VALUE);
+    }
+
+    return dbQuery.build();
+  }
+
+  private void checkPermissions(ComponentDto baseComponent) {
+    String projectUuid = firstNonNull(baseComponent.projectUuid(), baseComponent.uuid());
+    if (!userSession.hasGlobalPermission(GlobalPermissions.SYSTEM_ADMIN) &&
+      !userSession.hasProjectPermissionByUuid(UserRole.ADMIN, projectUuid) &&
+      !userSession.hasProjectPermissionByUuid(UserRole.USER, projectUuid)) {
+      throw insufficientPrivilegesException();
+    }
+  }
+
+  private static class ComponentDtosAndTotal {
+    private final List<ComponentDtoWithSnapshotId> componentDtos;
+    private final int total;
+
+    private ComponentDtosAndTotal(List<ComponentDtoWithSnapshotId> componentDtos, int total) {
+      this.componentDtos = componentDtos;
+      this.total = total;
+    }
+  }
+
+  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;
+
+    @Override
+    public boolean apply(@Nonnull ComponentDtoWithSnapshotId input) {
+      return QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE.contains(input.qualifier());
+    }
+  }
+
+  private static class MetricDtoToMetricDtoWithBestValue implements Function<MetricDto, MetricDtoWithBestValue> {
+    private final List<Integer> periodIndexes;
+
+    MetricDtoToMetricDtoWithBestValue(List<WsMeasures.Period> periods) {
+      this.periodIndexes = Lists.transform(periods, WsPeriodToIndex.INSTANCE);
+    }
+
+    @Override
+    public MetricDtoWithBestValue apply(@Nonnull MetricDto input) {
+      return new MetricDtoWithBestValue(input, periodIndexes);
+    }
+  }
+
+  private static class MetricDtoWithBestValue {
+    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())
+        .setValue(metric.getBestValue());
+      for (Integer periodIndex : periodIndexes) {
+        measure.setVariation(periodIndex, 0.0d);
+      }
+      this.bestValue = measure;
+    }
+  }
+
+  private enum WsPeriodToIndex implements Function<WsMeasures.Period, Integer> {
+    INSTANCE;
+
+    @Override
+    public Integer apply(@Nonnull WsMeasures.Period input) {
+      return input.getIndex();
+    }
+  }
+
+  private enum ComponentDtoWithSnapshotIdToSnapshotIdFunction implements Function<ComponentDtoWithSnapshotId, Long> {
+    INSTANCE;
+
+    @Override
+    public Long apply(@Nonnull ComponentDtoWithSnapshotId input) {
+      return input.getSnapshotId();
+    }
+  }
+
+  private enum IsNotMetricSort implements Predicate<String> {
+    INSTANCE;
+
+    @Override
+    public boolean apply(@Nonnull String input) {
+      return !input.equals(METRIC_SORT);
+    }
+  }
+
+  private enum ComponentDtoWithSnapshotIdToCopyResourceIdFunction implements Function<ComponentDtoWithSnapshotId, Long> {
+    INSTANCE;
+    @Override
+    public Long apply(@Nonnull ComponentDtoWithSnapshotId input) {
+      return input.getCopyResourceId();
+    }
+  }
+}
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
new file mode 100644 (file)
index 0000000..83f964f
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * SonarQube :: Server
+ * 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.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Table;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import org.sonar.db.component.ComponentDtoWithSnapshotId;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.metric.MetricDtoFunctions;
+import org.sonarqube.ws.client.measure.ComponentTreeWsRequest;
+
+import static java.lang.String.CASE_INSENSITIVE_ORDER;
+import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.NAME_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.PATH_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.QUALIFIER_SORT;
+
+class ComponentTreeSort {
+
+  private ComponentTreeSort() {
+    // static method only
+  }
+
+  static List<ComponentDtoWithSnapshotId> sortComponents(List<ComponentDtoWithSnapshotId> components, ComponentTreeWsRequest wsRequest, List<MetricDto> metrics,
+    Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric) {
+    boolean isAscending = wsRequest.getAsc();
+    Map<String, Ordering<ComponentDtoWithSnapshotId>> orderingsBySortField = ImmutableMap.<String, Ordering<ComponentDtoWithSnapshotId>>builder()
+      .put(NAME_SORT, componentNameOrdering(isAscending))
+      .put(QUALIFIER_SORT, componentQualifierOrdering(isAscending))
+      .put(PATH_SORT, componentPathOrdering(isAscending))
+      .put(METRIC_SORT, metricOrdering(wsRequest, metrics, measuresByComponentUuidAndMetric))
+      .build();
+
+    List<String> sortParameters = wsRequest.getSort();
+    String firstSortParameter = sortParameters.get(0);
+    Ordering<ComponentDtoWithSnapshotId> primaryOrdering = orderingsBySortField.get(firstSortParameter);
+    if (sortParameters.size() > 1) {
+      for (int i = 1; i < sortParameters.size(); i++) {
+        String secondarySortParameter = sortParameters.get(i);
+        Ordering<ComponentDtoWithSnapshotId> secondaryOrdering = orderingsBySortField.get(secondarySortParameter);
+        primaryOrdering = primaryOrdering.compound(secondaryOrdering);
+      }
+    }
+
+    return primaryOrdering.immutableSortedCopy(components);
+  }
+
+  private static Ordering<ComponentDtoWithSnapshotId> componentNameOrdering(boolean isAscending) {
+    return genericComponentFieldOrdering(isAscending, ComponentDtoWithSnapshotIdToName.INSTANCE);
+  }
+
+  private static Ordering<ComponentDtoWithSnapshotId> componentQualifierOrdering(boolean isAscending) {
+    return genericComponentFieldOrdering(isAscending, ComponentDtoWithSnapshotIdToQualifier.INSTANCE);
+  }
+
+  private static Ordering<ComponentDtoWithSnapshotId> componentPathOrdering(boolean isAscending) {
+    return genericComponentFieldOrdering(isAscending, ComponentDtoWithSnapshotIdToPath.INSTANCE);
+  }
+
+  private static Ordering<ComponentDtoWithSnapshotId> genericComponentFieldOrdering(boolean isAscending, Function<ComponentDtoWithSnapshotId, String> function) {
+    Ordering<String> ordering = Ordering.from(CASE_INSENSITIVE_ORDER)
+      .nullsLast();
+    if (!isAscending) {
+      ordering = ordering.reverse();
+    }
+
+    return ordering.onResultOf(function);
+  }
+
+  /**
+   * Order by measure value, taking the metric direction into account
+   * Metric direction is taken into account in {@link ComponentDtoWithSnapshotIdToMeasureValue}
+   */
+  private static Ordering<ComponentDtoWithSnapshotId> metricOrdering(ComponentTreeWsRequest wsRequest, List<MetricDto> metrics,
+    Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric) {
+    Ordering<Double> ordering = Ordering.natural()
+      .nullsLast();
+
+    if (!wsRequest.getAsc()) {
+      ordering = ordering.reverse();
+    }
+
+    return ordering.onResultOf(new ComponentDtoWithSnapshotIdToMeasureValue(wsRequest, metrics, measuresByComponentUuidAndMetric));
+  }
+
+  private static class ComponentDtoWithSnapshotIdToMeasureValue implements Function<ComponentDtoWithSnapshotId, Double> {
+    private final String metricKey;
+    private final Map<String, MetricDto> metricsByKey;
+    private final Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric;
+
+    private ComponentDtoWithSnapshotIdToMeasureValue(ComponentTreeWsRequest wsRequest, List<MetricDto> metrics,
+      Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric) {
+      this.metricKey = wsRequest.getMetricSort();
+      this.metricsByKey = Maps.uniqueIndex(metrics, MetricDtoFunctions.toKey());
+      this.measuresByComponentUuidAndMetric = measuresByComponentUuidAndMetric;
+    }
+
+    @Override
+    public Double apply(@Nonnull ComponentDtoWithSnapshotId input) {
+      MetricDto metric = metricsByKey.get(metricKey);
+      MeasureDto measure = measuresByComponentUuidAndMetric.get(input.uuid(), metric);
+      if (measure == null || measure.getValue() == null) {
+        return null;
+      }
+
+      return metric.getDirection() >= 0 ? measure.getValue() : -measure.getValue();
+    }
+  }
+
+  private enum ComponentDtoWithSnapshotIdToName implements Function<ComponentDtoWithSnapshotId, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull ComponentDtoWithSnapshotId input) {
+      return input.name();
+    }
+  }
+
+  private enum ComponentDtoWithSnapshotIdToQualifier implements Function<ComponentDtoWithSnapshotId, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull ComponentDtoWithSnapshotId input) {
+      return input.qualifier();
+    }
+  }
+
+  private enum ComponentDtoWithSnapshotIdToPath implements Function<ComponentDtoWithSnapshotId, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull ComponentDtoWithSnapshotId input) {
+      return input.path();
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java
new file mode 100644 (file)
index 0000000..3aaf476
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube :: Server
+ * 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.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonarqube.ws.WsMeasures;
+
+import static org.sonar.server.measure.ws.MeasureValueFormatter.formatDoubleValue;
+import static org.sonar.server.measure.ws.MeasureValueFormatter.formatMeasureValue;
+
+class MeasureDtoToWsMeasure {
+
+  private MeasureDtoToWsMeasure() {
+    // static methods
+  }
+
+  static WsMeasures.Measure measureDtoToWsMeasure(MetricDto metricDto, MeasureDto measureDto) {
+    WsMeasures.Measure.Builder measure = WsMeasures.Measure.newBuilder();
+    measure.setMetric(metricDto.getKey());
+    // a measure value can be null, new_violations metric for example
+    if (measureDto.getValue() != null
+      || measureDto.getData()!=null) {
+      measure.setValue(formatMeasureValue(measureDto, metricDto));
+    }
+    if (measureDto.getVariation(1) != null) {
+      measure.setVariationValueP1(formatDoubleValue(measureDto.getVariation(1), metricDto));
+    }
+    if (measureDto.getVariation(2) != null) {
+      measure.setVariationValueP2(formatDoubleValue(measureDto.getVariation(2), metricDto));
+    }
+    if (measureDto.getVariation(3) != null) {
+      measure.setVariationValueP3(formatDoubleValue(measureDto.getVariation(3), metricDto));
+    }
+    if (measureDto.getVariation(4) != null) {
+      measure.setVariationValueP4(formatDoubleValue(measureDto.getVariation(4), metricDto));
+    }
+    if (measureDto.getVariation(5) != null) {
+      measure.setVariationValueP5(formatDoubleValue(measureDto.getVariation(5), metricDto));
+    }
+
+    return measure.build();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureValueFormatter.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureValueFormatter.java
new file mode 100644 (file)
index 0000000..484a787
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube :: Server
+ * 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.measures.Metric;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+
+class MeasureValueFormatter {
+  private static final double DELTA = 0.000001d;
+
+  private MeasureValueFormatter() {
+    // static methods
+  }
+
+  static String formatMeasureValue(MeasureDto measure, MetricDto metric) {
+    Metric.ValueType metricType = Metric.ValueType.valueOf(metric.getValueType());
+    Double doubleValue = measure.getValue();
+    String stringValue = measure.getData();
+
+    switch (metricType) {
+      case BOOL:
+        return formatBoolean(doubleValue);
+      case INT:
+      case MILLISEC:
+        return formatInteger(doubleValue);
+      case WORK_DUR:
+        return formatLong(doubleValue);
+      case FLOAT:
+      case PERCENT:
+      case RATING:
+        return String.valueOf(doubleValue);
+      case LEVEL:
+      case STRING:
+      case DATA:
+      case DISTRIB:
+        return stringValue;
+      default:
+        throw new IllegalArgumentException("Unsupported metric type: " + metricType.name());
+    }
+  }
+
+  static String formatDoubleValue(Double value, MetricDto metric) {
+    Metric.ValueType metricType = Metric.ValueType.valueOf(metric.getValueType());
+
+    switch (metricType) {
+      case BOOL:
+        return formatBoolean(value);
+      case INT:
+      case MILLISEC:
+        return formatInteger(value);
+      case WORK_DUR:
+        return formatLong(value);
+      case FLOAT:
+      case PERCENT:
+      case RATING:
+        return String.valueOf(value);
+      case LEVEL:
+      case STRING:
+      case DATA:
+      case DISTRIB:
+      default:
+        throw new IllegalArgumentException(String.format("Unsupported metric type '%s' for numerical value", metricType.name()));
+    }
+  }
+
+  private static String formatBoolean(Double value) {
+    return Math.abs(value - 1.0d) < DELTA ? "true" : "false";
+  }
+
+  private static String formatInteger(Double value) {
+    return String.valueOf(value.intValue());
+  }
+
+  private static String formatLong(Double value) {
+    return String.valueOf(value.longValue());
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWs.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWs.java
new file mode 100644 (file)
index 0000000..b7b948b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube :: Server
+ * 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;
+import org.sonarqube.ws.client.measure.MeasuresWsParameters;
+
+public class MeasuresWs implements WebService {
+  private final MeasuresWsAction[] actions;
+
+  public MeasuresWs(MeasuresWsAction... actions) {
+    this.actions = actions;
+  }
+
+  @Override
+  public void define(Context context) {
+    NewController controller = context.createController(MeasuresWsParameters.CONTROLLER_MEASURES)
+      .setSince("5.4")
+      .setDescription("Measures search");
+
+    for (MeasuresWsAction action : actions) {
+      action.define(controller);
+    }
+
+    controller.done();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsAction.java
new file mode 100644 (file)
index 0000000..5aaafdc
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube :: Server
+ * 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.server.ws.WsAction;
+
+public interface MeasuresWsAction extends WsAction {
+  // marker interface
+}
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
new file mode 100644 (file)
index 0000000..1fd593b
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube :: Server
+ * 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.core.platform.Module;
+
+public class MeasuresWsModule extends Module {
+  @Override
+  protected void configureModule() {
+    add(
+      ComponentTreeDataLoader.class,
+      MeasuresWs.class,
+      ComponentTreeAction.class
+    );
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoToWsMetric.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoToWsMetric.java
new file mode 100644 (file)
index 0000000..6932335
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube :: Server
+ * 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.db.metric.MetricDto;
+import org.sonarqube.ws.Common.Metric;
+
+import static org.sonar.server.measure.ws.MeasureValueFormatter.formatDoubleValue;
+
+class MetricDtoToWsMetric {
+  private MetricDtoToWsMetric() {
+    // static methods only
+  }
+
+  static Metric metricDtoToWsMetric(MetricDto metricDto) {
+    Metric.Builder metric = Metric.newBuilder();
+    metric.setKey(metricDto.getKey());
+    metric.setType(metricDto.getValueType());
+    metric.setName(metricDto.getShortName());
+    if (metricDto.getDescription() != null) {
+      metric.setDescription(metricDto.getDescription());
+    }
+    metric.setDomain(metricDto.getDomain());
+    if (metricDto.getDirection() != 0) {
+      metric.setHigherValuesAreBetter(metricDto.getDirection() > 0);
+    }
+    metric.setQualitative(metricDto.isQualitative());
+    metric.setHidden(metricDto.isHidden());
+    metric.setCustom(metricDto.isUserManaged());
+    if (metricDto.getDecimalScale() != null) {
+      metric.setDecimalScale(metricDto.getDecimalScale());
+    }
+    if (metricDto.getBestValue() != null) {
+      metric.setBestValue(formatDoubleValue(metricDto.getBestValue(), metricDto));
+    }
+    if (metricDto.getWorstValue() != null) {
+      metric.setWorstValue(formatDoubleValue(metricDto.getWorstValue(), metricDto));
+    }
+
+    return metric.build();
+  }
+}
index 1c2381c565053fb807567b72f990f64685ee9e32..5313719a43410edd19902598a84ba7388e12ce63 100644 (file)
@@ -152,6 +152,7 @@ import org.sonar.server.measure.MeasureFilterFactory;
 import org.sonar.server.measure.custom.ws.CustomMeasuresWsModule;
 import org.sonar.server.measure.template.MyFavouritesFilter;
 import org.sonar.server.measure.template.ProjectFilter;
+import org.sonar.server.measure.ws.MeasuresWsModule;
 import org.sonar.server.measure.ws.TimeMachineWs;
 import org.sonar.server.metric.CoreCustomMetrics;
 import org.sonar.server.metric.DefaultMetricFinder;
@@ -479,6 +480,7 @@ public class PlatformLevel4 extends PlatformLevel {
       MeasureFilterExecutor.class,
       MeasureFilterEngine.class,
       MetricsWsModule.class,
+      MeasuresWsModule.class,
       CustomMeasuresWsModule.class,
       ProjectFilter.class,
       MyFavouritesFilter.class,
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json
new file mode 100644 (file)
index 0000000..1db85e0
--- /dev/null
@@ -0,0 +1,134 @@
+{
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 3
+  },
+  "baseComponent": {
+    "id": "project-id",
+    "key": "MY_PROJECT",
+    "name": "My Project",
+    "qualifier": "TRK"
+  },
+  "components": [
+    {
+      "id": "AVIwDXE-bJbJqrw6wFv5",
+      "key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/ElementImpl.java",
+      "name": "ElementImpl.java",
+      "qualifier": "FIL",
+      "path": "src/main/java/com/sonarsource/markdown/impl/ElementImpl.java",
+      "measures": [
+        {
+          "metric": "new_violations",
+          "variationValueP1": "25",
+          "variationValueP2": "0",
+          "variationValueP3": "25"
+        },
+        {
+          "metric": "ncloc",
+          "value": "114"
+        },
+        {
+          "metric": "complexity",
+          "value": "12"
+        }
+      ]
+    },
+    {
+      "id": "AVIwDXE_bJbJqrw6wFwJ",
+      "key": "com.sonarsource:java-markdown:src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java",
+      "name": "ElementImplTest.java",
+      "qualifier": "UTS",
+      "path": "src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java",
+      "measures": [
+        {
+          "metric": "new_violations",
+          "value": "0",
+          "variationValueP1": "0",
+          "variationValueP2": "0",
+          "variationValueP3": "0"
+        }
+      ]
+    },
+    {
+      "id": "AVIwDXE-bJbJqrw6wFv8",
+      "key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl",
+      "name": "src/main/java/com/sonarsource/markdown/impl",
+      "qualifier": "DIR",
+      "path": "src/main/java/com/sonarsource/markdown/impl",
+      "measures": [
+        {
+          "metric": "ncloc",
+          "value": "217",
+          "variationValueP2": "0"
+        },
+        {
+          "metric": "new_violations",
+          "variationValueP1": "25",
+          "variationValueP2": "0",
+          "variationValueP3": "25"
+        },
+        {
+          "metric": "complexity",
+          "value": "35",
+          "variationValueP2": "0"
+        }
+      ]
+    }
+  ],
+  "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/component/ComponentFinderTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentFinderTest.java
new file mode 100644 (file)
index 0000000..ad6982f
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * SonarQube :: Server
+ * 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.component;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+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.server.exceptions.NotFoundException;
+import org.sonar.test.DbTests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.server.component.ComponentFinder.ParamNames.ID_AND_KEY;
+
+@Category(DbTests.class)
+public class ComponentFinderTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+  ComponentDbTester componentDb = new ComponentDbTester(db);
+  DbSession dbSession = db.getSession();
+
+  ComponentFinder underTest = new ComponentFinder(db.getDbClient());
+
+  @Test
+  public void fail_when_the_uuid_and_key_are_null() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Either 'id' or 'key' must be provided, not both");
+
+    underTest.getByUuidOrKey(dbSession, null, null, ID_AND_KEY);
+  }
+
+  @Test
+  public void fail_when_the_uuid_and_key_are_provided() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Either 'id' or 'key' must be provided, not both");
+
+    underTest.getByUuidOrKey(dbSession, "project-uuid", "project-key", ID_AND_KEY);
+  }
+
+  @Test
+  public void fail_when_the_uuid_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The 'id' parameter must not be empty");
+
+    underTest.getByUuidOrKey(dbSession, "", null, ID_AND_KEY);
+  }
+
+  @Test
+  public void fail_when_the_key_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The 'key' parameter must not be empty");
+
+    underTest.getByUuidOrKey(dbSession, null, "", ID_AND_KEY);
+  }
+
+  @Test
+  public void fail_when_component_uuid_not_found() {
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Component id 'project-uuid' not found");
+
+    underTest.getByUuidOrKey(dbSession, "project-uuid", null, ID_AND_KEY);
+  }
+
+  @Test
+  public void fail_when_component_key_not_found() {
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Component key 'project-key' not found");
+
+    underTest.getByUuidOrKey(dbSession, null, "project-key", ID_AND_KEY);
+  }
+
+  @Test
+  public void get_component_by_uuid() {
+    componentDb.insertComponent(newProjectDto("project-uuid"));
+
+    ComponentDto component = underTest.getByUuidOrKey(dbSession, "project-uuid", null, ID_AND_KEY);
+
+    assertThat(component.uuid()).isEqualTo("project-uuid");
+  }
+
+  @Test
+  public void get_component_by_key() {
+    componentDb.insertComponent(newProjectDto().setKey("project-key"));
+
+    ComponentDto component = underTest.getByUuidOrKey(dbSession, null, "project-key", ID_AND_KEY);
+
+    assertThat(component.key()).isEqualTo("project-key");
+  }
+}
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
new file mode 100644 (file)
index 0000000..ce48509
--- /dev/null
@@ -0,0 +1,497 @@
+/*
+ * SonarQube :: Server
+ * 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 java.util.List;
+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.measures.Metric.ValueType;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.server.ws.WebService.Param;
+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.ResourceTypesRule;
+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.i18n.I18nRule;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+import org.sonar.test.DbTests;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.ComponentTreeWsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.utils.DateUtils.parseDateTime;
+import static org.sonar.db.component.ComponentTesting.newDevProjectCopy;
+import static org.sonar.db.component.ComponentTesting.newDeveloper;
+import static org.sonar.db.component.ComponentTesting.newDirectory;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+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.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;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+
+@Category(DbTests.class)
+public class ComponentTreeActionTest {
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  I18nRule i18n = new I18nRule();
+  ResourceTypesRule resourceTypes = new ResourceTypesRule();
+
+  @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 ComponentTreeAction(
+      new ComponentTreeDataLoader(dbClient, new ComponentFinder(dbClient), userSession, resourceTypes),
+      userSession, i18n, resourceTypes));
+
+  @Before
+  public void setUp() {
+    userSession.setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN);
+  }
+
+  @Test
+  public void json_example() {
+    insertJsonExampleData();
+
+    String response = ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-id")
+      .setParam(PARAM_METRIC_KEYS, "ncloc, complexity, new_violations")
+      .setParam(PARAM_ADDITIONAL_FIELDS, "metrics,periods")
+      .execute()
+      .getInput();
+
+    assertJson(response).isSimilarTo(getClass().getResource("component_tree-example.json"));
+  }
+
+  @Test
+  public void empty_response() {
+    componentDb.insertComponent(newProjectDto("project-uuid"));
+
+    ComponentTreeWsResponse response = call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc, complexity"));
+
+    assertThat(response.getBaseComponent().getId()).isEqualTo("project-uuid");
+    assertThat(response.getComponentsList()).isEmpty();
+    assertThat(response.getMetrics().getMetricsList()).isEmpty();
+    assertThat(response.getPeriods().getPeriodsList()).isEmpty();
+  }
+
+  @Test
+  public void load_measures_and_periods() {
+    ComponentDto projectDto = newProjectDto("project-uuid");
+    componentDb.insertComponent(projectDto);
+    SnapshotDto projectSnapshot = dbClient.snapshotDao().insert(dbSession,
+      newSnapshotForProject(projectDto)
+        .setPeriodDate(1, System.currentTimeMillis())
+        .setPeriodMode(1, "last_version")
+        .setPeriodDate(3, System.currentTimeMillis())
+        .setPeriodMode(3, "last_analysis"));
+    userSession.anonymous().addProjectUuidPermissions(UserRole.ADMIN, "project-uuid");
+    ComponentDto directoryDto = newDirectory(projectDto, "directory-uuid", "path/to/directory").setName("directory-1");
+    SnapshotDto directorySnapshot = componentDb.insertComponentAndSnapshot(directoryDto, projectSnapshot);
+    SnapshotDto fileSnapshot = componentDb.insertComponentAndSnapshot(newFileDto(directoryDto, "file-uuid").setName("file-1"), directorySnapshot);
+    MetricDto ncloc = insertNclocMetric();
+    MetricDto coverage = insertCoverageMetric();
+    dbClient.measureDao().insert(dbSession,
+      newMeasureDto(ncloc, fileSnapshot.getId()).setValue(5.0d).setVariation(1, 4.0d),
+      newMeasureDto(coverage, fileSnapshot.getId()).setValue(15.5d).setVariation(3, 2.0d),
+      newMeasureDto(coverage, directorySnapshot.getId()).setValue(15.0d));
+    db.commit();
+
+    ComponentTreeWsResponse response = call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc,coverage")
+      .setParam(PARAM_ADDITIONAL_FIELDS, ADDITIONAL_PERIODS));
+
+    assertThat(response.getComponentsList().get(0).getMeasures().getMeasuresList()).extracting("metric").containsOnly("coverage");
+    // file measures
+    List<WsMeasures.Measure> fileMeasures = response.getComponentsList().get(1).getMeasures().getMeasuresList();
+    assertThat(fileMeasures).extracting("metric").containsOnly("ncloc", "coverage");
+    assertThat(fileMeasures).extracting("value").containsOnly("5", "15.5");
+    assertThat(fileMeasures).extracting("variationValueP1").containsOnly("", "4");
+    assertThat(fileMeasures).extracting("variationValueP3").containsOnly("", "2.0");
+    assertThat(response.getPeriods().getPeriodsList()).extracting("mode").containsOnly("last_version", "last_analysis");
+  }
+
+  @Test
+  public void load_measures_with_best_value() {
+    ComponentDto projectDto = newProjectDto("project-uuid");
+    SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(projectDto);
+    userSession.anonymous().addProjectUuidPermissions(UserRole.ADMIN, "project-uuid");
+    ComponentDto directoryDto = newDirectory(projectDto, "directory-uuid", "path/to/directory").setName("directory-1");
+    SnapshotDto directorySnapshot = componentDb.insertComponentAndSnapshot(directoryDto, projectSnapshot);
+    SnapshotDto fileSnapshot = componentDb.insertComponentAndSnapshot(newFileDto(directoryDto, "file-uuid").setName("file-1"), directorySnapshot);
+    MetricDto ncloc = newMetricDto()
+      .setKey("ncloc")
+      .setValueType(ValueType.INT.name())
+      .setOptimizedBestValue(true)
+      .setBestValue(100d)
+      .setWorstValue(1_000d);
+    dbClient.metricDao().insert(dbSession, ncloc);
+    MetricDto coverage = insertCoverageMetric();
+    dbClient.measureDao().insert(dbSession,
+      newMeasureDto(coverage, fileSnapshot.getId()).setValue(15.5d),
+      newMeasureDto(coverage, directorySnapshot.getId()).setValue(42.0d));
+    db.commit();
+
+    ComponentTreeWsResponse response = call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc,coverage")
+      .setParam(PARAM_ADDITIONAL_FIELDS, "metrics"));
+
+    // directory measures
+    assertThat(response.getComponentsList().get(0).getMeasures().getMeasuresList()).extracting("metric").containsOnly("coverage");
+    // file measures
+    assertThat(response.getComponentsList().get(1).getMeasures().getMeasuresList()).extracting("metric").containsOnly("ncloc", "coverage");
+    assertThat(response.getComponentsList().get(1).getMeasures().getMeasuresList()).extracting("value").containsOnly("100", "15.5");
+
+    List<Common.Metric> metrics = response.getMetrics().getMetricsList();
+    assertThat(metrics).extracting("bestValue").contains("100");
+    assertThat(metrics).extracting("worstValue").contains("1000");
+  }
+
+  @Test
+  public void load_measures_multi_sort_with_metric_key_and_paginated() {
+    ComponentDto projectDto = newProjectDto("project-uuid");
+    SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(projectDto);
+    SnapshotDto fileSnapshot9 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-9").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot8 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-8").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot7 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-7").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot6 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-6").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot5 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-5").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot4 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-4").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot3 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-3").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot2 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-2").setName("file-1"), projectSnapshot);
+    SnapshotDto fileSnapshot1 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-1").setName("file-1"), projectSnapshot);
+    MetricDto coverage = insertCoverageMetric();
+    dbClient.measureDao().insert(dbSession,
+      newMeasureDto(coverage, fileSnapshot1.getId()).setValue(1.0d),
+      newMeasureDto(coverage, fileSnapshot2.getId()).setValue(2.0d),
+      newMeasureDto(coverage, fileSnapshot3.getId()).setValue(3.0d),
+      newMeasureDto(coverage, fileSnapshot4.getId()).setValue(4.0d),
+      newMeasureDto(coverage, fileSnapshot5.getId()).setValue(5.0d),
+      newMeasureDto(coverage, fileSnapshot6.getId()).setValue(6.0d),
+      newMeasureDto(coverage, fileSnapshot7.getId()).setValue(7.0d),
+      newMeasureDto(coverage, fileSnapshot8.getId()).setValue(8.0d),
+      newMeasureDto(coverage, fileSnapshot9.getId()).setValue(9.0d));
+    db.commit();
+
+    ComponentTreeWsResponse response = call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(Param.SORT, NAME_SORT + ", " + METRIC_SORT)
+      .setParam(PARAM_METRIC_SORT, "coverage")
+      .setParam(PARAM_METRIC_KEYS, "coverage")
+      .setParam(Param.PAGE, "2")
+      .setParam(Param.PAGE_SIZE, "3"));
+
+    assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-4", "file-uuid-5", "file-uuid-6");
+  }
+
+  @Test
+  public void sort_by_metric_key() {
+    ComponentDto projectDto = newProjectDto("project-uuid");
+    SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(projectDto);
+    SnapshotDto fileSnapshot3 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-3"), projectSnapshot);
+    SnapshotDto fileSnapshot1 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-1"), projectSnapshot);
+    SnapshotDto fileSnapshot2 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-2"), projectSnapshot);
+    MetricDto ncloc = newMetricDtoWithoutOptimization().setKey("ncloc").setValueType(ValueType.INT.name()).setDirection(1);
+    dbClient.metricDao().insert(dbSession, ncloc);
+    dbClient.measureDao().insert(dbSession,
+      newMeasureDto(ncloc, fileSnapshot1.getId()).setValue(1.0d),
+      newMeasureDto(ncloc, fileSnapshot2.getId()).setValue(2.0d),
+      newMeasureDto(ncloc, fileSnapshot3.getId()).setValue(3.0d));
+    db.commit();
+
+    ComponentTreeWsResponse response = call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(Param.SORT, METRIC_SORT)
+      .setParam(PARAM_METRIC_SORT, "ncloc")
+      .setParam(PARAM_METRIC_KEYS, "ncloc"));
+
+    assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-1", "file-uuid-2", "file-uuid-3");
+  }
+
+  @Test
+  public void load_developer_descendants() {
+    ComponentDto developer = newDeveloper("developer").setUuid("developer-uuid");
+    ComponentDto project = newProjectDto("project-uuid");
+    SnapshotDto developerSnapshot = componentDb.insertDeveloperAndSnapshot(developer);
+    componentDb.insertProjectAndSnapshot(project);
+    componentDb.insertComponentAndSnapshot(newDevProjectCopy("project-uuid-copy", project, developer), developerSnapshot);
+    insertNclocMetric();
+
+    ComponentTreeWsResponse response = call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "developer-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc"));
+
+    assertThat(response.getComponentsCount()).isEqualTo(1);
+    WsMeasures.Component projectCopy = response.getComponents(0);
+    assertThat(projectCopy.getId()).isEqualTo("project-uuid-copy");
+    assertThat(projectCopy.getRefId()).isEqualTo("project-uuid");
+  }
+
+  @Test
+  public void fail_when_metric_keys_parameter_is_empty() {
+    componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid"));
+
+    expectedException.expect(BadRequestException.class);
+    expectedException.expectMessage("The 'metricKeys' parameter must contain at least one metric key");
+
+    call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, ""));
+  }
+
+  @Test
+  public void fail_when_a_metric_is_not_found() {
+    componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid"));
+    insertNclocMetric();
+    insertNewViolationsMetric();
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("The following metric keys are not found: unknown-metric, another-unknown-metric");
+
+    call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc, new_violations, unknown-metric, another-unknown-metric"));
+  }
+
+  @Test
+  public void fail_when_insufficient_privileges() {
+    userSession.anonymous().setGlobalPermissions(GlobalPermissions.QUALITY_PROFILE_ADMIN);
+    componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid"));
+    expectedException.expect(ForbiddenException.class);
+
+    call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc"));
+  }
+
+  @Test
+  public void fail_when_sort_by_metric_and_no_metric_sort_provided() {
+    componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid"));
+    expectedException.expect(BadRequestException.class);
+    expectedException.expectMessage("To sort by a metric, the 's' parameter must contain 'metric' and a metric key must be provided in the 'metricSort' parameter");
+
+    call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc")
+      // PARAM_METRIC_SORT is not set
+      .setParam(Param.SORT, METRIC_SORT));
+  }
+
+  @Test
+  public void fail_when_sort_by_metric_and_not_in_the_list_of_metric_keys() {
+    componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid"));
+    expectedException.expect(BadRequestException.class);
+    expectedException.expectMessage("To sort by the 'complexity' metric, it must be in the list of metric keys in the 'metricKeys' parameter");
+
+    call(ws.newRequest()
+      .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+      .setParam(PARAM_METRIC_KEYS, "ncloc,violations")
+      .setParam(PARAM_METRIC_SORT, "complexity")
+      .setParam(Param.SORT, METRIC_SORT));
+  }
+
+  private static ComponentTreeWsResponse call(TestRequest request) {
+    TestResponse testResponse = request
+      .setMediaType(MediaTypes.PROTOBUF)
+      .execute();
+
+    try (InputStream responseStream = testResponse.getInputStream()) {
+      return ComponentTreeWsResponse.parseFrom(responseStream);
+    } catch (IOException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+
+  private static MetricDto newMetricDtoWithoutOptimization() {
+    return newMetricDto()
+      .setWorstValue(null)
+      .setBestValue(null)
+      .setOptimizedBestValue(false)
+      .setUserManaged(false);
+  }
+
+  private void insertJsonExampleData() {
+    ComponentDto project = newProjectDto("project-id")
+      .setKey("MY_PROJECT")
+      .setName("My Project")
+      .setQualifier(Qualifiers.PROJECT);
+    componentDb.insertComponent(project);
+    SnapshotDto projectSnapshot = dbClient.snapshotDao().insert(dbSession, newSnapshotForProject(project)
+      .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"));
+
+    SnapshotDto file1Snapshot = componentDb.insertComponentAndSnapshot(newFileDto(project)
+      .setUuid("AVIwDXE-bJbJqrw6wFv5")
+      .setKey("com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/ElementImpl.java")
+      .setName("ElementImpl.java")
+      .setQualifier(Qualifiers.FILE)
+      .setPath("src/main/java/com/sonarsource/markdown/impl/ElementImpl.java"), projectSnapshot);
+    SnapshotDto file2Snapshot = componentDb.insertComponentAndSnapshot(newFileDto(project)
+      .setUuid("AVIwDXE_bJbJqrw6wFwJ")
+      .setKey("com.sonarsource:java-markdown:src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java")
+      .setName("ElementImplTest.java")
+      .setQualifier(Qualifiers.UNIT_TEST_FILE)
+      .setPath("src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java"), projectSnapshot);
+    SnapshotDto directorySnapshot = componentDb.insertComponentAndSnapshot(newDirectory(project, "src/main/java/com/sonarsource/markdown/impl")
+      .setUuid("AVIwDXE-bJbJqrw6wFv8")
+      .setKey("com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl")
+      .setQualifier(Qualifiers.DIRECTORY), projectSnapshot);
+
+    MetricDto complexity = insertComplexityMetric();
+    dbClient.measureDao().insert(dbSession,
+      newMeasureDto(complexity, file1Snapshot.getId())
+        .setValue(12.0d),
+      newMeasureDto(complexity, directorySnapshot.getId())
+        .setValue(35.0d)
+        .setVariation(2, 0.0d));
+
+    MetricDto ncloc = insertNclocMetric();
+    dbClient.measureDao().insert(dbSession,
+      newMeasureDto(ncloc, file1Snapshot.getId())
+        .setValue(114.0d),
+      newMeasureDto(ncloc, directorySnapshot.getId())
+        .setValue(217.0d)
+        .setVariation(2, 0.0d));
+
+    MetricDto newViolations = insertNewViolationsMetric();
+    dbClient.measureDao().insert(dbSession,
+      newMeasureDto(newViolations, file1Snapshot.getId())
+        .setVariation(1, 25.0d)
+        .setVariation(2, 0.0d)
+        .setVariation(3, 25.0d),
+      newMeasureDto(newViolations, file2Snapshot.getId())
+        .setValue(0.0d)
+        .setVariation(1, 0.0d)
+        .setVariation(2, 0.0d)
+        .setVariation(3, 0.0d),
+      newMeasureDto(newViolations, directorySnapshot.getId())
+        .setVariation(1, 25.0d)
+        .setVariation(2, 0.0d)
+        .setVariation(3, 25.0d));
+
+    db.commit();
+  }
+
+  private MetricDto insertNewViolationsMetric() {
+    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 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 insertCoverageMetric() {
+    MetricDto metric = dbClient.metricDao().insert(dbSession, newMetricDtoWithoutOptimization()
+      .setKey("coverage")
+      .setShortName("Coverage")
+      .setDescription("Code Coverage")
+      .setDomain("Coverage")
+      .setValueType(ValueType.FLOAT.name())
+      .setDirection(1)
+      .setQualitative(false)
+      .setHidden(false)
+      .setUserManaged(false));
+    db.commit();
+    return metric;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java
new file mode 100644 (file)
index 0000000..f4903c0
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * SonarQube :: Server
+ * 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.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.component.ComponentDtoWithSnapshotId;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonarqube.ws.client.measure.ComponentTreeWsRequest;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.metric.MetricTesting.newMetricDto;
+import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.NAME_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.PATH_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.QUALIFIER_SORT;
+
+public class ComponentTreeSortTest {
+  private static final String METRIC_KEY = "violations";
+
+  private List<MetricDto> metrics;
+  private Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric;
+  private List<ComponentDtoWithSnapshotId> components;
+
+  @Before
+  public void setUp() {
+    components = newArrayList(
+      newComponentWithoutSnapshotId("name-1", "qualifier-2", "path-9"),
+      newComponentWithoutSnapshotId("name-3", "qualifier-3", "path-8"),
+      newComponentWithoutSnapshotId("name-2", "qualifier-4", "path-7"),
+      newComponentWithoutSnapshotId("name-4", "qualifier-5", "path-6"),
+      newComponentWithoutSnapshotId("name-7", "qualifier-6", "path-5"),
+      newComponentWithoutSnapshotId("name-6", "qualifier-7", "path-4"),
+      newComponentWithoutSnapshotId("name-5", "qualifier-8", "path-3"),
+      newComponentWithoutSnapshotId("name-9", "qualifier-9", "path-2"),
+      newComponentWithoutSnapshotId("name-8", "qualifier-1", "path-1"));
+
+    MetricDto metric = newMetricDto().setKey(METRIC_KEY).setDirection(1);
+    metrics = newArrayList(metric);
+
+    measuresByComponentUuidAndMetric = HashBasedTable.create(components.size(), 1);
+    // same number than path field
+    double currentValue = 9;
+    for (ComponentDtoWithSnapshotId component : components) {
+      measuresByComponentUuidAndMetric.put(component.uuid(), metric, new MeasureDto().setValue(currentValue));
+      currentValue--;
+    }
+  }
+
+  @Test
+  public void sort_by_names() {
+    ComponentTreeWsRequest wsRequest = newRequest(singletonList(NAME_SORT), true, null);
+    List<ComponentDtoWithSnapshotId> result = sortComponents(wsRequest);
+
+    assertThat(result).extracting("name")
+      .containsExactly("name-1", "name-2", "name-3", "name-4", "name-5", "name-6", "name-7", "name-8", "name-9");
+  }
+
+  @Test
+  public void sort_by_qualifier() {
+    ComponentTreeWsRequest wsRequest = newRequest(singletonList(QUALIFIER_SORT), false, null);
+
+    List<ComponentDtoWithSnapshotId> result = sortComponents(wsRequest);
+
+    assertThat(result).extracting("qualifier")
+      .containsExactly("qualifier-9", "qualifier-8", "qualifier-7", "qualifier-6", "qualifier-5", "qualifier-4", "qualifier-3", "qualifier-2", "qualifier-1");
+  }
+
+  @Test
+  public void sort_by_path() {
+    ComponentTreeWsRequest wsRequest = newRequest(singletonList(PATH_SORT), true, null);
+
+    List<ComponentDtoWithSnapshotId> result = sortComponents(wsRequest);
+
+    assertThat(result).extracting("path")
+      .containsExactly("path-1", "path-2", "path-3", "path-4", "path-5", "path-6", "path-7", "path-8", "path-9");
+  }
+
+  @Test
+  public void sort_by_metric_key() {
+    ComponentTreeWsRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, METRIC_KEY);
+
+    List<ComponentDtoWithSnapshotId> result = sortComponents(wsRequest);
+
+    assertThat(result).extracting("path")
+      .containsExactly("path-1", "path-2", "path-3", "path-4", "path-5", "path-6", "path-7", "path-8", "path-9");
+  }
+
+  @Test
+  public void sort_by_metric_key_with_negative_metric_direction() {
+    metrics.get(0).setDirection(-1);
+    ComponentTreeWsRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, METRIC_KEY);
+
+    List<ComponentDtoWithSnapshotId> result = sortComponents(wsRequest);
+
+    assertThat(result).extracting("path")
+      .containsExactly("path-9", "path-8", "path-7", "path-6", "path-5", "path-4", "path-3", "path-2", "path-1");
+  }
+
+  @Test
+  public void sort_on_multiple_fields() {
+    components = newArrayList(
+      newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-2"),
+      newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-3"),
+      newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-1"));
+    ComponentTreeWsRequest wsRequest = newRequest(newArrayList(NAME_SORT, QUALIFIER_SORT, PATH_SORT), true, null);
+
+    List<ComponentDtoWithSnapshotId> result = sortComponents(wsRequest);
+
+    assertThat(result).extracting("path")
+      .containsExactly("path-1", "path-2", "path-3");
+  }
+
+  private List<ComponentDtoWithSnapshotId> sortComponents(ComponentTreeWsRequest wsRequest) {
+    return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
+  }
+
+  private static ComponentDtoWithSnapshotId newComponentWithoutSnapshotId(String name, String qualifier, String path) {
+    return (ComponentDtoWithSnapshotId) new ComponentDtoWithSnapshotId()
+      .setUuid(Uuids.createFast())
+      .setName(name)
+      .setQualifier(qualifier)
+      .setPath(path);
+  }
+
+  private static ComponentTreeWsRequest newRequest(List<String> sortFields, boolean isAscending, @Nullable String metricKey) {
+    return new ComponentTreeWsRequest()
+      .setAsc(isAscending)
+      .setSort(sortFields)
+      .setMetricSort(metricKey);
+  }
+}
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
new file mode 100644 (file)
index 0000000..7525238
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube :: Server
+ * 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.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MeasuresWsModuleTest {
+  @Test
+  public void verify_count_of_added_components() {
+    ComponentContainer container = new ComponentContainer();
+    new MeasuresWsModule().configure(container);
+    assertThat(container.size()).isEqualTo(3 + 2);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsTest.java
new file mode 100644 (file)
index 0000000..3963d19
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube :: Server
+ * 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.junit.Test;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.WsTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class MeasuresWsTest {
+  WsTester ws = new WsTester(
+    new MeasuresWs(
+      new ComponentTreeAction(mock(ComponentTreeDataLoader.class), mock(UserSession.class), mock(I18n.class), mock(ResourceTypes.class))
+    ));
+
+  @Test
+  public void define_ws() {
+    WebService.Controller controller = ws.controller("api/measures");
+
+    assertThat(controller).isNotNull();
+    assertThat(controller.since()).isEqualTo("5.4");
+  }
+}
index 21b20373ad945514afd4cfbfcfdf964fb1bb6ced..6555942244e9dec257c7c7bac1cdf92031a5692e 100644 (file)
@@ -34,6 +34,7 @@ import org.sonar.db.activity.ActivityMapper;
 import org.sonar.db.ce.CeActivityMapper;
 import org.sonar.db.ce.CeQueueMapper;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentDtoWithSnapshotId;
 import org.sonar.db.component.ComponentLinkDto;
 import org.sonar.db.component.ComponentLinkMapper;
 import org.sonar.db.component.ComponentMapper;
@@ -164,6 +165,7 @@ public class MyBatis {
     confBuilder.loadAlias("ActiveDashboard", ActiveDashboardDto.class);
     confBuilder.loadAlias("Author", AuthorDto.class);
     confBuilder.loadAlias("Component", ComponentDto.class);
+    confBuilder.loadAlias("ComponentWithSnapshot", ComponentDtoWithSnapshotId.class);
     confBuilder.loadAlias("ComponentLink", ComponentLinkDto.class);
     confBuilder.loadAlias("Dashboard", DashboardDto.class);
     confBuilder.loadAlias("DuplicationUnit", DuplicationUnitDto.class);
index bd66c881d5ad5510792fa182e005c04baf728d70..c98d585e368e6a8cd6c706d0d61639400b1dc582 100644 (file)
@@ -150,12 +150,12 @@ public class ComponentDao implements Dao {
     return mapper(session).selectComponentsHavingSameKeyOrderedById(key);
   }
 
-  public List<ComponentDto> selectDirectChildren(DbSession dbSession, ComponentTreeQuery componentQuery) {
+  public List<ComponentDtoWithSnapshotId> selectDirectChildren(DbSession dbSession, ComponentTreeQuery componentQuery) {
     RowBounds rowBounds = new RowBounds(offset(componentQuery.getPage(), componentQuery.getPageSize()), componentQuery.getPageSize());
     return mapper(dbSession).selectDirectChildren(componentQuery, rowBounds);
   }
 
-  public List<ComponentDto> selectAllChildren(DbSession dbSession, ComponentTreeQuery componentQuery) {
+  public List<ComponentDtoWithSnapshotId> selectAllChildren(DbSession dbSession, ComponentTreeQuery componentQuery) {
     RowBounds rowBounds = new RowBounds(offset(componentQuery.getPage(), componentQuery.getPageSize()), componentQuery.getPageSize());
     return mapper(dbSession).selectAllChildren(componentQuery, rowBounds);
   }
index d24e066f70e017040c38ddb33924f993e991dcd8..db15e751d297e4a68ca4c67dbdda244dac931980 100644 (file)
@@ -59,7 +59,6 @@ public final class ComponentDtoFunctions {
     }
   }
 
-
   private enum ToKey implements Function<ComponentDto, String> {
     INSTANCE;
 
diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentDtoWithSnapshotId.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentDtoWithSnapshotId.java
new file mode 100644 (file)
index 0000000..45ca736
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube :: Database
+ * 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.component;
+
+public class ComponentDtoWithSnapshotId extends ComponentDto {
+  private Long snapshotId;
+
+  public Long getSnapshotId() {
+    return snapshotId;
+  }
+
+  public ComponentDtoWithSnapshotId setSnapshotId(Long snapshotId) {
+    this.snapshotId = snapshotId;
+    return this;
+  }
+}
index 41a873a524bb5975b413dc3ae82cd724ac5f6766..5a492a554f7ccfc287e9d39cfb82d8e5d8c8f676 100644 (file)
@@ -65,14 +65,14 @@ public interface ComponentMapper {
   /**
    * Return direct children components
    */
-  List<ComponentDto> selectDirectChildren(@Param("query") ComponentTreeQuery componentTreeQuery, RowBounds rowBounds);
+  List<ComponentDtoWithSnapshotId> selectDirectChildren(@Param("query") ComponentTreeQuery componentTreeQuery, RowBounds rowBounds);
 
   int countDirectChildren(@Param("query") ComponentTreeQuery componentTreeQuery);
 
   /**
    * Return all children components.
    */
-  List<ComponentDto> selectAllChildren(@Param("query") ComponentTreeQuery componentTreeQuery,
+  List<ComponentDtoWithSnapshotId> selectAllChildren(@Param("query") ComponentTreeQuery componentTreeQuery,
                                        RowBounds rowBounds);
 
   int countAllChildren(@Param("query") ComponentTreeQuery componentTreeQuery);
index ebe74a6b62a19e0f0b2496c46ea9e4f42a89ec4c..96ac077613ae0ff24b0165552a2d89ab2ce17194 100644 (file)
@@ -28,6 +28,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import org.sonar.db.WildcardPosition;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.FluentIterable.from;
 import static java.util.Objects.requireNonNull;
 import static org.sonar.db.DatabaseUtils.buildLikeValue;
@@ -163,8 +164,9 @@ public class ComponentTreeQuery {
       return this;
     }
 
-    public Builder setSortFields(List<String> sorts) {
-      this.sortFields = requireNonNull(sorts);
+    public Builder setSortFields(@Nullable List<String> sorts) {
+      checkArgument(sorts != null && !sorts.isEmpty());
+      this.sortFields = sorts;
       return this;
     }
 
index 49ec6e465f657b854d83ed835896c7481951a5cc..dc4c01deffecc1e7d5cb8e4a8879dc00ad8f2041 100644 (file)
@@ -55,7 +55,7 @@ public class MeasureDao implements Dao {
 
   /**
    * Selects all measures of a specific snapshot for the specified metric keys.
-   *
+   * <p/>
    * Uses by Views.
    */
   public List<MeasureDto> selectBySnapshotIdAndMetricKeys(final long snapshotId, Set<String> metricKeys, final DbSession dbSession) {
@@ -69,7 +69,7 @@ public class MeasureDao implements Dao {
   }
 
   public List<PastMeasureDto> selectByComponentUuidAndProjectSnapshotIdAndMetricIds(final DbSession session, final String componentUuid, final long projectSnapshotId,
-    Set<Integer> metricIds) {
+                                                                                    Set<Integer> metricIds) {
     return DatabaseUtils.executeLargeInputs(metricIds, new Function<List<Integer>, List<PastMeasureDto>>() {
       @Override
       public List<PastMeasureDto> apply(List<Integer> ids) {
@@ -105,6 +105,16 @@ public class MeasureDao implements Dao {
     });
   }
 
+  public List<MeasureDto> selectBySnapshotIdsAndMetricIds(final DbSession dbSession, List<Long> snapshotIds, final List<Integer> metricIds) {
+    return DatabaseUtils.executeLargeInputs(snapshotIds, new Function<List<Long>, List<MeasureDto>>() {
+      @Override
+      @Nonnull
+      public List<MeasureDto> apply(@Nonnull List<Long> input) {
+        return mapper(dbSession).selectBySnapshotIdsAndMetricIds(input, metricIds);
+      }
+    });
+  }
+
   public void insert(DbSession session, MeasureDto measureDto) {
     mapper(session).insert(measureDto);
   }
index f9b70f46e17e19d23a0d0d3198929e4cf674d98c..e9ce46a9e741b7df890c60a3e89d2f783996aee1 100644 (file)
@@ -36,6 +36,8 @@ public interface MeasureMapper {
 
   List<MeasureDto> selectBySnapshotAndMetrics(@Param("snapshotId") long snapshotId, @Param("metricIds") List<Integer> input);
 
+  List<MeasureDto> selectBySnapshotIdsAndMetricIds(@Param("snapshotIds") List<Long> snapshotIds, @Param("metricIds") List<Integer> metricIds);
+
   @CheckForNull
   MeasureDto selectByComponentAndMetric(@Param("componentKey") String componentKey, @Param("metricKey") String metricKey);
 
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
new file mode 100644 (file)
index 0000000..a1b0fa4
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube :: Database
+ * 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.metric;
+
+import com.google.common.base.Function;
+import javax.annotation.Nonnull;
+
+/**
+ * Common functions on MetricDto
+ */
+public class MetricDtoFunctions {
+  private MetricDtoFunctions() {
+    // prevents instantiation
+  }
+
+  public static Function<MetricDto, Integer> toId() {
+    return ToId.INSTANCE;
+  }
+
+  public static Function<MetricDto, String> toKey() {
+    return ToKey.INSTANCE;
+  }
+
+  private enum ToId implements Function<MetricDto, Integer> {
+    INSTANCE;
+
+    @Override
+    public Integer apply(@Nonnull MetricDto input) {
+      return input.getId();
+    }
+  }
+
+  private enum ToKey implements Function<MetricDto, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull MetricDto input) {
+      return input.getKey();
+    }
+  }
+}
index 0618022f2f8cfea1d86b0727b51a18aea4ec024b..28c94bb38f574bf94dcd9c79cb9b155293750182 100644 (file)
     </where>
   </sql>
 
-  <select id="selectDirectChildren" resultType="Component">
+  <select id="selectDirectChildren" resultType="ComponentWithSnapshot">
     select
-    <include refid="componentColumns"/>
+    <include refid="componentColumns"/>, s.id as snapshotId
     <include refid="sqlSelectByTreeQuery"/>
     and s.parent_snapshot_id = #{query.baseSnapshot.id}
     order by ${query.sqlSort}
     and s.parent_snapshot_id = #{query.baseSnapshot.id}
   </select>
 
-  <select id="selectAllChildren" resultType="Component">
+  <select id="selectAllChildren" resultType="ComponentWithSnapshot">
     select
-    <include refid="componentColumns"/>
+    <include refid="componentColumns"/>, s.id as snapshotId
     <include refid="sqlSelectAllChildren" />
     order by ${query.sqlSort}
   </select>
index f2d223963745b72b42dede949400ea475f406da9..ed8a80b3f70db33c809d7a2cc6fb12948661a0ac 100644 (file)
     </where>
   </select>
 
+  <select id="selectBySnapshotIdsAndMetricIds" parameterType="map" resultType="Measure">
+    SELECT
+    <include refid="measureColumns"/>
+    FROM project_measures pm
+    <where>
+      pm.snapshot_id in
+      <foreach item="snapshotId" collection="snapshotIds" open="(" separator="," close=")">
+        #{snapshotId}
+      </foreach>
+      AND
+      pm.metric_id in
+      <foreach item="metricId" collection="metricIds" open="(" separator="," close=")">
+        #{metricId}
+      </foreach>
+      AND pm.rule_id is NULL
+      AND pm.characteristic_id is NULL
+      AND pm.person_id is NULL
+    </where>
+  </select>
+
   <select id="selectBySnapshotAndMetrics" parameterType="map" resultType="Measure">
     SELECT
     <include refid="measureColumns"/>
index 5a46d9fed3747a362f8bdb4c7af32f4bf5b38a3f..0eaf33e3482685e5cfc1d27b821129e909b4b9ea 100644 (file)
@@ -742,7 +742,7 @@ public class ComponentDaoTest {
 
     ComponentTreeQuery query = newTreeQuery(projectSnapshot).build();
 
-    List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectDirectChildren(dbSession, query);
     int count = underTest.countDirectChildren(dbSession, query);
 
     assertThat(count).isEqualTo(2);
@@ -762,7 +762,7 @@ public class ComponentDaoTest {
     ComponentTreeQuery query = newTreeQuery(projectSnapshot)
       .setNameOrKeyQuery("file-name").build();
 
-    List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectDirectChildren(dbSession, query);
     int count = underTest.countDirectChildren(dbSession, query);
 
     assertThat(count).isEqualTo(1);
@@ -782,7 +782,7 @@ public class ComponentDaoTest {
     ComponentTreeQuery query = newTreeQuery(projectSnapshot)
       .setNameOrKeyQuery("file-key").build();
 
-    List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectDirectChildren(dbSession, query);
     int count = underTest.countDirectChildren(dbSession, query);
 
     assertThat(count).isEqualTo(1);
@@ -805,7 +805,7 @@ public class ComponentDaoTest {
       .setAsc(false)
       .build();
 
-    List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectDirectChildren(dbSession, query);
     int count = underTest.countDirectChildren(dbSession, query);
 
     assertThat(count).isEqualTo(9);
@@ -827,7 +827,7 @@ public class ComponentDaoTest {
       .setAsc(true)
       .build();
 
-    List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectDirectChildren(dbSession, query);
 
     assertThat(result).extracting("uuid").containsExactly("file-uuid-3", "file-uuid-2", "file-uuid-1");
   }
@@ -844,7 +844,7 @@ public class ComponentDaoTest {
 
     ComponentTreeQuery query = newTreeQuery(moduleSnapshot).build();
 
-    List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectDirectChildren(dbSession, query);
 
     assertThat(result).extracting("uuid").containsOnly("file-2-uuid");
   }
@@ -861,7 +861,7 @@ public class ComponentDaoTest {
 
     ComponentTreeQuery query = newTreeQuery(projectSnapshot).build();
 
-    List<ComponentDto> result = underTest.selectAllChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectAllChildren(dbSession, query);
     int count = underTest.countAllChildren(dbSession, query);
 
     assertThat(count).isEqualTo(3);
@@ -890,7 +890,7 @@ public class ComponentDaoTest {
       .setAsc(false)
       .build();
 
-    List<ComponentDto> result = underTest.selectAllChildren(dbSession, query);
+    List<ComponentDtoWithSnapshotId> result = underTest.selectAllChildren(dbSession, query);
     int count = underTest.countAllChildren(dbSession, query);
 
     assertThat(count).isEqualTo(9);
index 3c1f85bfba6a96d424668cd8bed47dfcc9110008..0727e1163d25818e6325af2a4094887a087e8d46 100644 (file)
@@ -24,6 +24,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 
 import static org.sonar.db.component.SnapshotTesting.createForComponent;
+import static org.sonar.db.component.SnapshotTesting.newSnapshotForDeveloper;
 import static org.sonar.db.component.SnapshotTesting.newSnapshotForProject;
 import static org.sonar.db.component.SnapshotTesting.newSnapshotForView;
 
@@ -54,6 +55,14 @@ public class ComponentDbTester {
     return snapshot;
   }
 
+  public SnapshotDto insertDeveloperAndSnapshot(ComponentDto component) {
+    dbClient.componentDao().insert(dbSession, component);
+    SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, newSnapshotForDeveloper(component));
+    db.commit();
+
+    return snapshot;
+  }
+
   public SnapshotDto insertComponentAndSnapshot(ComponentDto component, SnapshotDto parentSnapshot) {
     dbClient.componentDao().insert(dbSession, component);
     SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, createForComponent(component, parentSnapshot));
index 6e59f8a6bf96e36dbf5a557d7d4728514c777e91..37d04cb66ad7c07151ff38ba061c7667d37d1944 100644 (file)
@@ -46,6 +46,10 @@ public class ComponentTesting {
       .setLanguage("xoo");
   }
 
+  public static ComponentDto newDirectory(ComponentDto module, String path) {
+    return newDirectory(module, Uuids.create(), path);
+  }
+
   public static ComponentDto newDirectory(ComponentDto module, String uuid, String path) {
     return newChildComponent(uuid, module)
         .setKey(!path.equals("/") ? module.getKey() + ":" + path : module.getKey() + ":/")
@@ -65,10 +69,6 @@ public class ComponentTesting {
         .setQualifier(Qualifiers.SUBVIEW);
   }
 
-  public static ComponentDto newDirectory(ComponentDto module, String path) {
-    return newDirectory(module, Uuids.create(), path);
-  }
-
   public static ComponentDto newModuleDto(String uuid, ComponentDto subProjectOrProject) {
     return newChildComponent(uuid, subProjectOrProject)
       .setModuleUuidPath(subProjectOrProject.moduleUuidPath() + uuid + MODULE_UUID_PATH_SEP)
index 9fa87e631f8edf8339d13be09bd4a8a33d62fe07..2b464c2e4422406c01480946bf2d1e3599672ae0 100644 (file)
  */
 package org.sonar.db.component;
 
-import java.util.Collections;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class ComponentTreeQueryTest {
@@ -47,7 +47,7 @@ public class ComponentTreeQueryTest {
     expectedException.expect(NullPointerException.class);
 
     ComponentTreeQuery.builder()
-      .setSortFields(Collections.<String>emptyList())
+      .setSortFields(singletonList("name"))
       .build();
   }
 
index 49fa5a33065c35de254c3043249c515669761171..82018b73e1c96543acbd7cc191739efb18731e4c 100644 (file)
@@ -96,7 +96,7 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "shared.xml");
 
     List<MeasureDto> results = underTest.selectByComponentKeyAndMetricKeys(db.getSession(), "org.struts:struts-core:src/org/struts/RequestContext.java",
-        newArrayList("ncloc", "authors_by_line"));
+      newArrayList("ncloc", "authors_by_line"));
     assertThat(results).hasSize(2);
 
     results = underTest.selectByComponentKeyAndMetricKeys(db.getSession(), "org.struts:struts-core:src/org/struts/RequestContext.java", newArrayList("ncloc"));
@@ -329,8 +329,8 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "shared.xml");
 
     List<MeasureDto> measureDtos = underTest.selectByDeveloperForSnapshotAndMetrics(db.getSession(),
-        DEVELOPER_ID, SNAPSHOT_ID,
-        ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
+      DEVELOPER_ID, SNAPSHOT_ID,
+      ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
 
     assertThat(measureDtos).isEmpty();
   }
@@ -340,8 +340,8 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "with_some_measures_for_developer.xml");
 
     List<MeasureDto> measureDtos = underTest.selectByDeveloperForSnapshotAndMetrics(db.getSession(),
-        DEVELOPER_ID, SNAPSHOT_ID,
-        ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
+      DEVELOPER_ID, SNAPSHOT_ID,
+      ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
 
     assertThat(measureDtos).extracting("id").containsOnly(30L, 31L, 32L);
   }
@@ -351,8 +351,8 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "with_some_measures_for_developer.xml");
 
     List<MeasureDto> measureDtos = underTest.selectByDeveloperForSnapshotAndMetrics(db.getSession(),
-        DEVELOPER_ID, SNAPSHOT_ID,
-        ImmutableList.of(NCLOC_METRIC_ID));
+      DEVELOPER_ID, SNAPSHOT_ID,
+      ImmutableList.of(NCLOC_METRIC_ID));
 
     assertThat(measureDtos).extracting("id").containsOnly(32L);
   }
@@ -362,8 +362,8 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "with_some_measures_for_developer.xml");
 
     List<MeasureDto> measureDtos = underTest.selectBySnapshotAndMetrics(db.getSession(),
-        SNAPSHOT_ID,
-        ImmutableList.of(666));
+      SNAPSHOT_ID,
+      ImmutableList.of(666));
 
     assertThat(measureDtos).isEmpty();
   }
@@ -373,8 +373,8 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "with_some_measures_for_developer.xml");
 
     List<MeasureDto> measureDtos = underTest.selectBySnapshotAndMetrics(db.getSession(),
-        SNAPSHOT_ID,
-        ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
+      SNAPSHOT_ID,
+      ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
 
     assertThat(measureDtos).extracting("id").containsOnly(20L, 21L, 22L);
   }
@@ -384,8 +384,8 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "with_some_measures_for_developer.xml");
 
     List<MeasureDto> measureDtos = underTest.selectBySnapshotAndMetrics(db.getSession(),
-        SNAPSHOT_ID,
-        ImmutableList.of(NCLOC_METRIC_ID));
+      SNAPSHOT_ID,
+      ImmutableList.of(NCLOC_METRIC_ID));
 
     assertThat(measureDtos).extracting("id").containsOnly(22L);
   }
@@ -395,8 +395,8 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "with_some_measures_for_developer.xml");
 
     List<MeasureDto> measureDtos = underTest.selectByDeveloperForSnapshotAndMetrics(db.getSession(),
-        DEVELOPER_ID, SNAPSHOT_ID,
-        ImmutableList.of(666));
+      DEVELOPER_ID, SNAPSHOT_ID,
+      ImmutableList.of(666));
 
     assertThat(measureDtos).isEmpty();
   }
@@ -406,33 +406,35 @@ public class MeasureDaoTest {
     db.prepareDbUnit(getClass(), "with_some_measures_for_developer.xml");
 
     List<MeasureDto> measureDtos = underTest.selectByDeveloperForSnapshotAndMetrics(db.getSession(),
-        DEVELOPER_ID, 10,
-        ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
+      DEVELOPER_ID, 10,
+      ImmutableList.of(AUTHORS_BY_LINE_METRIC_ID, COVERAGE_LINE_HITS_DATA_METRIC_ID, NCLOC_METRIC_ID));
 
     assertThat(measureDtos).isEmpty();
   }
 
+  //TODO add test for selectBySnapshotIdsAndMetricIds
+
   @Test
   public void insert() {
     db.prepareDbUnit(getClass(), "empty.xml");
 
     underTest.insert(db.getSession(), new MeasureDto()
-        .setSnapshotId(2L)
-        .setMetricId(3)
-        .setCharacteristicId(4)
+      .setSnapshotId(2L)
+      .setMetricId(3)
+      .setCharacteristicId(4)
       .setDeveloperId(23L)
-        .setRuleId(5)
-        .setComponentId(6L)
-        .setValue(2.0d)
-        .setData("measure-value")
-        .setVariation(1, 1.0d)
-        .setVariation(2, 2.0d)
-        .setVariation(3, 3.0d)
-        .setVariation(4, 4.0d)
-        .setVariation(5, 5.0d)
-        .setAlertStatus("alert")
-        .setAlertText("alert-text")
-        .setDescription("measure-description")
+      .setRuleId(5)
+      .setComponentId(6L)
+      .setValue(2.0d)
+      .setData("measure-value")
+      .setVariation(1, 1.0d)
+      .setVariation(2, 2.0d)
+      .setVariation(3, 3.0d)
+      .setVariation(4, 4.0d)
+      .setVariation(5, 5.0d)
+      .setAlertStatus("alert")
+      .setAlertText("alert-text")
+      .setDescription("measure-description")
     );
     db.getSession().commit();
 
index 5e58b6962bb4acfed8d35da78a74e6d132909b17..bbdd410f1066e869d1a8a00f8e1fd27802d15d75 100644 (file)
@@ -32,7 +32,6 @@ public class MeasureTesting {
     return new MeasureDto()
       .setMetricId(metricDto.getId())
       .setMetricKey(metricDto.getKey())
-      .setSnapshotId((long) nextInt())
       .setComponentId((long) nextInt())
       .setSnapshotId(snapshotId);
   }
index 05247b30feeea3271eab1bf9800e30ce156f5197..65be1a5f1bfe3d0704c71088391890fa3163bb1d 100644 (file)
@@ -41,11 +41,10 @@ public class MetricTesting {
       .setDeleteHistoricalData(RandomUtils.nextBoolean())
       .setDirection(RandomUtils.nextInt())
       .setHidden(RandomUtils.nextBoolean())
-      .setEnabled(RandomUtils.nextBoolean())
+      .setEnabled(true)
       .setOptimizedBestValue(RandomUtils.nextBoolean())
       .setQualitative(RandomUtils.nextBoolean())
       .setUserManaged(RandomUtils.nextBoolean())
       .setWorstValue(RandomUtils.nextDouble());
   }
-
 }
index 5bc022283d72ff325158302c647a322157a483f0..f517cc49c5d86330f2bbe842430aff62ab38dbaa 100644 (file)
@@ -411,32 +411,24 @@ public interface WebService extends Definable<WebService.Context> {
      * Add predefined parameters related to sorting of results.
      */
     public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
-      genericAddSortParam(possibleValues, defaultValue, defaultAscending, "Sort field");
-
+      createSortParams(possibleValues, defaultValue, defaultAscending);
       return this;
     }
 
     /**
-     * Add predefined parameters related to sorting of results. Comma-separated list
+     * Add predefined parameters related to sorting of results.
      */
-    public <V> NewAction addMultiSortsParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
-      genericAddSortParam(possibleValues, defaultValue, defaultAscending, "Comma-separated list of sort fields");
-
-      return this;
-    }
-
-    public <V> NewAction genericAddSortParam(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending, String description) {
-      createParam(Param.SORT)
-        .setDescription(description)
-        .setDeprecatedKey("sort")
-        .setDefaultValue(defaultValue)
-        .setPossibleValues(possibleValues);
-
+    public <V> NewParam createSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
       createParam(Param.ASCENDING)
         .setDescription("Ascending sort")
         .setBooleanPossibleValues()
         .setDefaultValue(defaultAscending);
-      return this;
+
+      return createParam(Param.SORT)
+        .setDescription("Sort field")
+        .setDeprecatedKey("sort")
+        .setDefaultValue(defaultValue)
+        .setPossibleValues(possibleValues);
     }
 
     /**
index e3bd7ad1aabe7b3acd55a69abb5b5669cc6cf804..c8f2e816117f8033f3e5dbd4408f641e84d9c27c 100644 (file)
  */
 package org.sonarqube.ws.client;
 
+import com.google.common.base.Joiner;
 import com.google.protobuf.Message;
 import com.google.protobuf.Parser;
 import java.io.InputStream;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.sonarqube.ws.MediaTypes;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -29,6 +33,8 @@ import static com.google.common.base.Strings.isNullOrEmpty;
 
 public abstract class BaseService {
 
+  private static final Joiner MULTI_VALUES_JOINER = Joiner.on(",");
+
   private final WsConnector wsConnector;
   protected final String controller;
 
@@ -60,4 +66,9 @@ public abstract class BaseService {
   protected String path(String action) {
     return String.format("%s/%s", controller, action);
   }
+
+  @CheckForNull
+  protected static String inlineMultipleParamValue(@Nullable List<String> values) {
+    return values == null ? null : MULTI_VALUES_JOINER.join(values);
+  }
 }
index f779b9869c1b56186b0aaf146d72642701335c65..8921549e96267b2141242791801d51c5a7b2562c 100644 (file)
@@ -21,6 +21,7 @@ package org.sonarqube.ws.client;
 
 import org.sonarqube.ws.client.component.ComponentsService;
 import org.sonarqube.ws.client.issue.IssuesService;
+import org.sonarqube.ws.client.measure.MeasuresService;
 import org.sonarqube.ws.client.permission.PermissionsService;
 import org.sonarqube.ws.client.qualitygate.QualityGatesService;
 import org.sonarqube.ws.client.qualityprofile.QualityProfilesService;
@@ -39,6 +40,7 @@ public class HttpWsClient implements WsClient {
   private final IssuesService issuesService;
   private final UserTokensService userTokensService;
   private final QualityGatesService qualityGatesService;
+  private final MeasuresService measuresService;
   private final WsConnector wsConnector;
 
   public HttpWsClient(WsConnector wsConnector) {
@@ -49,6 +51,7 @@ public class HttpWsClient implements WsClient {
     this.issuesService = new IssuesService(wsConnector);
     this.userTokensService = new UserTokensService(wsConnector);
     this.qualityGatesService = new QualityGatesService(wsConnector);
+    this.measuresService = new MeasuresService(wsConnector);
   }
 
   @Override
@@ -85,4 +88,9 @@ public class HttpWsClient implements WsClient {
   public QualityGatesService qualityGates() {
     return qualityGatesService;
   }
+
+  @Override
+  public MeasuresService measures() {
+    return measuresService;
+  }
 }
index a632900586403678a46027c8bc55c8a263a8e261..e833c20f1d1ebeea14b4b7a9eed5620eb1ed0027 100644 (file)
@@ -21,6 +21,7 @@ package org.sonarqube.ws.client;
 
 import org.sonarqube.ws.client.component.ComponentsService;
 import org.sonarqube.ws.client.issue.IssuesService;
+import org.sonarqube.ws.client.measure.MeasuresService;
 import org.sonarqube.ws.client.permission.PermissionsService;
 import org.sonarqube.ws.client.qualitygate.QualityGatesService;
 import org.sonarqube.ws.client.qualityprofile.QualityProfilesService;
@@ -42,5 +43,7 @@ public interface WsClient {
 
   QualityGatesService qualityGates();
 
+  MeasuresService measures();
+
   WsConnector wsConnector();
 }
index 95dac85cb66e41d08fd889c6becc1d6ec870c1ac..3188123e551841468937e13d47c5265fb580c8f2 100644 (file)
@@ -55,7 +55,7 @@ public class ComponentsService extends BaseService {
     GetRequest get = new GetRequest(path(ACTION_TREE))
       .setParam(PARAM_BASE_COMPONENT_ID, request.getBaseComponentId())
       .setParam(PARAM_BASE_COMPONENT_KEY, request.getBaseComponentKey())
-      .setParam(PARAM_QUALIFIERS, request.getQualifiers())
+      .setParam(PARAM_QUALIFIERS, inlineMultipleParamValue(request.getQualifiers()))
       .setParam(PARAM_STRATEGY, request.getStrategy())
       .setParam("p", request.getPage())
       .setParam("ps", request.getPageSize())
index 945174c838fcce40f3bdd8a1dbe21660034c6f9d..dbd2f1f60e6d4f6d8f54f113ae88efb4f2ca2ad2 100644 (file)
@@ -20,9 +20,6 @@
 package org.sonarqube.ws.client.issue;
 
 import com.google.common.base.Joiner;
-import java.util.List;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
 import org.sonarqube.ws.Issues.SearchWsResponse;
 import org.sonarqube.ws.client.BaseService;
 import org.sonarqube.ws.client.GetRequest;
@@ -72,50 +69,43 @@ public class IssuesService extends BaseService {
   public SearchWsResponse search(SearchWsRequest request) {
     return call(
       new GetRequest(path("search"))
-        .setParam(ACTION_PLANS, listToParamList(request.getActionPlans()))
-        .setParam(ADDITIONAL_FIELDS, listToParamList(request.getAdditionalFields()))
+        .setParam(ACTION_PLANS, inlineMultipleParamValue(request.getActionPlans()))
+        .setParam(ADDITIONAL_FIELDS, inlineMultipleParamValue(request.getAdditionalFields()))
         .setParam(ASC, request.getAsc())
         .setParam(ASSIGNED, request.getAssigned())
-        .setParam(ASSIGNEES, listToParamList(request.getAssignees()))
-        .setParam(AUTHORS, listToParamList(request.getAuthors()))
-        .setParam(COMPONENT_KEYS, listToParamList(request.getComponentKeys()))
-        .setParam(COMPONENT_ROOT_UUIDS, listToParamList(request.getComponentRootUuids()))
-        .setParam(COMPONENT_ROOTS, listToParamList(request.getComponentRoots()))
-        .setParam(COMPONENT_UUIDS, listToParamList(request.getComponentUuids()))
-        .setParam(COMPONENTS, listToParamList(request.getComponents()))
+        .setParam(ASSIGNEES, inlineMultipleParamValue(request.getAssignees()))
+        .setParam(AUTHORS, inlineMultipleParamValue(request.getAuthors()))
+        .setParam(COMPONENT_KEYS, inlineMultipleParamValue(request.getComponentKeys()))
+        .setParam(COMPONENT_ROOT_UUIDS, inlineMultipleParamValue(request.getComponentRootUuids()))
+        .setParam(COMPONENT_ROOTS, inlineMultipleParamValue(request.getComponentRoots()))
+        .setParam(COMPONENT_UUIDS, inlineMultipleParamValue(request.getComponentUuids()))
+        .setParam(COMPONENTS, inlineMultipleParamValue(request.getComponents()))
         .setParam(CREATED_AFTER, request.getCreatedAfter())
         .setParam(CREATED_AT, request.getCreatedAt())
         .setParam(CREATED_BEFORE, request.getCreatedBefore())
         .setParam(CREATED_IN_LAST, request.getCreatedInLast())
-        .setParam(DIRECTORIES, listToParamList(request.getDirectories()))
+        .setParam(DIRECTORIES, inlineMultipleParamValue(request.getDirectories()))
         .setParam(FACET_MODE, request.getFacetMode())
-        .setParam("facets", listToParamList(request.getFacets()))
-        .setParam(FILE_UUIDS, listToParamList(request.getFileUuids()))
-        .setParam(ISSUES, listToParamList(request.getIssues()))
-        .setParam(LANGUAGES, listToParamList(request.getLanguages()))
-        .setParam(MODULE_UUIDS, listToParamList(request.getModuleUuids()))
+        .setParam("facets", inlineMultipleParamValue(request.getFacets()))
+        .setParam(FILE_UUIDS, inlineMultipleParamValue(request.getFileUuids()))
+        .setParam(ISSUES, inlineMultipleParamValue(request.getIssues()))
+        .setParam(LANGUAGES, inlineMultipleParamValue(request.getLanguages()))
+        .setParam(MODULE_UUIDS, inlineMultipleParamValue(request.getModuleUuids()))
         .setParam(ON_COMPONENT_ONLY, request.getOnComponentOnly())
         .setParam("p", request.getPage())
         .setParam("ps", request.getPageSize())
         .setParam(PLANNED, request.getPlanned())
-        .setParam(PROJECT_KEYS, listToParamList(request.getProjectKeys()))
-        .setParam(PROJECT_UUIDS, listToParamList(request.getProjectUuids()))
-        .setParam(PROJECTS, listToParamList(request.getProjects()))
-        .setParam(REPORTERS, listToParamList(request.getReporters()))
-        .setParam(RESOLUTIONS, listToParamList(request.getResolutions()))
+        .setParam(PROJECT_KEYS, inlineMultipleParamValue(request.getProjectKeys()))
+        .setParam(PROJECT_UUIDS, inlineMultipleParamValue(request.getProjectUuids()))
+        .setParam(PROJECTS, inlineMultipleParamValue(request.getProjects()))
+        .setParam(REPORTERS, inlineMultipleParamValue(request.getReporters()))
+        .setParam(RESOLUTIONS, inlineMultipleParamValue(request.getResolutions()))
         .setParam(RESOLVED, request.getResolved())
-        .setParam(RULES, listToParamList(request.getRules()))
+        .setParam(RULES, inlineMultipleParamValue(request.getRules()))
         .setParam("s", request.getSort())
-        .setParam(SEVERITIES, listToParamList(request.getSeverities()))
-        .setParam(STATUSES, listToParamList(request.getStatuses()))
-        .setParam(TAGS, listToParamList(request.getTags())),
+        .setParam(SEVERITIES, inlineMultipleParamValue(request.getSeverities()))
+        .setParam(STATUSES, inlineMultipleParamValue(request.getStatuses()))
+        .setParam(TAGS, inlineMultipleParamValue(request.getTags())),
       SearchWsResponse.parser());
   }
-
-  @CheckForNull
-  private static String listToParamList(@Nullable List<String> strings) {
-    return strings == null
-      ? null
-      : LIST_TO_PARAMS_STRING.join(strings);
-  }
 }
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentTreeWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentTreeWsRequest.java
new file mode 100644 (file)
index 0000000..2519f13
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * SonarQube :: Web Service
+ * 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 ComponentTreeWsRequest {
+
+  private String baseComponentId;
+  private String baseComponentKey;
+  private String strategy;
+  private List<String> qualifiers;
+  private List<String> additionalFields;
+  private String query;
+  private List<String> sort;
+  private Boolean asc;
+  private String metricSort;
+  private List<String> metricKeys;
+  private Integer page;
+  private Integer pageSize;
+
+  @CheckForNull
+  public String getBaseComponentId() {
+    return baseComponentId;
+  }
+
+  public ComponentTreeWsRequest setBaseComponentId(@Nullable String baseComponentId) {
+    this.baseComponentId = baseComponentId;
+    return this;
+  }
+
+  @CheckForNull
+  public String getBaseComponentKey() {
+    return baseComponentKey;
+  }
+
+  public ComponentTreeWsRequest setBaseComponentKey(@Nullable String baseComponentKey) {
+    this.baseComponentKey = baseComponentKey;
+    return this;
+  }
+
+  @CheckForNull
+  public String getStrategy() {
+    return strategy;
+  }
+
+  public ComponentTreeWsRequest setStrategy(String strategy) {
+    this.strategy = strategy;
+    return this;
+  }
+
+  @CheckForNull
+  public List<String> getQualifiers() {
+    return qualifiers;
+  }
+
+  public ComponentTreeWsRequest setQualifiers(@Nullable List<String> qualifiers) {
+    this.qualifiers = qualifiers;
+    return this;
+  }
+
+  @CheckForNull
+  public List<String> getAdditionalFields() {
+    return additionalFields;
+  }
+
+  public ComponentTreeWsRequest setAdditionalFields(@Nullable List<String> additionalFields) {
+    this.additionalFields = additionalFields;
+    return this;
+  }
+
+  @CheckForNull
+  public String getQuery() {
+    return query;
+  }
+
+  public ComponentTreeWsRequest setQuery(@Nullable String query) {
+    this.query = query;
+    return this;
+  }
+
+  @CheckForNull
+  public List<String> getSort() {
+    return sort;
+  }
+
+  public ComponentTreeWsRequest setSort(@Nullable List<String> sort) {
+    this.sort = sort;
+    return this;
+  }
+
+  @CheckForNull
+  public String getMetricSort() {
+    return metricSort;
+  }
+
+  public ComponentTreeWsRequest setMetricSort(@Nullable String metricSort) {
+    this.metricSort = metricSort;
+    return this;
+  }
+
+  @CheckForNull
+  public List<String> getMetricKeys() {
+    return metricKeys;
+  }
+
+  public ComponentTreeWsRequest setMetricKeys(List<String> metricKeys) {
+    this.metricKeys = metricKeys;
+    return this;
+  }
+
+  @CheckForNull
+  public Boolean getAsc() {
+    return asc;
+  }
+
+  public ComponentTreeWsRequest setAsc(boolean asc) {
+    this.asc = asc;
+    return this;
+  }
+
+  @CheckForNull
+  public Integer getPage() {
+    return page;
+  }
+
+  public ComponentTreeWsRequest setPage(int page) {
+    this.page = page;
+    return this;
+  }
+
+  @CheckForNull
+  public Integer getPageSize() {
+    return pageSize;
+  }
+
+  public ComponentTreeWsRequest setPageSize(int pageSize) {
+    this.pageSize = pageSize;
+    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
new file mode 100644 (file)
index 0000000..3e34aac
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube :: Web Service
+ * 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 org.sonarqube.ws.WsMeasures;
+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_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_METRIC_KEYS;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_STRATEGY;
+
+public class MeasuresService extends BaseService {
+  public MeasuresService(WsConnector wsConnector) {
+    super(wsConnector, CONTROLLER_MEASURES);
+  }
+
+  public WsMeasures.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())
+      .setParam(PARAM_STRATEGY, request.getStrategy())
+      .setParam(PARAM_QUALIFIERS, inlineMultipleParamValue(request.getQualifiers()))
+      .setParam(PARAM_METRIC_KEYS, inlineMultipleParamValue(request.getMetricKeys()))
+      .setParam(PARAM_ADDITIONAL_FIELDS, inlineMultipleParamValue(request.getAdditionalFields()))
+      .setParam("q", request.getQuery())
+      .setParam("p", request.getPage())
+      .setParam("ps", request.getPageSize())
+      .setParam("s", inlineMultipleParamValue(request.getSort()))
+      .setParam("asc", request.getAsc())
+      .setParam(PARAM_METRIC_SORT, request.getMetricSort());
+
+    return call(getRequest, WsMeasures.ComponentTreeWsResponse.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
new file mode 100644 (file)
index 0000000..fdc58f6
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube :: Web Service
+ * 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;
+
+public class MeasuresWsParameters {
+  private MeasuresWsParameters() {
+    // static constants only
+  }
+
+  public static final String CONTROLLER_MEASURES = "api/measures";
+
+  // actions
+  public static final String ACTION_COMPONENT_TREE = "component_tree";
+
+  // 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";
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/package-info.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/package-info.java
new file mode 100644 (file)
index 0000000..09161aa
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube :: Web Service
+ * 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.
+ */
+
+
+@ParametersAreNonnullByDefault
+package org.sonarqube.ws.client.measure;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
index 58b57986c78cd6848cbe980ccdfd8b6e1307df17..efd2fee3ca4a352d2cb7bdc08aceb1708fdb4adc 100644 (file)
@@ -97,3 +97,18 @@ message TextRange {
   // If absent it means range ends at the last offset of end line
   optional int32 endOffset = 4;
 }
+
+message Metric {
+  optional string key = 1;
+  optional string name = 2;
+  optional string description = 3;
+  optional string domain = 4;
+  optional string type = 5;
+  optional bool higherValuesAreBetter = 6;
+  optional bool qualitative = 7;
+  optional bool hidden = 8;
+  optional bool custom = 9;
+  optional int32 decimalScale = 10;
+  optional string bestValue = 11;
+  optional string worstValue = 12;
+}
diff --git a/sonar-ws/src/main/protobuf/ws-measures.proto b/sonar-ws/src/main/protobuf/ws-measures.proto
new file mode 100644 (file)
index 0000000..3954846
--- /dev/null
@@ -0,0 +1,78 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2015 SonarSource
+// mailto:contact AT sonarsource DOT com
+//
+// SonarQube 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.
+//
+// SonarQube 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.
+
+syntax = "proto2";
+
+package sonarqube.ws.measures;
+
+import "ws-commons.proto";
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "WsMeasures";
+option optimize_for = SPEED;
+
+// WS api/measures/component_tree
+message ComponentTreeWsResponse {
+  optional sonarqube.ws.commons.Paging paging = 1;
+  optional Component baseComponent = 2;
+  repeated Component components = 3;
+  optional Metrics metrics = 4;
+  optional Periods periods = 5;
+}
+
+message Component {
+  optional string id = 1;
+  optional string refId = 2;
+  optional string key = 3;
+  optional string projectId = 4;
+  optional string name = 5;
+  optional string description = 6;
+  optional string qualifier = 7;
+  optional string path = 8;
+  optional string language = 9;
+  optional Measures measures = 10;
+}
+
+message Period {
+  optional int32 index = 1;
+  optional string mode = 2;
+  optional string date = 3;
+  optional string parameter = 4;
+}
+
+message Periods {
+  repeated Period periods = 1;
+}
+
+message Metrics {
+  repeated sonarqube.ws.commons.Metric metrics = 1;
+}
+
+message Measures {
+  repeated Measure measures = 1;
+}
+
+message Measure {
+  optional string metric = 1;
+  optional string value = 2;
+  optional string variationValueP1 = 3;
+  optional string variationValueP2 = 4;
+  optional string variationValueP3 = 5;
+  optional string variationValueP4 = 6;
+  optional string variationValueP5 = 7;
+}
index 3ba2e88aef44889952b232f3acd523623cd41980..01f0019f3b861400c1e0d5b0edaeef8f1fdb8663 100644 (file)
@@ -308,6 +308,16 @@ public class ServiceTester<T extends BaseService> extends ExternalResource {
       return this;
     }
 
+    public RequestAssert hasParam(String key, boolean value) {
+      isNotNull();
+
+      MapEntry<String, String> entry = MapEntry.entry(key, String.valueOf(value));
+      Assertions.assertThat(actual.getParams()).contains(entry);
+      this.assertedParams.add(entry);
+
+      return this;
+    }
+
     public RequestAssert andNoOtherParam() {
       isNotNull();
 
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/measure/MeasuresServiceTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/measure/MeasuresServiceTest.java
new file mode 100644 (file)
index 0000000..c41080a
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube :: Web Service
+ * 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.ArrayList;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures.ComponentTreeWsResponse;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.ServiceTester;
+import org.sonarqube.ws.client.WsConnector;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+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_METRIC_KEYS;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_STRATEGY;
+
+public class MeasuresServiceTest {
+  private static final String VALUE_BASE_COMPONENT_ID = "base-component-id";
+  private static final String VALUE_BASE_COMPONENT_KEY = "base-component-key";
+  private static final List<String> VALUE_METRIC_KEYS = newArrayList("ncloc", "complexity");
+  private static final String VALUE_STRATEGY = "all";
+  private static final List<String> VALUE_QUALIFIERS = newArrayList("FIL", "PRJ");
+  private static final ArrayList<String> VALUE_ADDITIONAL_FIELDS = newArrayList("metrics");
+  private static final List<String> VALUE_SORT = newArrayList("qualifier", "metric");
+  private static final boolean VALUE_ASC = false;
+  private static final String VALUE_METRIC_SORT = "ncloc";
+  private static final int VALUE_PAGE = 42;
+  private static final int VALUE_PAGE_SIZE = 1984;
+  private static final String VALUE_QUERY = "query-sq";
+
+  @Rule
+  public ServiceTester<MeasuresService> serviceTester = new ServiceTester<>(new MeasuresService(mock(WsConnector.class)));
+
+  private MeasuresService underTest = serviceTester.getInstanceUnderTest();
+
+  @Test
+  public void component_tree() {
+    ComponentTreeWsRequest componentTreeRequest = new ComponentTreeWsRequest()
+      .setBaseComponentId(VALUE_BASE_COMPONENT_ID)
+      .setBaseComponentKey(VALUE_BASE_COMPONENT_KEY)
+      .setMetricKeys(VALUE_METRIC_KEYS)
+      .setStrategy(VALUE_STRATEGY)
+      .setQualifiers(VALUE_QUALIFIERS)
+      .setAdditionalFields(VALUE_ADDITIONAL_FIELDS)
+      .setSort(VALUE_SORT)
+      .setAsc(VALUE_ASC)
+      .setMetricSort(VALUE_METRIC_SORT)
+      .setPage(VALUE_PAGE)
+      .setPageSize(VALUE_PAGE_SIZE)
+      .setQuery(VALUE_QUERY);
+
+    underTest.componentTree(componentTreeRequest);
+    GetRequest getRequest = serviceTester.getGetRequest();
+
+    assertThat(serviceTester.getGetParser()).isSameAs(ComponentTreeWsResponse.parser());
+    serviceTester.assertThat(getRequest)
+      .hasParam(PARAM_BASE_COMPONENT_ID, VALUE_BASE_COMPONENT_ID)
+      .hasParam(PARAM_BASE_COMPONENT_KEY, VALUE_BASE_COMPONENT_KEY)
+      .hasParam(PARAM_METRIC_KEYS, "ncloc,complexity")
+      .hasParam(PARAM_STRATEGY, VALUE_STRATEGY)
+      .hasParam(PARAM_QUALIFIERS, "FIL,PRJ")
+      .hasParam(PARAM_ADDITIONAL_FIELDS, "metrics")
+      .hasParam("s", "qualifier,metric")
+      .hasParam("asc", VALUE_ASC)
+      .hasParam(PARAM_METRIC_SORT, VALUE_METRIC_SORT)
+      .hasParam("p", VALUE_PAGE)
+      .hasParam("ps", VALUE_PAGE_SIZE)
+      .hasParam("q", VALUE_QUERY)
+      .andNoOtherParam();
+  }
+}