diff options
author | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2016-01-06 17:46:38 +0100 |
---|---|---|
committer | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2016-01-13 14:13:35 +0100 |
commit | 0242e1da3fea5a96a9f0632156b1cacdd89b9ace (patch) | |
tree | 68166c8f6ed52713ba24b5d94e9fe00616491e05 /server | |
parent | 49b3b0bc394ce675356d832e1d12459b9be543bd (diff) | |
download | sonarqube-0242e1da3fea5a96a9f0632156b1cacdd89b9ace.tar.gz sonarqube-0242e1da3fea5a96a9f0632156b1cacdd89b9ace.zip |
SONAR-7135 WS api/measures/component_tree navigate through components and display measures
Diffstat (limited to 'server')
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"); + } +} |