summaryrefslogtreecommitdiffstats
path: root/server
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 /server
parent49b3b0bc394ce675356d832e1d12459b9be543bd (diff)
downloadsonarqube-0242e1da3fea5a96a9f0632156b1cacdd89b9ace.tar.gz
sonarqube-0242e1da3fea5a96a9f0632156b1cacdd89b9ace.zip
SONAR-7135 WS api/measures/component_tree navigate through components and display measures
Diffstat (limited to 'server')
-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
20 files changed, 2373 insertions, 4 deletions
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");
+ }
+}