aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>2016-09-26 12:11:12 +0200
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>2016-09-30 14:16:55 +0200
commit2bbfd0c6ca169d20ae2d51e7cbb029ad3f9ea73b (patch)
treeb942ab1833e9f3bf51234cd24fbcfdaccd7e93c5
parent8492c97d913d9aa4330975017b68200977a02123 (diff)
downloadsonarqube-2bbfd0c6ca169d20ae2d51e7cbb029ad3f9ea73b.tar.gz
sonarqube-2bbfd0c6ca169d20ae2d51e7cbb029ad3f9ea73b.zip
SONAR-8120 Create WS measures/search to search for measures
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentDtoToWsComponent.java4
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java3
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java214
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java2
-rw-r--r--server/sonar-server/src/main/resources/org/sonar/server/measure/ws/search-example.json128
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java2
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java312
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java1
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/measure/SearchRequest.java96
-rw-r--r--sonar-ws/src/main/protobuf/ws-measures.proto5
-rw-r--r--sonar-ws/src/test/java/org/sonarqube/ws/client/measure/SearchRequestTest.java123
11 files changed, 888 insertions, 2 deletions
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
index ea97045cd0c..f0d8356d63b 100644
--- 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
@@ -49,6 +49,10 @@ class ComponentDtoToWsComponent {
return wsComponent;
}
+ static WsMeasures.Component dbToWsComponent(ComponentDto dbComponent, Iterable<WsMeasures.Measure> measures) {
+ return componentDtoToWsComponent(dbComponent).addAllMeasures(measures).build();
+ }
+
static WsMeasures.Component.Builder componentDtoToWsComponent(ComponentDto component) {
WsMeasures.Component.Builder wsComponent = WsMeasures.Component.newBuilder()
.setId(component.uuid())
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
index 23d6b832814..eeac683913a 100644
--- 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
@@ -29,6 +29,7 @@ public class MeasuresWsModule extends Module {
ComponentTreeDataLoader.class,
MeasuresWs.class,
ComponentTreeAction.class,
- ComponentAction.class);
+ ComponentAction.class,
+ SearchAction.class);
}
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java
new file mode 100644
index 00000000000..8d290cfa9b0
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java
@@ -0,0 +1,214 @@
+/*
+ * SonarQube
+ * 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.ImmutableMultimap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.measure.MeasureQuery;
+import org.sonar.db.metric.MetricDto;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.SearchWsResponse;
+import org.sonarqube.ws.client.measure.SearchRequest;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Maps.immutableEntry;
+import static java.util.Objects.requireNonNull;
+import static java.util.function.Function.identity;
+import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
+import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02;
+import static org.sonar.core.util.Uuids.UUID_EXAMPLE_03;
+import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.dbToWsComponent;
+import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.measureDtoToWsMeasure;
+import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
+import static org.sonar.server.ws.KeyExamples.KEY_FILE_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_FILE_EXAMPLE_002;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_002;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS;
+
+public class SearchAction implements MeasuresWsAction {
+ private static final int MAX_NB_COMPONENTS = 100;
+ static final String PARAM_COMPONENT_IDS = "componentIds";
+ static final String PARAM_COMPONENT_KEYS = "componentKeys";
+
+ private final DbClient dbClient;
+
+ public SearchAction(DbClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public void define(WebService.NewController context) {
+ WebService.NewAction action = context.createAction("search")
+ .setInternal(true)
+ .setDescription("Search for component measures ordered by component names.<br>" +
+ "At most %d components can be provided.<br>" +
+ "Either '%s' or '%s' must be provided, not both.<br>" +
+ "Requires one of the following permissions:" +
+ "<ul>" +
+ " <li>'Administer System'</li>" +
+ " <li>'Administer' rights on the provided components</li>" +
+ " <li>'Browse' on the provided components</li>" +
+ "</ul>",
+ MAX_NB_COMPONENTS, PARAM_COMPONENT_IDS, PARAM_COMPONENT_KEYS)
+ .setSince("6.1")
+ .setResponseExample(getClass().getResource("search-example.json"))
+ .setHandler(this);
+
+ createMetricKeysParameter(action);
+
+ action.createParam(PARAM_COMPONENT_IDS)
+ .setDescription("Comma-separated list of component ids")
+ .setExampleValue(String.join(",", UUID_EXAMPLE_01, UUID_EXAMPLE_02, UUID_EXAMPLE_03));
+
+ action.createParam(PARAM_COMPONENT_KEYS)
+ .setDescription("Comma-separated list of component keys")
+ .setExampleValue(String.join(",", KEY_PROJECT_EXAMPLE_001, KEY_FILE_EXAMPLE_001, KEY_PROJECT_EXAMPLE_002, KEY_FILE_EXAMPLE_002));
+
+ }
+
+ @Override
+ public void handle(Request httpRequest, Response httpResponse) throws Exception {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ SearchWsResponse response = new ResponseBuilder(httpRequest, dbSession).build();
+ writeProtobuf(response, httpRequest, httpResponse);
+ }
+
+ }
+
+ private class ResponseBuilder {
+ private final DbSession dbSession;
+ private final Request httpRequest;
+ private SearchRequest request;
+ private List<ComponentDto> components;
+ private List<MetricDto> metrics;
+ private List<MeasureDto> measures;
+
+ ResponseBuilder(Request httpRequest, DbSession dbSession) {
+ this.dbSession = dbSession;
+ this.httpRequest = httpRequest;
+ }
+
+ SearchWsResponse build() {
+ this.request = setRequest();
+ this.components = searchComponents();
+ this.metrics = searchMetrics();
+ this.measures = searchMeasures();
+
+ return buildResponse();
+ }
+
+ private SearchRequest setRequest() {
+ request = SearchRequest.builder()
+ .setMetricKeys(httpRequest.mandatoryParamAsStrings(PARAM_METRIC_KEYS))
+ .setComponentIds(httpRequest.paramAsStrings(PARAM_COMPONENT_IDS))
+ .setComponentKeys(httpRequest.paramAsStrings(PARAM_COMPONENT_KEYS))
+ .build();
+
+ this.components = searchComponents();
+ this.metrics = searchMetrics();
+
+ return request;
+ }
+
+ private List<MetricDto> searchMetrics() {
+ requireNonNull(request);
+ return dbClient.metricDao().selectByKeys(dbSession, request.getMetricKeys());
+ }
+
+ private List<ComponentDto> searchComponents() {
+ requireNonNull(request);
+ if (request.hasComponentIds()) {
+ List<ComponentDto> componentsByUuid = searchByComponentUuids(dbSession, request.getComponentIds());
+ checkArgument(componentsByUuid.size() == request.getComponentIds().size(), "Some components are not found in: '%s'", String.join(", ", request.getComponentIds()));
+ return componentsByUuid;
+ } else {
+ List<ComponentDto> componentsByKey = searchByComponentKeys(dbSession, request.getComponentKeys());
+ checkArgument(componentsByKey.size() == request.getComponentKeys().size(), "Some components are not found in: '%s'", String.join(", ", request.getComponentKeys()));
+ return componentsByKey;
+ }
+ }
+
+ private List<ComponentDto> searchByComponentUuids(DbSession dbSession, List<String> componentUuids) {
+ return dbClient.componentDao().selectByUuids(dbSession, componentUuids);
+ }
+
+ private List<ComponentDto> searchByComponentKeys(DbSession dbSession, List<String> componentKeys) {
+ return dbClient.componentDao().selectByKeys(dbSession, componentKeys);
+ }
+
+ private List<MeasureDto> searchMeasures() {
+ requireNonNull(components);
+ requireNonNull(metrics);
+
+ return dbClient.measureDao().selectByQuery(dbSession, MeasureQuery.builder()
+ .setComponentUuids(components.stream().map(ComponentDto::uuid).collect(Collectors.toList()))
+ .setMetricIds(metrics.stream().map(MetricDto::getId).collect(Collectors.toList()))
+ .build());
+ }
+
+ private SearchWsResponse buildResponse() {
+ requireNonNull(metrics);
+ requireNonNull(measures);
+ requireNonNull(components);
+
+ Map<Integer, MetricDto> metricById = metrics.stream().collect(Collectors.toMap(MetricDto::getId, identity()));
+
+ ImmutableMultimap<String, WsMeasures.Measure> wsMeasuresByComponentUuid = measures.stream()
+ .collect(Collectors.toMap(identity(), MeasureDto::getComponentUuid))
+ .entrySet().stream()
+ .map(entry -> immutableEntry(
+ measureDtoToWsMeasure(metricById.get(entry.getKey().getMetricId()), entry.getKey()),
+ entry.getValue()))
+ .sorted((e1, e2) -> e1.getKey().getMetric().compareTo(e2.getKey().getMetric()))
+ .collect(Collector.of(
+ ImmutableMultimap::<String, WsMeasures.Measure>builder,
+ (result, entry) -> result.put(entry.getValue(), entry.getKey()),
+ (result1, result2) -> {
+ throw new IllegalStateException("Parallel execution forbidden during WS measures");
+ },
+ ImmutableMultimap.Builder::build));
+
+ return components.stream()
+ .map(dbComponent -> dbToWsComponent(dbComponent, wsMeasuresByComponentUuid.get(dbComponent.uuid())))
+ .sorted((c1, c2) -> c1.getName().compareTo(c2.getName()))
+ .collect(Collector.of(
+ SearchWsResponse::newBuilder,
+ SearchWsResponse.Builder::addComponents,
+ (result1, result2) -> {
+ throw new IllegalStateException("Parallel execution forbidden while build SearchWsResponse");
+ },
+ SearchWsResponse.Builder::build));
+ }
+
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java b/server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java
index 72ecdebd799..d017bb1c013 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java
@@ -21,7 +21,9 @@ package org.sonar.server.ws;
public class KeyExamples {
public static final String KEY_FILE_EXAMPLE_001 = "my_project:/src/foo/Bar.php";
+ public static final String KEY_FILE_EXAMPLE_002 = "another_project:/src/foo/Foo.php";
public static final String KEY_PROJECT_EXAMPLE_001 = "my_project";
+ public static final String KEY_PROJECT_EXAMPLE_002 = "another_project";
public static final String KEY_DEVELOPER_EXAMPLE_001 = "DEV:ada@lovelace.com";
private KeyExamples() {
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/search-example.json b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/search-example.json
new file mode 100644
index 00000000000..57d4989debb
--- /dev/null
+++ b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/search-example.json
@@ -0,0 +1,128 @@
+{
+ "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",
+ "language": "java",
+ "measures": [
+ {
+ "metric": "complexity",
+ "value": "12"
+ },
+ {
+ "metric": "ncloc",
+ "value": "114"
+ },
+ {
+ "metric": "new_violations",
+ "periods": [
+ {
+ "index": 1,
+ "value": "25"
+ },
+ {
+ "index": 2,
+ "value": "0"
+ },
+ {
+ "index": 3,
+ "value": "25"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "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",
+ "language": "java",
+ "measures": []
+ },
+ {
+ "id": "project-id",
+ "key": "MY_PROJECT",
+ "name": "My Project",
+ "description": "My Project Description",
+ "qualifier": "TRK",
+ "measures": [
+ {
+ "metric": "complexity",
+ "value": "42"
+ },
+ {
+ "metric": "ncloc",
+ "value": "1984"
+ },
+ {
+ "metric": "new_violations",
+ "periods": [
+ {
+ "index": 1,
+ "value": "255"
+ },
+ {
+ "index": 2,
+ "value": "0"
+ },
+ {
+ "index": 3,
+ "value": "255"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "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": "complexity",
+ "value": "35",
+ "periods": [
+ {
+ "index": 2,
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "metric": "ncloc",
+ "value": "217",
+ "periods": [
+ {
+ "index": 2,
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "metric": "new_violations",
+ "periods": [
+ {
+ "index": 1,
+ "value": "25"
+ },
+ {
+ "index": 2,
+ "value": "0"
+ },
+ {
+ "index": 3,
+ "value": "25"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
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
index 67e60760bba..c5a2b957d11 100644
--- 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
@@ -29,6 +29,6 @@ public class MeasuresWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new MeasuresWsModule().configure(container);
- assertThat(container.size()).isEqualTo(4 + 2);
+ assertThat(container.size()).isEqualTo(5 + 2);
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java
new file mode 100644
index 00000000000..9563cf615d5
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java
@@ -0,0 +1,312 @@
+/*
+ * SonarQube
+ * 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.util.List;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+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.SnapshotDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.WsMeasures.Component;
+import org.sonarqube.ws.WsMeasures.SearchWsResponse;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.utils.DateUtils.parseDateTime;
+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.newAnalysis;
+import static org.sonar.db.measure.MeasureTesting.newMeasureDto;
+import static org.sonar.db.metric.MetricTesting.newMetricDto;
+import static org.sonar.server.measure.ws.SearchAction.PARAM_COMPONENT_IDS;
+import static org.sonar.server.measure.ws.SearchAction.PARAM_COMPONENT_KEYS;
+import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.MediaTypes.PROTOBUF;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS;
+
+public class SearchActionTest {
+
+ @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 SearchAction(dbClient));
+
+ @Test
+ public void json_example() {
+ insertJsonExampleData();
+
+ String result = ws.newRequest()
+ .setParam(PARAM_COMPONENT_IDS, "project-id,AVIwDXE-bJbJqrw6wFv5,AVIwDXE-bJbJqrw6wFv8,AVIwDXE_bJbJqrw6wFwJ")
+ .setParam(PARAM_METRIC_KEYS, "ncloc, complexity, new_violations")
+ .execute()
+ .getInput();
+
+ assertJson(result).withStrictArrayOrder().isSimilarTo(ws.getDef().responseExampleAsString());
+
+ }
+
+ @Test
+ public void project_without_measures_map_all_fields() {
+ ComponentDto dbComponent = componentDb.insertComponent(newProjectDto());
+ insertComplexityMetric();
+
+ SearchWsResponse result = callByComponentUuids(singletonList(dbComponent.uuid()), singletonList("complexity"));
+
+ assertThat(result.getComponentsCount()).isEqualTo(1);
+ Component wsComponent = result.getComponents(0);
+ assertThat(wsComponent.getMeasuresCount()).isEqualTo(0);
+ assertThat(wsComponent.getId()).isEqualTo(dbComponent.uuid());
+ assertThat(wsComponent.getKey()).isEqualTo(dbComponent.key());
+ assertThat(wsComponent.getQualifier()).isEqualTo(dbComponent.qualifier());
+ assertThat(wsComponent.getName()).isEqualTo(dbComponent.name());
+ assertThat(wsComponent.getDescription()).isEqualTo(dbComponent.description());
+ assertThat(wsComponent.getProjectId()).isEqualTo("");
+ assertThat(wsComponent.getLanguage()).isEqualTo("");
+ assertThat(wsComponent.getPath()).isEqualTo("");
+ assertThat(wsComponent.getRefId()).isEqualTo("");
+ assertThat(wsComponent.getRefKey()).isEqualTo("");
+ }
+
+ @Test
+ public void search_by_component_key() {
+ ComponentDto project = componentDb.insertProject();
+ insertComplexityMetric();
+
+ SearchWsResponse result = callByComponentKeys(singletonList(project.key()), singletonList("complexity"));
+
+ assertThat(result.getComponentsCount()).isEqualTo(1);
+ assertThat(result.getComponents(0).getId()).isEqualTo(project.uuid());
+ }
+
+ @Test
+ public void definition() {
+ WebService.Action result = ws.getDef();
+
+ assertThat(result.key()).isEqualTo("search");
+ assertThat(result.isPost()).isFalse();
+ assertThat(result.isInternal()).isTrue();
+ assertThat(result.since()).isEqualTo("6.1");
+ assertThat(result.params()).hasSize(3);
+ assertThat(result.responseExampleAsString()).isNotEmpty();
+ assertThat(result.description()).isEqualToIgnoringWhitespace("" +
+ "Search for component measures ordered by component names.<br>" +
+ "At most 100 components can be provided.<br>" +
+ "Either 'componentIds' or 'componentKeys' must be provided, not both.<br>" +
+ "Requires one of the following permissions:" +
+ "<ul>" +
+ " <li>'Administer System'</li>" +
+ " <li>'Administer' rights on the provided components</li>" +
+ " <li>'Browse' on the provided components</li>" +
+ "</ul>");
+ }
+
+ private SearchWsResponse callByComponentUuids(@Nullable List<String> uuids, @Nullable List<String> metrics) {
+ return call(uuids, null, metrics);
+ }
+
+ private SearchWsResponse callByComponentKeys(@Nullable List<String> keys, @Nullable List<String> metrics) {
+ return call(null, keys, metrics);
+ }
+
+ private SearchWsResponse call(@Nullable List<String> uuids, @Nullable List<String> keys, @Nullable List<String> metrics) {
+ TestRequest request = ws.newRequest()
+ .setMediaType(PROTOBUF);
+
+ if (uuids != null) {
+ request.setParam(PARAM_COMPONENT_IDS, String.join(",", uuids));
+ }
+ if (keys != null) {
+ request.setParam(PARAM_COMPONENT_KEYS, String.join(",", keys));
+ }
+ if (metrics != null) {
+ request.setParam(PARAM_METRIC_KEYS, String.join(",", metrics));
+ }
+
+ try {
+ return SearchWsResponse.parseFrom(request.execute().getInputStream());
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ private static MetricDto newMetricDtoWithoutOptimization() {
+ return newMetricDto()
+ .setWorstValue(null)
+ .setBestValue(null)
+ .setOptimizedBestValue(false)
+ .setUserManaged(false);
+ }
+
+ 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)
+ .setOptimizedBestValue(true)
+ .setBestValue(0.0d));
+ 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(Metric.ValueType.FLOAT.name())
+ .setDirection(1)
+ .setQualitative(false)
+ .setHidden(false)
+ .setUserManaged(false));
+ db.commit();
+ return metric;
+ }
+
+ private void insertJsonExampleData() {
+ ComponentDto project = newProjectDto("project-id")
+ .setKey("MY_PROJECT")
+ .setName("My Project")
+ .setDescription("My Project Description")
+ .setQualifier(Qualifiers.PROJECT);
+ componentDb.insertComponent(project);
+ SnapshotDto projectSnapshot = dbClient.snapshotDao().insert(dbSession, newAnalysis(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"));
+
+ ComponentDto file1 = componentDb.insertComponent(newFileDto(project, null)
+ .setUuid("AVIwDXE-bJbJqrw6wFv5")
+ .setKey("com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/ElementImpl.java")
+ .setName("ElementImpl.java")
+ .setLanguage("java")
+ .setQualifier(Qualifiers.FILE)
+ .setPath("src/main/java/com/sonarsource/markdown/impl/ElementImpl.java"));
+ componentDb.insertComponent(newFileDto(project, null)
+ .setUuid("AVIwDXE_bJbJqrw6wFwJ")
+ .setKey("com.sonarsource:java-markdown:src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java")
+ .setName("ElementImplTest.java")
+ .setLanguage("java")
+ .setQualifier(Qualifiers.UNIT_TEST_FILE)
+ .setPath("src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java"));
+ ComponentDto dir = componentDb.insertComponent(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));
+
+ MetricDto complexity = insertComplexityMetric();
+ dbClient.measureDao().insert(dbSession,
+ newMeasureDto(complexity, file1, projectSnapshot)
+ .setValue(12.0d),
+ newMeasureDto(complexity, dir, projectSnapshot)
+ .setValue(35.0d)
+ .setVariation(2, 0.0d),
+ newMeasureDto(complexity, project, projectSnapshot)
+ .setValue(42.0d));
+
+ MetricDto ncloc = insertNclocMetric();
+ dbClient.measureDao().insert(dbSession,
+ newMeasureDto(ncloc, file1, projectSnapshot)
+ .setValue(114.0d),
+ newMeasureDto(ncloc, dir, projectSnapshot)
+ .setValue(217.0d)
+ .setVariation(2, 0.0d),
+ newMeasureDto(ncloc, project, projectSnapshot)
+ .setValue(1984.0d));
+
+ MetricDto newViolations = insertNewViolationsMetric();
+ dbClient.measureDao().insert(dbSession,
+ newMeasureDto(newViolations, file1, projectSnapshot)
+ .setVariation(1, 25.0d)
+ .setVariation(2, 0.0d)
+ .setVariation(3, 25.0d),
+ newMeasureDto(newViolations, dir, projectSnapshot)
+ .setVariation(1, 25.0d)
+ .setVariation(2, 0.0d)
+ .setVariation(3, 25.0d),
+ newMeasureDto(newViolations, project, projectSnapshot)
+ .setVariation(1, 255.0d)
+ .setVariation(2, 0.0d)
+ .setVariation(3, 255.0d));
+
+ db.commit();
+ }
+}
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 2116b81f5d2..e1b2f364b3b 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
@@ -100,6 +100,7 @@ public class ComponentTesting {
.setKey("KEY_" + uuid)
.setName("NAME_" + uuid)
.setLongName("LONG_NAME_" + uuid)
+ .setDescription("DESCRIPTION_" + uuid)
.setScope(Scopes.PROJECT)
.setQualifier(Qualifiers.PROJECT)
.setPath(null)
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/SearchRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/SearchRequest.java
new file mode 100644
index 00000000000..ad16a8c3a5b
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/measure/SearchRequest.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * 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 static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+public class SearchRequest {
+ private final List<String> metricKeys;
+ private final List<String> componentIds;
+ private final List<String> componentKeys;
+
+ public SearchRequest(Builder builder) {
+ metricKeys = builder.metricKeys;
+ componentIds = builder.componentIds;
+ componentKeys = builder.componentKeys;
+ }
+
+ public List<String> getMetricKeys() {
+ return metricKeys;
+ }
+
+ public boolean hasComponentIds() {
+ return componentIds != null;
+ }
+
+ public List<String> getComponentIds() {
+ return requireNonNull(componentIds, "No component id in request");
+ }
+
+ public boolean hasComponentKeys() {
+ return componentKeys != null;
+ }
+
+ public List<String> getComponentKeys() {
+ return requireNonNull(componentKeys, "No component key in request");
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private List<String> metricKeys;
+ private List<String> componentIds;
+ private List<String> componentKeys;
+
+ private Builder() {
+ // enforce method constructor
+ }
+
+ public Builder setMetricKeys(List<String> metricKeys) {
+ this.metricKeys = metricKeys;
+ return this;
+ }
+
+ public Builder setComponentIds(List<String> componentIds) {
+ this.componentIds = componentIds;
+ return this;
+ }
+
+ public Builder setComponentKeys(List<String> componentKeys) {
+ this.componentKeys = componentKeys;
+ return this;
+ }
+
+ public SearchRequest build() {
+ checkArgument(metricKeys != null && !metricKeys.isEmpty(), "Metric keys must be provided");
+ checkArgument(
+ (componentIds != null && !componentIds.isEmpty())
+ ^ (componentKeys != null && !componentKeys.isEmpty()),
+ "Either component ids or component keys must be provided, not both.");
+ return new SearchRequest(this);
+ }
+ }
+}
diff --git a/sonar-ws/src/main/protobuf/ws-measures.proto b/sonar-ws/src/main/protobuf/ws-measures.proto
index 2ade7c75b56..b29ffdf8c12 100644
--- a/sonar-ws/src/main/protobuf/ws-measures.proto
+++ b/sonar-ws/src/main/protobuf/ws-measures.proto
@@ -42,6 +42,11 @@ message ComponentWsResponse {
optional Periods periods = 3;
}
+// WS api/measures/search
+message SearchWsResponse {
+ repeated Component components = 1;
+}
+
message Component {
optional string id = 1;
optional string key = 2;
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/measure/SearchRequestTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/measure/SearchRequestTest.java
new file mode 100644
index 00000000000..7cffedb9471
--- /dev/null
+++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/measure/SearchRequestTest.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * 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.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SearchRequestTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ SearchRequest.Builder underTest = SearchRequest.builder();
+
+ @Test
+ public void with_component_ids() {
+ SearchRequest result = underTest
+ .setMetricKeys(singletonList("metric"))
+ .setComponentIds(singletonList("uuid"))
+ .build();
+
+ assertThat(result.getMetricKeys()).containsExactly("metric");
+ assertThat(result.getComponentIds()).containsExactly("uuid");
+ assertThat(result.hasComponentKeys()).isFalse();
+ }
+
+ @Test
+ public void with_component_keys() {
+ SearchRequest result = underTest
+ .setMetricKeys(singletonList("metric"))
+ .setComponentKeys(singletonList("key"))
+ .build();
+
+ assertThat(result.getMetricKeys()).containsExactly("metric");
+ assertThat(result.getComponentKeys()).containsExactly("key");
+ assertThat(result.hasComponentIds()).isFalse();
+ }
+
+ @Test
+ public void fail_when_non_null_metric_keys() {
+ expectExceptionOnMetricKeys();
+
+ underTest.setMetricKeys(null).build();
+ }
+
+ @Test
+ public void fail_when_non_empty_metric_keys() {
+ expectExceptionOnMetricKeys();
+
+ underTest.setMetricKeys(emptyList()).build();
+ }
+
+ @Test
+ public void fail_when_unset_metric_keys() {
+ expectExceptionOnMetricKeys();
+
+ underTest.build();
+ }
+
+ @Test
+ public void fail_when_component_ids_and_keys_provided() {
+ expectExceptionOnComponents();
+
+ underTest
+ .setMetricKeys(singletonList("metric"))
+ .setComponentIds(singletonList("uuid"))
+ .setComponentKeys(singletonList("key"))
+ .build();
+ }
+
+ @Test
+ public void fail_when_component_ids_is_empty() {
+ expectExceptionOnComponents();
+
+ underTest
+ .setMetricKeys(singletonList("metric"))
+ .setComponentIds(emptyList())
+ .build();
+ }
+
+ @Test
+ public void fail_when_component_keys_is_empty() {
+ expectExceptionOnComponents();
+
+ underTest
+ .setMetricKeys(singletonList("metric"))
+ .setComponentKeys(emptyList())
+ .build();
+ }
+
+ private void expectExceptionOnMetricKeys() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Metric keys must be provided");
+ }
+
+ private void expectExceptionOnComponents() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Either component ids or component keys must be provided, not both.");
+ }
+}