aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2017-02-16 16:55:13 +0100
committerJulien Lancelot <julien.lancelot@sonarsource.com>2017-02-17 09:27:54 +0100
commitc00a059069b34fc14716a5fb40f6eaa5a2cddfe3 (patch)
treecc0f2a5fe2a2440f5b5fbfcdbcbd5a0031cb970f
parentb4b1940277e877c853fbe2b32696c3cb1d50f816 (diff)
downloadsonarqube-c00a059069b34fc14716a5fb40f6eaa5a2cddfe3.tar.gz
sonarqube-c00a059069b34fc14716a5fb40f6eaa5a2cddfe3.zip
SONAR-8804 Create api/projects/search
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java3
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java153
-rw-r--r--server/sonar-server/src/main/resources/org/sonar/server/project/ws/search-example.json23
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java2
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/project/ws/SearchActionTest.java292
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java19
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java3
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java109
-rw-r--r--sonar-ws/src/main/protobuf/ws-projects.proto14
-rw-r--r--sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java23
10 files changed, 638 insertions, 3 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java
index ee421e8213c..d3c00cf99ed 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java
@@ -34,6 +34,7 @@ public class ProjectsWsModule extends Module {
GhostsAction.class,
ProvisionedAction.class,
SearchMyProjectsAction.class,
- SearchMyProjectsDataLoader.class);
+ SearchMyProjectsDataLoader.class,
+ SearchAction.class);
}
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java
new file mode 100644
index 00000000000..1d7aa790a68
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info 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.project.ws;
+
+import java.util.List;
+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.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentQuery;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.WsProjects.SearchWsResponse;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Optional.ofNullable;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.api.resources.Qualifiers.VIEW;
+import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonarqube.ws.WsProjects.SearchWsResponse.Component;
+import static org.sonarqube.ws.WsProjects.SearchWsResponse.newBuilder;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_SEARCH;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
+
+public class SearchAction implements ProjectsWsAction {
+
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final DefaultOrganizationProvider defaultOrganizationProvider;
+ private final ProjectsWsSupport support;
+
+ public SearchAction(DbClient dbClient, UserSession userSession, DefaultOrganizationProvider defaultOrganizationProvider, ProjectsWsSupport support) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.defaultOrganizationProvider = defaultOrganizationProvider;
+ this.support = support;
+ }
+
+ @Override
+ public void define(WebService.NewController context) {
+ WebService.NewAction action = context.createAction(ACTION_SEARCH)
+ .setSince("6.3")
+ .setDescription("Search for projects or views.<br>" +
+ "Requires 'System Administrator' permission")
+ .setInternal(true)
+ .addPagingParams(100)
+ .addSearchQuery("sona", "component names", "component keys")
+ .setResponseExample(getClass().getResource("search-example.json"))
+ .setHandler(this);
+ action.createParam(PARAM_QUALIFIERS)
+ .setDescription("Comma-separated list of component qualifiers. Filter the results with the specified qualifiers")
+ .setPossibleValues(PROJECT, VIEW)
+ .setDefaultValue(PROJECT);
+ support.addOrganizationParam(action);
+ }
+
+ @Override
+ public void handle(Request wsRequest, Response wsResponse) throws Exception {
+ SearchWsResponse searchWsResponse = doHandle(toSearchWsRequest(wsRequest));
+ writeProtobuf(searchWsResponse, wsRequest, wsResponse);
+ }
+
+ private static SearchWsRequest toSearchWsRequest(Request request) {
+ return SearchWsRequest.builder()
+ .setOrganization(request.param(PARAM_ORGANIZATION))
+ .setQualifiers(request.mandatoryParamAsStrings(PARAM_QUALIFIERS))
+ .setQuery(request.param(Param.TEXT_QUERY))
+ .setPage(request.mandatoryParamAsInt(Param.PAGE))
+ .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE)).build();
+ }
+
+ private SearchWsResponse doHandle(SearchWsRequest request) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ OrganizationDto organization = support.getOrganization(dbSession, ofNullable(request.getOrganization()).orElseGet(defaultOrganizationProvider.get()::getKey));
+ userSession.checkOrganizationPermission(organization.getUuid(), SYSTEM_ADMIN);
+
+ ComponentQuery query = buildQuery(request);
+ Paging paging = buildPaging(dbSession, request, organization, query);
+ List<ComponentDto> components = dbClient.componentDao().selectByQuery(dbSession, organization.getUuid(), query, paging.offset(), paging.pageSize());
+ return buildResponse(components, organization, paging);
+ }
+ }
+
+ private static ComponentQuery buildQuery(SearchWsRequest request) {
+ List<String> qualifiers = request.getQualifiers();
+ return ComponentQuery.builder()
+ .setNameOrKeyQuery(request.getQuery())
+ .setQualifiers(qualifiers.toArray(new String[qualifiers.size()]))
+ .build();
+ }
+
+ private Paging buildPaging(DbSession dbSession, SearchWsRequest request, OrganizationDto organization, ComponentQuery query) {
+ int total = dbClient.componentDao().countByQuery(dbSession, organization.getUuid(), query);
+ return Paging.forPageIndex(request.getPage())
+ .withPageSize(request.getPageSize())
+ .andTotal(total);
+ }
+
+ private static SearchWsResponse buildResponse(List<ComponentDto> components, OrganizationDto organization, Paging paging) {
+ SearchWsResponse.Builder responseBuilder = newBuilder();
+ responseBuilder.getPagingBuilder()
+ .setPageIndex(paging.pageIndex())
+ .setPageSize(paging.pageSize())
+ .setTotal(paging.total())
+ .build();
+
+ components.stream()
+ .map(dto -> dtoToProject(organization, dto))
+ .forEach(responseBuilder::addComponents);
+ return responseBuilder.build();
+ }
+
+ private static Component dtoToProject(OrganizationDto organization, ComponentDto dto) {
+ checkArgument(
+ organization.getUuid().equals(dto.getOrganizationUuid()),
+ "No Organization found for uuid '%s'",
+ dto.getOrganizationUuid());
+
+ Component.Builder builder = Component.newBuilder()
+ .setOrganization(organization.getKey())
+ .setId(dto.uuid())
+ .setKey(dto.key())
+ .setName(dto.name())
+ .setQualifier(dto.qualifier());
+ return builder.build();
+ }
+
+}
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/project/ws/search-example.json b/server/sonar-server/src/main/resources/org/sonar/server/project/ws/search-example.json
new file mode 100644
index 00000000000..fca854523c0
--- /dev/null
+++ b/server/sonar-server/src/main/resources/org/sonar/server/project/ws/search-example.json
@@ -0,0 +1,23 @@
+{
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 100,
+ "total": 2
+ },
+ "components": [
+ {
+ "organization": "my-org-1",
+ "id": "project-uuid-1",
+ "key": "project-key-1",
+ "name": "Project Name 1",
+ "qualifier": "TRK"
+ },
+ {
+ "organization": "my-org-1",
+ "id": "project-uuid-2",
+ "key": "project-key-2",
+ "name": "Project Name 1",
+ "qualifier": "TRK"
+ }
+ ]
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java
index e6c03e41ebf..abd0abd5105 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java
@@ -30,6 +30,6 @@ public class ProjectsWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new ProjectsWsModule().configure(container);
- assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 10);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 11);
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/SearchActionTest.java
new file mode 100644
index 00000000000..9b62ee686fd
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/SearchActionTest.java
@@ -0,0 +1,292 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info 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.project.ws;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.WsProjects.SearchWsResponse;
+import org.sonarqube.ws.WsProjects.SearchWsResponse.Component;
+import org.sonarqube.ws.client.component.ComponentsWsParameters;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.server.ws.WebService.Param.PAGE;
+import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
+import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
+import static org.sonar.core.permission.GlobalPermissions.QUALITY_PROFILE_ADMIN;
+import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.db.component.ComponentTesting.newDirectory;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.component.ComponentTesting.newModuleDto;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.db.component.ComponentTesting.newView;
+import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.MediaTypes.PROTOBUF;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
+
+public class SearchActionTest {
+
+ private static final String PROJECT_KEY_1 = "project1";
+ private static final String PROJECT_KEY_2 = "project2";
+ private static final String PROJECT_KEY_3 = "project3";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ @Rule
+ public DbTester db = DbTester.create();
+
+ private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
+
+ private WsActionTester ws = new WsActionTester(new SearchAction(db.getDbClient(), userSession, defaultOrganizationProvider, new ProjectsWsSupport(db.getDbClient())));
+
+ @Test
+ public void search_by_key_query() throws IOException {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), SYSTEM_ADMIN);
+ db.components().insertComponents(
+ newProjectDto(db.getDefaultOrganization()).setKey("project-_%-key"),
+ newProjectDto(db.getDefaultOrganization()).setKey("project-key-without-escaped-characters"));
+
+ SearchWsResponse response = call(SearchWsRequest.builder().setQuery("project-_%-key").build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly("project-_%-key");
+ }
+
+ @Test
+ public void search_projects_when_no_qualifier_set() throws IOException {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), SYSTEM_ADMIN);
+ db.components().insertComponents(
+ newProjectDto(db.getDefaultOrganization()).setKey(PROJECT_KEY_1),
+ newView(db.getDefaultOrganization()));
+
+ SearchWsResponse response = call(SearchWsRequest.builder().build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1);
+ }
+
+ @Test
+ public void search_projects() throws IOException {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), SYSTEM_ADMIN);
+ ComponentDto project = newProjectDto(db.getDefaultOrganization()).setKey(PROJECT_KEY_1);
+ ComponentDto module = newModuleDto(project);
+ ComponentDto directory = newDirectory(module, "dir");
+ ComponentDto file = newFileDto(directory);
+ db.components().insertComponents(
+ project, module, directory, file,
+ newProjectDto(db.getDefaultOrganization()).setKey(PROJECT_KEY_2),
+ newView(db.getDefaultOrganization()));
+
+ SearchWsResponse response = call(SearchWsRequest.builder().setQualifiers(singletonList("TRK")).build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1, PROJECT_KEY_2);
+ }
+
+ @Test
+ public void search_views() throws IOException {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), SYSTEM_ADMIN);
+ db.components().insertComponents(
+ newProjectDto(db.getDefaultOrganization()).setKey(PROJECT_KEY_1),
+ newView(db.getDefaultOrganization()).setKey("view1"));
+
+ SearchWsResponse response = call(SearchWsRequest.builder().setQualifiers(singletonList("VW")).build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly("view1");
+ }
+
+ @Test
+ public void search_projects_and_views() throws IOException {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), SYSTEM_ADMIN);
+ db.components().insertComponents(
+ newProjectDto(db.getDefaultOrganization()).setKey(PROJECT_KEY_1),
+ newView(db.getDefaultOrganization()).setKey("view1"));
+
+ SearchWsResponse response = call(SearchWsRequest.builder().setQualifiers(asList("TRK", "VW")).build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1, "view1");
+ }
+
+ @Test
+ public void search_on_default_organization_when_no_organization_set() throws IOException {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), SYSTEM_ADMIN);
+ OrganizationDto otherOrganization = db.organizations().insert();
+ db.components().insertComponents(
+ newProjectDto(db.getDefaultOrganization()).setKey(PROJECT_KEY_1),
+ newProjectDto(db.getDefaultOrganization()).setKey(PROJECT_KEY_2),
+ newProjectDto(otherOrganization).setKey(PROJECT_KEY_3));
+
+ SearchWsResponse response = call(SearchWsRequest.builder().build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1, PROJECT_KEY_2);
+ }
+
+ @Test
+ public void search_for_projects_on_given_organization() throws IOException {
+ OrganizationDto organization1 = db.organizations().insert();
+ OrganizationDto organization2 = db.organizations().insert();
+ userSession.addOrganizationPermission(organization1, SYSTEM_ADMIN);
+ ComponentDto project1 = newProjectDto(organization1);
+ ComponentDto project2 = newProjectDto(organization1);
+ ComponentDto project3 = newProjectDto(organization2);
+ db.components().insertComponents(project1, project2, project3);
+
+ SearchWsResponse response = call(SearchWsRequest.builder().setOrganization(organization1.getKey()).build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(project1.key(), project2.key());
+ }
+
+ @Test
+ public void result_is_paginated() throws IOException {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), SYSTEM_ADMIN);
+ List<ComponentDto> componentDtoList = new ArrayList<>();
+ for (int i = 1; i <= 9; i++) {
+ componentDtoList.add(newProjectDto(db.getDefaultOrganization(), "project-uuid-" + i).setKey("project-key-" + i).setName("Project Name " + i));
+ }
+ db.components().insertComponents(componentDtoList.toArray(new ComponentDto[] {}));
+
+ SearchWsResponse response = call(SearchWsRequest.builder().setPage(2).setPageSize(3).build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsExactly("project-key-4", "project-key-5", "project-key-6");
+ }
+
+ @Test
+ public void fail_when_not_system_admin() throws Exception {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), QUALITY_PROFILE_ADMIN);
+ expectedException.expect(ForbiddenException.class);
+
+ call(SearchWsRequest.builder().build());
+ }
+
+ @Test
+ public void fail_on_unknown_organization() throws Exception {
+ expectedException.expect(NotFoundException.class);
+
+ call(SearchWsRequest.builder().setOrganization("unknown").build());
+ }
+
+ @Test
+ public void fail_on_invalid_qualifier() throws Exception {
+ userSession.addOrganizationPermission(db.getDefaultOrganization(), QUALITY_PROFILE_ADMIN);
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Value of parameter 'qualifiers' (BRC) must be one of: [TRK, VW]");
+
+ call(SearchWsRequest.builder().setQualifiers(singletonList("BRC")).build());
+ }
+
+ @Test
+ public void verify_define() {
+ WebService.Action action = ws.getDef();
+ assertThat(action.key()).isEqualTo("search");
+ assertThat(action.isPost()).isFalse();
+ assertThat(action.description()).isEqualTo("Search for projects or views.<br>Requires 'System Administrator' permission");
+ assertThat(action.isInternal()).isTrue();
+ assertThat(action.since()).isEqualTo("6.3");
+ assertThat(action.handler()).isEqualTo(ws.getDef().handler());
+ assertThat(action.params()).hasSize(5);
+ assertThat(action.responseExample()).isEqualTo(getClass().getResource("search-example.json"));
+
+ WebService.Param organization = action.param("organization");
+ Assertions.assertThat(organization.description()).isEqualTo("The key of the organization");
+ Assertions.assertThat(organization.isInternal()).isTrue();
+ Assertions.assertThat(organization.isRequired()).isFalse();
+ Assertions.assertThat(organization.since()).isEqualTo("6.3");
+
+ WebService.Param qParam = action.param("q");
+ assertThat(qParam.isRequired()).isFalse();
+ assertThat(qParam.description()).isEqualTo("Limit search to component names or component keys that contain the supplied string.");
+
+ WebService.Param qualifierParam = action.param("qualifiers");
+ assertThat(qualifierParam.isRequired()).isFalse();
+ assertThat(qualifierParam.description()).isEqualTo("Comma-separated list of component qualifiers. Filter the results with the specified qualifiers");
+ assertThat(qualifierParam.possibleValues()).containsOnly("TRK", "VW");
+ assertThat(qualifierParam.defaultValue()).isEqualTo("TRK");
+
+ WebService.Param pParam = action.param("p");
+ assertThat(pParam.isRequired()).isFalse();
+ assertThat(pParam.defaultValue()).isEqualTo("1");
+ assertThat(pParam.description()).isEqualTo("1-based page number");
+
+ WebService.Param psParam = action.param("ps");
+ assertThat(psParam.isRequired()).isFalse();
+ assertThat(psParam.defaultValue()).isEqualTo("100");
+ assertThat(psParam.description()).isEqualTo("Page size. Must be greater than 0.");
+ }
+
+ @Test
+ public void verify_response_example() throws URISyntaxException, IOException {
+ OrganizationDto organizationDto = db.organizations().insertForKey("my-org-1");
+ userSession.addOrganizationPermission(organizationDto, SYSTEM_ADMIN);
+ db.components().insertComponents(
+ newProjectDto(organizationDto, "project-uuid-1").setName("Project Name 1").setKey("project-key-1"),
+ newProjectDto(organizationDto, "project-uuid-2").setName("Project Name 1").setKey("project-key-2"));
+
+ String response = ws.newRequest()
+ .setMediaType(MediaTypes.JSON)
+ .setParam(PARAM_ORGANIZATION, organizationDto.getKey())
+ .execute().getInput();
+ assertJson(response).isSimilarTo(ws.getDef().responseExampleAsString());
+ }
+
+ private SearchWsResponse call(SearchWsRequest wsRequest) {
+ TestRequest request = ws.newRequest()
+ .setMediaType(PROTOBUF);
+ setNullable(wsRequest.getOrganization(), organization -> request.setParam(PARAM_ORGANIZATION, organization));
+ List<String> qualifiers = wsRequest.getQualifiers();
+ if (!qualifiers.isEmpty()) {
+ request.setParam(ComponentsWsParameters.PARAM_QUALIFIERS, Joiner.on(",").join(qualifiers));
+ }
+ setNullable(wsRequest.getQuery(), query -> request.setParam(TEXT_QUERY, query));
+ setNullable(wsRequest.getPage(), page -> request.setParam(PAGE, String.valueOf(page)));
+ setNullable(wsRequest.getPageSize(), pageSize -> request.setParam(PAGE_SIZE, String.valueOf(pageSize)));
+ try {
+ return SearchWsResponse.parseFrom(request.execute().getInputStream());
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java
index e3142f248bd..b2646fa7ccc 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java
@@ -19,16 +19,23 @@
*/
package org.sonarqube.ws.client.project;
+import com.google.common.base.Joiner;
+import org.sonarqube.ws.WsProjects;
import org.sonarqube.ws.WsProjects.CreateWsResponse;
import org.sonarqube.ws.client.BaseService;
+import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsConnector;
+import static org.sonar.api.server.ws.WebService.Param.*;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_CREATE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_SEARCH;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.CONTROLLER;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_BRANCH;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NAME;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
/**
* Maps web service {@code api/projects}.
@@ -47,7 +54,7 @@ public class ProjectsService extends BaseService {
*/
public CreateWsResponse create(CreateRequest project) {
PostRequest request = new PostRequest(path(ACTION_CREATE))
- .setParam("organization", project.getOrganization())
+ .setParam(PARAM_ORGANIZATION, project.getOrganization())
.setParam(PARAM_PROJECT, project.getKey())
.setParam(PARAM_NAME, project.getName())
.setParam(PARAM_BRANCH, project.getBranch());
@@ -62,4 +69,14 @@ public class ProjectsService extends BaseService {
.setParam("id", request.getId())
.setParam("key", request.getKey()));
}
+
+ public WsProjects.SearchWsResponse search(SearchWsRequest request) {
+ GetRequest get = new GetRequest(path(ACTION_SEARCH))
+ .setParam(PARAM_ORGANIZATION, request.getOrganization())
+ .setParam(PARAM_QUALIFIERS, Joiner.on(",").join(request.getQualifiers()))
+ .setParam(TEXT_QUERY, request.getQuery())
+ .setParam(PAGE, request.getPage())
+ .setParam(PAGE_SIZE, request.getPageSize());
+ return call(get, WsProjects.SearchWsResponse.parser());
+ }
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java
index 616ed5814f3..9f6f707479b 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java
@@ -25,10 +25,13 @@ public class ProjectsWsParameters {
public static final String ACTION_CREATE = "create";
public static final String ACTION_INDEX = "index";
+ public static final String ACTION_SEARCH = "search";
public static final String PARAM_PROJECT = "project";
public static final String PARAM_NAME = "name";
public static final String PARAM_BRANCH = "branch";
+ public static final String PARAM_ORGANIZATION = "organization";
+ public static final String PARAM_QUALIFIERS = "qualifiers";
private ProjectsWsParameters() {
// static utils only
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java
new file mode 100644
index 00000000000..d6b18878ebb
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info 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.project;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+public class SearchWsRequest {
+ private final String organization;
+ private final String query;
+ private final List<String> qualifiers;
+ private final Integer page;
+ private final Integer pageSize;
+
+ public SearchWsRequest(Builder builder) {
+ this.organization = builder.organization;
+ this.query = builder.query;
+ this.qualifiers = builder.qualifiers;
+ this.page = builder.page;
+ this.pageSize = builder.pageSize;
+ }
+
+ @CheckForNull
+ public String getOrganization() {
+ return organization;
+ }
+
+ public List<String> getQualifiers() {
+ return qualifiers;
+ }
+
+ @CheckForNull
+ public Integer getPage() {
+ return page;
+ }
+
+ @CheckForNull
+ public Integer getPageSize() {
+ return pageSize;
+ }
+
+ @CheckForNull
+ public String getQuery() {
+ return query;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String organization;
+ private List<String> qualifiers = new ArrayList<>();
+ private Integer page;
+ private Integer pageSize;
+ private String query;
+
+ public Builder setOrganization(@Nullable String organization) {
+ this.organization = organization;
+ return this;
+ }
+
+ public Builder setQualifiers(List<String> qualifiers) {
+ this.qualifiers = requireNonNull(qualifiers, "Qualifiers cannot be null");
+ return this;
+ }
+
+ public Builder setPage(@Nullable Integer page) {
+ this.page = page;
+ return this;
+ }
+
+ public Builder setPageSize(@Nullable Integer pageSize) {
+ this.pageSize = pageSize;
+ return this;
+ }
+
+ public Builder setQuery(@Nullable String query) {
+ this.query = query;
+ return this;
+ }
+
+ public SearchWsRequest build() {
+ return new SearchWsRequest(this);
+ }
+ }
+
+}
diff --git a/sonar-ws/src/main/protobuf/ws-projects.proto b/sonar-ws/src/main/protobuf/ws-projects.proto
index 64fb78379ac..7dcb6260857 100644
--- a/sonar-ws/src/main/protobuf/ws-projects.proto
+++ b/sonar-ws/src/main/protobuf/ws-projects.proto
@@ -57,3 +57,17 @@ message CreateWsResponse {
}
}
+// WS api/projects/search
+message SearchWsResponse {
+ optional sonarqube.ws.commons.Paging paging = 1;
+ repeated Component components = 2;
+
+ message Component {
+ optional string organization = 1;
+ optional string id = 2;
+ optional string key = 3;
+ optional string name = 4;
+ optional string qualifier = 5;
+ }
+}
+
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java
index b2fca0a05ba..2576eb3eb09 100644
--- a/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java
+++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java
@@ -25,9 +25,12 @@ import org.sonarqube.ws.WsProjects;
import org.sonarqube.ws.client.ServiceTester;
import org.sonarqube.ws.client.WsConnector;
+import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.MapEntry.entry;
import static org.mockito.Mockito.mock;
+import static org.sonar.api.server.ws.WebService.Param.PAGE;
+import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
public class ProjectsServiceTest {
@@ -96,4 +99,24 @@ public class ProjectsServiceTest {
assertThat(serviceTester.getPostRequest().getPath()).isEqualTo("api/projects/delete");
assertThat(serviceTester.getPostRequest().getParams()).containsOnly(entry("key", "project_key"));
}
+
+ @Test
+ public void search() {
+ underTest.search(SearchWsRequest.builder()
+ .setOrganization("default")
+ .setQuery("project")
+ .setQualifiers(asList("TRK", "VW"))
+ .setPage(3)
+ .setPageSize(10)
+ .build());
+
+ serviceTester.assertThat(serviceTester.getGetRequest())
+ .hasPath("search")
+ .hasParam("organization", "default")
+ .hasParam("q", "project")
+ .hasParam("qualifiers", "TRK,VW")
+ .hasParam(PAGE, 3)
+ .hasParam(PAGE_SIZE, 10)
+ .andNoOtherParam();
+ }
}