diff options
author | Julien Lancelot <julien.lancelot@sonarsource.com> | 2017-02-16 16:55:13 +0100 |
---|---|---|
committer | Julien Lancelot <julien.lancelot@sonarsource.com> | 2017-02-17 09:27:54 +0100 |
commit | c00a059069b34fc14716a5fb40f6eaa5a2cddfe3 (patch) | |
tree | cc0f2a5fe2a2440f5b5fbfcdbcbd5a0031cb970f /server/sonar-server | |
parent | b4b1940277e877c853fbe2b32696c3cb1d50f816 (diff) | |
download | sonarqube-c00a059069b34fc14716a5fb40f6eaa5a2cddfe3.tar.gz sonarqube-c00a059069b34fc14716a5fb40f6eaa5a2cddfe3.zip |
SONAR-8804 Create api/projects/search
Diffstat (limited to 'server/sonar-server')
5 files changed, 471 insertions, 2 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); + } + } + +} |