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