aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>2016-01-06 17:46:38 +0100
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>2016-01-13 14:13:35 +0100
commit0242e1da3fea5a96a9f0632156b1cacdd89b9ace (patch)
tree68166c8f6ed52713ba24b5d94e9fe00616491e05
parent49b3b0bc394ce675356d832e1d12459b9be543bd (diff)
downloadsonarqube-0242e1da3fea5a96a9f0632156b1cacdd89b9ace.tar.gz
sonarqube-0242e1da3fea5a96a9f0632156b1cacdd89b9ace.zip
SONAR-7135 WS api/measures/component_tree navigate through components and display measures
-rw-r--r--it/it-tests/src/test/java/it/measure/MeasuresWsTest.java82
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java3
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java13
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentDtoToWsComponent.java65
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java241
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeData.java145
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java436
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java161
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java61
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureValueFormatter.java96
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWs.java45
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsAction.java28
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java35
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoToWsMetric.java59
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java2
-rw-r--r--server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json134
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/component/ComponentFinderTest.java116
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java497
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java158
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java35
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsTest.java47
-rw-r--r--sonar-db/src/main/java/org/sonar/db/MyBatis.java2
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java4
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentDtoFunctions.java1
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentDtoWithSnapshotId.java33
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java4
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java6
-rw-r--r--sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java14
-rw-r--r--sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java2
-rw-r--r--sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java58
-rw-r--r--sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml8
-rw-r--r--sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml20
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java16
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java9
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java8
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java4
-rw-r--r--sonar-db/src/test/java/org/sonar/db/measure/MeasureDaoTest.java66
-rw-r--r--sonar-db/src/test/java/org/sonar/db/measure/MeasureTesting.java1
-rw-r--r--sonar-db/src/test/java/org/sonar/db/metric/MetricTesting.java3
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java26
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java11
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java8
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java3
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java2
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesService.java58
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentTreeWsRequest.java160
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java59
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java42
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/measure/package-info.java26
-rw-r--r--sonar-ws/src/main/protobuf/ws-commons.proto15
-rw-r--r--sonar-ws/src/main/protobuf/ws-measures.proto78
-rw-r--r--sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java10
-rw-r--r--sonar-ws/src/test/java/org/sonarqube/ws/client/measure/MeasuresServiceTest.java96
53 files changed, 3194 insertions, 118 deletions
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
index 00000000000..b62e390e627
--- /dev/null
+++ b/it/it-tests/src/test/java/it/measure/MeasuresWsTest.java
@@ -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");
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java
index 93978a42850..105c343b506 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java
@@ -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);
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java
index 33caeefffed..62aeefb9fbe 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java
@@ -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
index 00000000000..f2d35bd3567
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentDtoToWsComponent.java
@@ -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
index 00000000000..0dbc5960ee7
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java
@@ -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
index 00000000000..6df00222bbe
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeData.java
@@ -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
index 00000000000..bc84c432c9d
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java
@@ -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
index 00000000000..83f964f83ba
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java
@@ -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
index 00000000000..3aaf476dc6e
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java
@@ -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
index 00000000000..484a78703c1
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureValueFormatter.java
@@ -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
index 00000000000..b7b948b5645
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWs.java
@@ -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
index 00000000000..5aaafdcc79a
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsAction.java
@@ -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
index 00000000000..1fd593b522c
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java
@@ -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
index 00000000000..6932335697d
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoToWsMetric.java
@@ -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();
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 1c2381c5650..5313719a434 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -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
index 00000000000..1db85e0e61d
--- /dev/null
+++ b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json
@@ -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
index 00000000000..ad6982f2858
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentFinderTest.java
@@ -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
index 00000000000..ce48509f983
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java
@@ -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
index 00000000000..f4903c0cdbe
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java
@@ -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
index 00000000000..7525238487a
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java
@@ -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
index 00000000000..3963d199f93
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsTest.java
@@ -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");
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/MyBatis.java b/sonar-db/src/main/java/org/sonar/db/MyBatis.java
index 21b20373ad9..6555942244e 100644
--- a/sonar-db/src/main/java/org/sonar/db/MyBatis.java
+++ b/sonar-db/src/main/java/org/sonar/db/MyBatis.java
@@ -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);
diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java
index bd66c881d5a..c98d585e368 100644
--- a/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java
+++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java
@@ -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);
}
diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentDtoFunctions.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentDtoFunctions.java
index d24e066f70e..db15e751d29 100644
--- a/sonar-db/src/main/java/org/sonar/db/component/ComponentDtoFunctions.java
+++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentDtoFunctions.java
@@ -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
index 00000000000..45ca736d67c
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentDtoWithSnapshotId.java
@@ -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;
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java
index 41a873a524b..5a492a554f7 100644
--- a/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java
+++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java
@@ -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);
diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java
index ebe74a6b62a..96ac077613a 100644
--- a/sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java
+++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java
@@ -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;
}
diff --git a/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java
index 49ec6e465f6..dc4c01deffe 100644
--- a/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java
+++ b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java
@@ -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);
}
diff --git a/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java b/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java
index f9b70f46e17..e9ce46a9e74 100644
--- a/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java
+++ b/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java
@@ -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
index 00000000000..a1b0fa484e4
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java
@@ -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();
+ }
+ }
+}
diff --git a/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml b/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml
index 0618022f2f8..28c94bb38f5 100644
--- a/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml
+++ b/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml
@@ -302,9 +302,9 @@
</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}
@@ -316,9 +316,9 @@
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>
diff --git a/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml b/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml
index f2d22396374..ed8a80b3f70 100644
--- a/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml
+++ b/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml
@@ -92,6 +92,26 @@
</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"/>
diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java
index 5a46d9fed37..0eaf33e3482 100644
--- a/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java
+++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java
@@ -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);
diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java
index 3c1f85bfba6..0727e1163d2 100644
--- a/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java
+++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java
@@ -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));
diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java
index 6e59f8a6bf9..37d04cb66ad 100644
--- a/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java
+++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java
@@ -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)
diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java
index 9fa87e631f8..2b464c2e442 100644
--- a/sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java
+++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java
@@ -19,12 +19,12 @@
*/
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();
}
diff --git a/sonar-db/src/test/java/org/sonar/db/measure/MeasureDaoTest.java b/sonar-db/src/test/java/org/sonar/db/measure/MeasureDaoTest.java
index 49fa5a33065..82018b73e1c 100644
--- a/sonar-db/src/test/java/org/sonar/db/measure/MeasureDaoTest.java
+++ b/sonar-db/src/test/java/org/sonar/db/measure/MeasureDaoTest.java
@@ -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();
diff --git a/sonar-db/src/test/java/org/sonar/db/measure/MeasureTesting.java b/sonar-db/src/test/java/org/sonar/db/measure/MeasureTesting.java
index 5e58b6962bb..bbdd410f106 100644
--- a/sonar-db/src/test/java/org/sonar/db/measure/MeasureTesting.java
+++ b/sonar-db/src/test/java/org/sonar/db/measure/MeasureTesting.java
@@ -32,7 +32,6 @@ public class MeasureTesting {
return new MeasureDto()
.setMetricId(metricDto.getId())
.setMetricKey(metricDto.getKey())
- .setSnapshotId((long) nextInt())
.setComponentId((long) nextInt())
.setSnapshotId(snapshotId);
}
diff --git a/sonar-db/src/test/java/org/sonar/db/metric/MetricTesting.java b/sonar-db/src/test/java/org/sonar/db/metric/MetricTesting.java
index 05247b30fee..65be1a5f1bf 100644
--- a/sonar-db/src/test/java/org/sonar/db/metric/MetricTesting.java
+++ b/sonar-db/src/test/java/org/sonar/db/metric/MetricTesting.java
@@ -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());
}
-
}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
index 5bc022283d7..f517cc49c5d 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
@@ -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);
}
/**
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java
index e3bd7ad1aab..c8f2e816117 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java
@@ -19,9 +19,13 @@
*/
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);
+ }
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java
index f779b9869c1..8921549e962 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java
@@ -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;
+ }
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
index a6329005864..e833c20f1d1 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
@@ -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();
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java
index 95dac85cb66..3188123e551 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java
@@ -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())
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesService.java
index 945174c838f..dbd2f1f60e6 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesService.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesService.java
@@ -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
index 00000000000..2519f13e436
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/ComponentTreeWsRequest.java
@@ -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
index 00000000000..3e34aacdf8d
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresService.java
@@ -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
index 00000000000..fdc58f60e69
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/MeasuresWsParameters.java
@@ -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
index 00000000000..09161aa4911
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/package-info.java
@@ -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;
+
diff --git a/sonar-ws/src/main/protobuf/ws-commons.proto b/sonar-ws/src/main/protobuf/ws-commons.proto
index 58b57986c78..efd2fee3ca4 100644
--- a/sonar-ws/src/main/protobuf/ws-commons.proto
+++ b/sonar-ws/src/main/protobuf/ws-commons.proto
@@ -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
index 00000000000..395484609a2
--- /dev/null
+++ b/sonar-ws/src/main/protobuf/ws-measures.proto
@@ -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;
+}
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java
index 3ba2e88aef4..01f0019f3b8 100644
--- a/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java
+++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java
@@ -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
index 00000000000..c41080a642d
--- /dev/null
+++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/measure/MeasuresServiceTest.java
@@ -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();
+ }
+}