From 6fe8b0cd64c4b4e2386d994016b465b0386ed09c Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Thu, 17 Dec 2015 16:19:44 +0100 Subject: [PATCH] SONAR-7129 WS api/components/tree --- .../server/component/ComponentFinder.java | 2 +- .../component/ws/ComponentsWsModule.java | 1 + .../server/component/ws/SearchAction.java | 8 +- .../sonar/server/component/ws/TreeAction.java | 273 +++++++++++++ .../ws/PermissionsWsParametersBuilder.java | 66 ---- .../ws/SearchProjectPermissionsAction.java | 6 +- .../ws/template/SetDefaultTemplateAction.java | 6 +- .../server/user/AbstractUserSession.java | 5 + .../sonar/server/ws/WsParameterBuilder.java | 121 ++++++ .../server/component/ws/tree-example.json | 90 +++++ .../component/ws/ComponentsWsModuleTest.java | 2 +- .../server/component/ws/SearchActionTest.java | 2 +- .../server/component/ws/TreeActionTest.java | 359 ++++++++++++++++++ .../computation/ws/ActivityActionTest.java | 6 +- .../measure/custom/ws/CreateActionTest.java | 4 +- .../measure/custom/ws/SearchActionTest.java | 4 +- .../server/source/ws/LinesActionTest.java | 2 +- .../org/sonar/db/component/ComponentDao.java | 19 + .../sonar/db/component/ComponentMapper.java | 26 +- .../db/component/ComponentTreeQuery.java | 188 +++++++++ .../sonar/db/component/ComponentMapper.xml | 69 +++- .../sonar/db/component/ComponentDaoTest.java | 191 +++++++++- .../sonar/db/component/ComponentDbTester.java | 25 +- .../sonar/db/component/ComponentTesting.java | 3 + .../db/component/ComponentTreeQueryTest.java | 63 +++ .../sonar/db/component/ResourceTypesRule.java | 21 +- .../sonar/db/component/SnapshotTesting.java | 10 +- .../org/sonar/api/server/ws/WebService.java | 17 +- .../main/java/org/sonar/api/utils/Paging.java | 4 + .../component/ComponentsWsParameters.java | 15 +- .../ws/client/component/TreeWsRequest.java | 127 +++++++ .../src/main/protobuf/ws-components.proto | 24 +- 32 files changed, 1635 insertions(+), 124 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/component/ws/tree-example.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java create mode 100644 sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java create mode 100644 sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java rename server/sonar-server/src/main/java/org/sonar/server/component/ws/WsComponentsParameters.java => sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java (66%) create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/component/TreeWsRequest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java index b9454a2545d..6c17563e99c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java @@ -45,7 +45,7 @@ public class ComponentFinder { } public ComponentDto getByUuidOrKey(DbSession dbSession, @Nullable String componentUuid, @Nullable String componentKey) { - checkArgument(componentUuid != null ^ componentKey != null, "The component key or the component id must be provided, not both."); + checkArgument(componentUuid != null ^ componentKey != null, "Either 'componentKey' or 'componentId' must be provided, not both"); if (componentUuid != null) { return getByUuid(dbSession, componentUuid); diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java index 6e80a568f7a..2b6896679dc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java @@ -32,6 +32,7 @@ public class ComponentsWsModule extends Module { // actions AppAction.class, SearchAction.class, + TreeAction.class, SearchViewComponentsAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchAction.java index 43807a65a3a..ad46f1d3a50 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchAction.java @@ -46,9 +46,9 @@ import static com.google.common.collect.FluentIterable.from; import static com.google.common.collect.Ordering.natural; import static java.lang.String.format; import static org.sonar.server.component.ResourceTypeFunctions.RESOURCE_TYPE_TO_QUALIFIER; -import static org.sonar.server.component.ws.WsComponentsParameters.PARAM_QUALIFIERS; import static org.sonar.server.ws.WsUtils.checkRequest; import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_QUALIFIER; public class SearchAction implements ComponentsWsAction { @@ -180,12 +180,12 @@ public class SearchAction implements ComponentsWsAction { return i18n.message(userSession.locale(), QUALIFIER_PROPERTY_PREFIX + qualifier, ""); } - private enum ComponentDToComponentResponseFunction implements Function { + private enum ComponentDToComponentResponseFunction implements Function { INSTANCE; @Override - public WsComponents.SearchWsResponse.Component apply(@Nonnull ComponentDto dto) { - return SearchWsResponse.Component.newBuilder() + public WsComponents.Component apply(@Nonnull ComponentDto dto) { + return WsComponents.Component.newBuilder() .setId(dto.uuid()) .setKey(dto.key()) .setName(dto.name()) diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java new file mode 100644 index 00000000000..8a8c4ede070 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java @@ -0,0 +1,273 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.component.ws; + +import com.google.common.collect.ImmutableSortedSet; +import java.util.List; +import java.util.Set; +import javax.annotation.CheckForNull; +import org.sonar.api.i18n.I18n; +import org.sonar.api.resources.ResourceTypes; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.utils.Paging; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTreeQuery; +import org.sonar.db.component.SnapshotDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.WsComponents; +import org.sonarqube.ws.WsComponents.TreeWsResponse; +import org.sonarqube.ws.client.component.TreeWsRequest; + +import static com.google.common.base.Objects.firstNonNull; +import static com.google.common.collect.Sets.newHashSet; +import static java.lang.String.format; +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02; +import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException; +import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext; +import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter; +import static org.sonar.server.ws.WsUtils.checkRequest; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_TREE; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BASE_COMPONENT_ID; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BASE_COMPONENT_KEY; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_STRATEGY; + +public class TreeAction implements ComponentsWsAction { + private static final int MAX_SIZE = 500; + private static final String ALL_STRATEGY = "all"; + private static final String CHILDREN_STRATEGY = "children"; + private static final String LEAVES_STRATEGY = "leaves"; + private static final Set STRATEGIES = ImmutableSortedSet.of(ALL_STRATEGY, CHILDREN_STRATEGY, LEAVES_STRATEGY); + private static final String NAME_SORT = "name"; + private static final Set SORTS = ImmutableSortedSet.of(NAME_SORT, "path", "qualifier"); + + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final ResourceTypes resourceTypes; + private final UserSession userSession; + private final I18n i18n; + + public TreeAction(DbClient dbClient, ComponentFinder componentFinder, ResourceTypes resourceTypes, UserSession userSession, I18n i18n) { + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.resourceTypes = resourceTypes; + this.userSession = userSession; + this.i18n = i18n; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_TREE) + .setDescription(format("Navigate through components based on the chosen strategy. The %s or the %s parameter must be provided.
" + + "Requires one of the following permissions:" + + "
    " + + "
  • 'Administer System'
  • " + + "
  • 'Administer' rights on the specified project
  • " + + "
  • 'Browse' on the specified project
  • " + + "
", + PARAM_BASE_COMPONENT_ID, PARAM_BASE_COMPONENT_KEY)) + .setSince("5.4") + .setResponseExample(getClass().getResource("tree-example.json")) + .setHandler(this) + .addSearchQuery("sonar", "component names", "component keys") + .addMultiSortsParams(newHashSet(SORTS), NAME_SORT, true) + .addPagingParams(100, MAX_SIZE); + + action.createParam(PARAM_BASE_COMPONENT_ID) + .setDescription("base component id. The search is based on this component. It is not included in the response.") + .setExampleValue(UUID_EXAMPLE_02); + + action.createParam(PARAM_BASE_COMPONENT_KEY) + .setDescription("base component key.The search is based on this component. It is not included in the response.") + .setExampleValue("org.apache.hbas:hbase"); + + createQualifiersParameter(action, newQualifierParameterContext(userSession, i18n, resourceTypes)); + + action.createParam(PARAM_STRATEGY) + .setDescription("Strategy to search for base component children:" + + "
    " + + "
  • children: return the direct children components of the base component. Grandchildren components are not returned
  • " + + "
  • all: return all the children components of the base component. Grandchildren are returned.
  • " + + "
  • leaves: return all the children components (files, in general) which don't have other children. They are the leaves of the component tree.
  • " + + "
") + .setPossibleValues(STRATEGIES) + .setDefaultValue(ALL_STRATEGY); + } + + @Override + public void handle(Request request, Response response) throws Exception { + TreeWsResponse treeWsResponse = doHandle(toTreeWsRequest(request)); + writeProtobuf(treeWsResponse, request, response); + } + + private TreeWsResponse doHandle(TreeWsRequest treeWsRequest) { + DbSession dbSession = dbClient.openSession(false); + try { + ComponentDto baseComponent = componentFinder.getByUuidOrKey(dbSession, treeWsRequest.getBaseComponentId(), treeWsRequest.getBaseComponentKey()); + checkPermissions(baseComponent); + SnapshotDto baseSnapshot = dbClient.snapshotDao().selectLastSnapshotByComponentId(dbSession, baseComponent.getId()); + if (baseSnapshot == null) { + return emptyResponse(treeWsRequest); + } + + ComponentTreeQuery query = toComponentTreeQuery(treeWsRequest, baseSnapshot); + List components; + int total; + switch (treeWsRequest.getStrategy()) { + case CHILDREN_STRATEGY: + components = dbClient.componentDao().selectDirectChildren(dbSession, query); + total = dbClient.componentDao().countDirectChildren(dbSession, query); + break; + case LEAVES_STRATEGY: + case ALL_STRATEGY: + components = dbClient.componentDao().selectAllChildren(dbSession, query); + total = dbClient.componentDao().countAllChildren(dbSession, query); + break; + default: + throw new IllegalStateException("Unknown component tree strategy"); + } + + return buildResponse(components, + Paging.forPageIndex(query.getPage()).withPageSize(query.getPageSize()).andTotal(total)); + } finally { + dbClient.closeSession(dbSession); + } + } + + private void checkPermissions(ComponentDto baseComponent) { + String projectUuid = firstNonNull(baseComponent.projectUuid(), baseComponent.uuid()); + if (!userSession.hasGlobalPermission(GlobalPermissions.SYSTEM_ADMIN) && + !userSession.hasProjectPermissionByUuid(UserRole.ADMIN, projectUuid) && + !userSession.hasProjectPermissionByUuid(UserRole.USER, projectUuid)) { + throw insufficientPrivilegesException(); + } + } + + private static TreeWsResponse buildResponse(List components, Paging paging) { + TreeWsResponse.Builder response = TreeWsResponse.newBuilder(); + response.getPagingBuilder() + .setPageIndex(paging.pageIndex()) + .setPageSize(paging.pageSize()) + .setTotal(paging.total()) + .build(); + + if (!components.isEmpty()) { + response.setProjectId(components.get(0).projectUuid()); + } + for (ComponentDto dto : components) { + response.addComponents(componentDtoToWsComponent(dto)); + } + + return response.build(); + } + + private static WsComponents.Component.Builder componentDtoToWsComponent(ComponentDto dto) { + WsComponents.Component.Builder wsComponent = WsComponents.Component.newBuilder() + .setId(dto.uuid()) + .setKey(dto.key()) + .setName(dto.name()) + .setQualifier(dto.qualifier()); + if (dto.path() != null) { + wsComponent.setPath(dto.path()); + } + if (dto.description() != null) { + wsComponent.setDescription(dto.description()); + } + + return wsComponent; + } + + private static TreeWsResponse emptyResponse(TreeWsRequest request) { + TreeWsResponse.Builder response = TreeWsResponse.newBuilder(); + response.getPagingBuilder() + .setTotal(0) + .setPageIndex(request.getPage()) + .setPageSize(request.getPageSize()); + + return response.build(); + } + + private ComponentTreeQuery toComponentTreeQuery(TreeWsRequest request, SnapshotDto baseSnapshot) { + List childrenQualifiers = childrenQualifiers(request, baseSnapshot.getQualifier()); + + ComponentTreeQuery.Builder query = ComponentTreeQuery.builder() + .setBaseSnapshot(baseSnapshot) + .setPage(request.getPage()) + .setPageSize(request.getPageSize()) + .setSortFields(request.getSort()) + .setAsc(request.getAsc()); + if (request.getQuery() != null) { + query.setNameOrKeyQuery(request.getQuery()); + } + if (childrenQualifiers != null) { + query.setQualifiers(childrenQualifiers); + } + + return query.build(); + } + + @CheckForNull + private List childrenQualifiers(TreeWsRequest request, String baseQualifier) { + List requestQualifiers = request.getQualifiers(); + List childrenQualifiers = null; + if (LEAVES_STRATEGY.equals(request.getStrategy())) { + childrenQualifiers = resourceTypes.getLeavesQualifiers(baseQualifier); + } + + if (requestQualifiers == null) { + return childrenQualifiers; + } + + if (childrenQualifiers == null) { + return requestQualifiers; + } + + // intersection of request and children qualifiers + childrenQualifiers.retainAll(requestQualifiers); + + return childrenQualifiers; + } + + private static TreeWsRequest toTreeWsRequest(Request request) { + TreeWsRequest treeWsRequest = new TreeWsRequest() + .setBaseComponentId(request.param(PARAM_BASE_COMPONENT_ID)) + .setBaseComponentKey(request.param(PARAM_BASE_COMPONENT_KEY)) + .setStrategy(request.param(PARAM_STRATEGY)) + .setQuery(request.param(Param.TEXT_QUERY)) + .setQualifiers(request.paramAsStrings(PARAM_QUALIFIERS)) + .setSort(request.mandatoryParamAsStrings(Param.SORT)) + .setAsc(request.mandatoryParamAsBoolean(Param.ASCENDING)) + .setPage(request.mandatoryParamAsInt(Param.PAGE)) + .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE)); + checkRequest(treeWsRequest.getPageSize() <= MAX_SIZE, "The '%s' parameter must be less thant %d", Param.PAGE_SIZE, MAX_SIZE); + + return treeWsRequest; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/PermissionsWsParametersBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/PermissionsWsParametersBuilder.java index 72d39c950d4..e36d212b466 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/PermissionsWsParametersBuilder.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/PermissionsWsParametersBuilder.java @@ -20,20 +20,12 @@ package org.sonar.server.permission.ws; -import java.util.Set; -import org.sonar.api.i18n.I18n; -import org.sonar.api.resources.ResourceTypes; import org.sonar.api.server.ws.WebService.NewAction; -import org.sonar.api.server.ws.WebService.NewParam; import org.sonar.core.permission.GlobalPermissions; import org.sonar.core.permission.ProjectPermissions; import org.sonar.core.util.Uuids; -import org.sonar.server.user.UserSession; -import static com.google.common.collect.FluentIterable.from; -import static com.google.common.collect.Ordering.natural; import static java.lang.String.format; -import static org.sonar.server.component.ResourceTypeFunctions.RESOURCE_TYPE_TO_QUALIFIER; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_DESCRIPTION; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_GROUP_ID; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_GROUP_NAME; @@ -42,7 +34,6 @@ import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_P import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_PROJECT_ID; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_PROJECT_KEY; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_PROJECT_KEY_PATTERN; -import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_QUALIFIER; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_ID; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_NAME; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_USER_LOGIN; @@ -149,61 +140,4 @@ public class PermissionsWsParametersBuilder { .setDescription("Id") .setExampleValue("af8cb8cc-1e78-4c4e-8c00-ee8e814009a5"); } - - public static NewParam createQualifierParameter(NewAction action, QualifierParameterContext context) { - return action.createParam(PARAM_QUALIFIER) - .setDescription("Project qualifier. Filter the results with the specified qualifier. Possible values are:" + buildRootQualifiersDescription(context)) - .setPossibleValues(getRootQualifiers(context.getResourceTypes())); - } - - private static Set getRootQualifiers(ResourceTypes resourceTypes) { - return from(resourceTypes.getRoots()) - .transform(RESOURCE_TYPE_TO_QUALIFIER) - .toSortedSet(natural()); - } - - private static String buildRootQualifiersDescription(QualifierParameterContext context) { - StringBuilder description = new StringBuilder(); - description.append("
    "); - String qualifierPattern = "
  • %s - %s
  • "; - for (String qualifier : getRootQualifiers(context.getResourceTypes())) { - description.append(format(qualifierPattern, qualifier, qualifierLabel(context, qualifier))); - } - description.append("
"); - - return description.toString(); - } - - private static String qualifierLabel(QualifierParameterContext context, String qualifier) { - String qualifiersPropertyPrefix = "qualifiers."; - return context.getI18n().message(context.getUserSession().locale(), qualifiersPropertyPrefix + qualifier, ""); - } - - public static class QualifierParameterContext { - private final I18n i18n; - private final ResourceTypes resourceTypes; - private final UserSession userSession; - - private QualifierParameterContext(UserSession userSession, I18n i18n, ResourceTypes resourceTypes) { - this.i18n = i18n; - this.resourceTypes = resourceTypes; - this.userSession = userSession; - } - - public static QualifierParameterContext newQualifierParameterContext(UserSession userSession, I18n i18n, ResourceTypes resourceTypes) { - return new QualifierParameterContext(userSession, i18n, resourceTypes); - } - - public I18n getI18n() { - return i18n; - } - - public ResourceTypes getResourceTypes() { - return resourceTypes; - } - - public UserSession getUserSession() { - return userSession; - } - } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsAction.java b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsAction.java index 41f5000ea2b..40b81f06722 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsAction.java @@ -43,10 +43,10 @@ import static org.sonar.server.permission.PermissionPrivilegeChecker.checkGlobal import static org.sonar.server.permission.PermissionPrivilegeChecker.checkProjectAdminUserByComponentKey; import static org.sonar.server.permission.PermissionPrivilegeChecker.checkProjectAdminUserByComponentUuid; import static org.sonar.server.permission.ws.PermissionRequestValidator.validateQualifier; -import static org.sonar.server.permission.ws.PermissionsWsParametersBuilder.QualifierParameterContext.newQualifierParameterContext; import static org.sonar.server.permission.ws.PermissionsWsParametersBuilder.createProjectParameter; -import static org.sonar.server.permission.ws.PermissionsWsParametersBuilder.createQualifierParameter; import static org.sonar.server.permission.ws.WsProjectRef.newOptionalWsProjectRef; +import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext; +import static org.sonar.server.ws.WsParameterBuilder.createRootQualifierParameter; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_PROJECT_ID; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_PROJECT_KEY; @@ -82,7 +82,7 @@ public class SearchProjectPermissionsAction implements PermissionsWsAction { .setHandler(this); createProjectParameter(action); - createQualifierParameter(action, newQualifierParameterContext(userSession, i18n, resourceTypes)) + createRootQualifierParameter(action, newQualifierParameterContext(userSession, i18n, resourceTypes)) .setSince("5.3"); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/SetDefaultTemplateAction.java b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/SetDefaultTemplateAction.java index 5d98cfb0b85..14735422589 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/SetDefaultTemplateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/SetDefaultTemplateAction.java @@ -38,10 +38,10 @@ import org.sonarqube.ws.client.permission.SetDefaultTemplateWsRequest; import static org.sonar.server.permission.DefaultPermissionTemplates.defaultRootQualifierTemplateProperty; import static org.sonar.server.permission.PermissionPrivilegeChecker.checkGlobalAdminUser; import static org.sonar.server.permission.ws.PermissionRequestValidator.validateQualifier; -import static org.sonar.server.permission.ws.PermissionsWsParametersBuilder.QualifierParameterContext.newQualifierParameterContext; -import static org.sonar.server.permission.ws.PermissionsWsParametersBuilder.createQualifierParameter; import static org.sonar.server.permission.ws.PermissionsWsParametersBuilder.createTemplateParameters; import static org.sonar.server.permission.ws.WsTemplateRef.newTemplateRef; +import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext; +import static org.sonar.server.ws.WsParameterBuilder.createRootQualifierParameter; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_QUALIFIER; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_ID; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_NAME; @@ -74,7 +74,7 @@ public class SetDefaultTemplateAction implements PermissionsWsAction { .setHandler(this); createTemplateParameters(action); - createQualifierParameter(action, newQualifierParameterContext(userSession, i18n, resourceTypes)) + createRootQualifierParameter(action, newQualifierParameterContext(userSession, i18n, resourceTypes)) .setDefaultValue(Qualifiers.PROJECT); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java index 99b43978e7d..6fa59c1570e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java @@ -41,6 +41,7 @@ import static com.google.common.collect.Maps.newHashMap; public abstract class AbstractUserSession implements UserSession { protected static final String INSUFFICIENT_PRIVILEGES_MESSAGE = "Insufficient privileges"; + private static final ForbiddenException INSUFFICIENT_PRIVILEGES_EXCEPTION = new ForbiddenException(INSUFFICIENT_PRIVILEGES_MESSAGE); protected Integer userId; protected String login; @@ -189,4 +190,8 @@ public abstract class AbstractUserSession impleme } return this; } + + public static ForbiddenException insufficientPrivilegesException() { + return INSUFFICIENT_PRIVILEGES_EXCEPTION; + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java new file mode 100644 index 00000000000..8e36801bed3 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java @@ -0,0 +1,121 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.ws; + +import java.util.Collections; +import java.util.Set; +import org.sonar.api.i18n.I18n; +import org.sonar.api.resources.ResourceTypes; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.user.UserSession; + +import static com.google.common.collect.FluentIterable.from; +import static com.google.common.collect.Ordering.natural; +import static java.lang.String.format; +import static org.sonar.server.component.ResourceTypeFunctions.RESOURCE_TYPE_TO_QUALIFIER; + +public class WsParameterBuilder { + private static final String PARAM_QUALIFIER = "qualifier"; + private static final String PARAM_QUALIFIERS = "qualifiers"; + + private WsParameterBuilder() { + // static methods only + } + + public static WebService.NewParam createRootQualifierParameter(WebService.NewAction action, QualifierParameterContext context) { + return action.createParam(PARAM_QUALIFIER) + .setDescription("Project qualifier. Filter the results with the specified qualifier. Possible values are:" + buildRootQualifiersDescription(context)) + .setPossibleValues(getRootQualifiers(context.getResourceTypes())); + } + + public static WebService.NewParam createQualifiersParameter(WebService.NewAction action, QualifierParameterContext context) { + action.addFieldsParam(Collections.emptyList()); + return action.createParam(PARAM_QUALIFIERS) + .setDescription( + "Comma-separated list of component qualifiers. Filter the results with the specified qualifiers. Possible values are:" + buildAllQualifiersDescription(context)) + .setPossibleValues(getAllQualifiers(context.getResourceTypes())); + } + + private static Set getRootQualifiers(ResourceTypes resourceTypes) { + return from(resourceTypes.getRoots()) + .transform(RESOURCE_TYPE_TO_QUALIFIER) + .toSortedSet(natural()); + } + + private static Set getAllQualifiers(ResourceTypes resourceTypes) { + return from(resourceTypes.getAll()) + .transform(RESOURCE_TYPE_TO_QUALIFIER) + .toSortedSet(natural()); + } + + private static String buildRootQualifiersDescription(QualifierParameterContext context) { + return buildQualifiersDescription(context, getRootQualifiers(context.getResourceTypes())); + } + + private static String buildAllQualifiersDescription(QualifierParameterContext context) { + return buildQualifiersDescription(context, getAllQualifiers(context.getResourceTypes())); + } + + private static String buildQualifiersDescription(QualifierParameterContext context, Set qualifiers) { + StringBuilder description = new StringBuilder(); + description.append("
    "); + String qualifierPattern = "
  • %s - %s
  • "; + for (String qualifier : qualifiers) { + description.append(format(qualifierPattern, qualifier, qualifierLabel(context, qualifier))); + } + description.append("
"); + + return description.toString(); + } + + private static String qualifierLabel(QualifierParameterContext context, String qualifier) { + String qualifiersPropertyPrefix = "qualifiers."; + return context.getI18n().message(context.getUserSession().locale(), qualifiersPropertyPrefix + qualifier, ""); + } + + public static class QualifierParameterContext { + private final I18n i18n; + private final ResourceTypes resourceTypes; + private final UserSession userSession; + + private QualifierParameterContext(UserSession userSession, I18n i18n, ResourceTypes resourceTypes) { + this.i18n = i18n; + this.resourceTypes = resourceTypes; + this.userSession = userSession; + } + + public static QualifierParameterContext newQualifierParameterContext(UserSession userSession, I18n i18n, ResourceTypes resourceTypes) { + return new QualifierParameterContext(userSession, i18n, resourceTypes); + } + + public I18n getI18n() { + return i18n; + } + + public ResourceTypes getResourceTypes() { + return resourceTypes; + } + + public UserSession getUserSession() { + return userSession; + } + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/component/ws/tree-example.json b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/tree-example.json new file mode 100644 index 00000000000..e5e4ef1d2f5 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/tree-example.json @@ -0,0 +1,90 @@ +{ + "paging": { + "pageIndex": 1, + "pageSize": 100, + "total": 10 + }, + "projectId": "project-id", + "components": [ + { + "id": "file-id-1", + "key": "file-key-1", + "name": "file-name-1", + "description": "description 1", + "qualifier": "FIL", + "path": "path/to/file-name-1" + }, + { + "id": "file-id-10", + "key": "file-key-10", + "name": "file-name-10", + "description": "description 10", + "qualifier": "FIL", + "path": "path/to/file-name-10" + }, + { + "id": "file-id-2", + "key": "file-key-2", + "name": "file-name-2", + "description": "description 2", + "qualifier": "FIL", + "path": "path/to/file-name-2" + }, + { + "id": "file-id-3", + "key": "file-key-3", + "name": "file-name-3", + "description": "description 3", + "qualifier": "FIL", + "path": "path/to/file-name-3" + }, + { + "id": "file-id-4", + "key": "file-key-4", + "name": "file-name-4", + "description": "description 4", + "qualifier": "FIL", + "path": "path/to/file-name-4" + }, + { + "id": "file-id-5", + "key": "file-key-5", + "name": "file-name-5", + "description": "description 5", + "qualifier": "FIL", + "path": "path/to/file-name-5" + }, + { + "id": "file-id-6", + "key": "file-key-6", + "name": "file-name-6", + "description": "description 6", + "qualifier": "FIL", + "path": "path/to/file-name-6" + }, + { + "id": "file-id-7", + "key": "file-key-7", + "name": "file-name-7", + "description": "description 7", + "qualifier": "FIL", + "path": "path/to/file-name-7" + }, + { + "id": "file-id-8", + "key": "file-key-8", + "name": "file-name-8", + "description": "description 8", + "qualifier": "FIL", + "path": "path/to/file-name-8" + }, + { + "id": "file-id-9", + "key": "file-key-9", + "name": "file-name-9", + "description": "description 9", + "qualifier": "FIL", + "path": "path/to/file-name-9" + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java index d68d694fb36..5e854bb1f5f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java @@ -30,6 +30,6 @@ public class ComponentsWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new ComponentsWsModule().configure(container); - assertThat(container.size()).isEqualTo(8); + assertThat(container.size()).isEqualTo(7 + 2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java index 140843d463e..5d557311a73 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java @@ -56,7 +56,7 @@ 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.server.component.ws.WsComponentsParameters.PARAM_QUALIFIERS; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS; import static org.sonar.test.JsonAssert.assertJson; public class SearchActionTest { diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java new file mode 100644 index 00000000000..6a979e97558 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java @@ -0,0 +1,359 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.component.ws; + +import java.io.IOException; +import java.io.InputStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.sonar.api.i18n.I18n; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.component.ResourceTypesRule; +import org.sonar.db.component.SnapshotDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; +import org.sonar.test.DbTests; +import org.sonar.test.JsonAssert; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.WsComponents; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.component.ComponentTesting.newDirectory; +import static org.sonar.db.component.ComponentTesting.newModuleDto; +import static org.sonar.db.component.ComponentTesting.newProjectCopy; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.component.ComponentTesting.newSubView; +import static org.sonar.db.component.ComponentTesting.newView; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BASE_COMPONENT_ID; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_STRATEGY; + +@Category(DbTests.class) +public class TreeActionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + ResourceTypesRule resourceTypes = new ResourceTypesRule(); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + ComponentDbTester componentDb = new ComponentDbTester(db); + DbClient dbClient = db.getDbClient(); + + WsActionTester ws; + + @Before + public void setUp() { + userSession.setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + ws = new WsActionTester(new TreeAction(dbClient, new ComponentFinder(dbClient), resourceTypes, userSession, Mockito.mock(I18n.class))); + resourceTypes.setChildrenQualifiers(Qualifiers.MODULE, Qualifiers.FILE, Qualifiers.DIRECTORY); + resourceTypes.setLeavesQualifiers(Qualifiers.FILE); + } + + @Test + public void json_example() throws IOException { + ComponentDto project = newProjectDto("project-id"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + for (int i = 1; i <= 10; i++) { + componentDb.insertComponentAndSnapshot(ComponentTesting.newFileDto(project, "file-id-" + i) + .setKey("file-key-" + i) + .setName("file-name-" + i) + .setPath("path/to/file-name-" + i) + .setProjectUuid("project-id") + .setDescription("description " + i) + .setCreatedAt(DateUtils.parseDateTime("2015-12-17T22:07:14+0100")), + projectSnapshot); + } + db.commit(); + componentDb.indexProjects(); + + String response = ws.newRequest() + .setParam(PARAM_BASE_COMPONENT_ID, "project-id") + .execute().getInput(); + + JsonAssert.assertJson(response) + .withStrictArrayOrder() + .isSimilarTo(getClass().getResource("tree-example.json")); + } + + @Test + public void direct_children() throws IOException { + userSession.anonymous().login().addProjectUuidPermissions(UserRole.ADMIN, "project-uuid"); + ComponentDto project = newProjectDto("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-uuid-1", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 1), projectSnapshot); + for (int i = 2; i <= 9; i++) { + componentDb.insertComponentAndSnapshot(newFileDto(project, i), moduleSnapshot); + } + SnapshotDto directorySnapshot = componentDb.insertComponentAndSnapshot(newDirectory(project, "directory-path-1"), moduleSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 10), directorySnapshot); + db.commit(); + componentDb.indexProjects(); + + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_STRATEGY, "children") + .setParam(PARAM_BASE_COMPONENT_ID, "module-uuid-1") + .setParam(Param.PAGE, "2") + .setParam(Param.PAGE_SIZE, "3") + .setParam(Param.TEXT_QUERY, "file-name") + .setParam(Param.ASCENDING, "false") + .setParam(Param.SORT, "name") + .execute().getInputStream(); + WsComponents.TreeWsResponse response = WsComponents.TreeWsResponse.parseFrom(responseStream); + + assertThat(response.getComponentsCount()).isEqualTo(3); + assertThat(response.getPaging().getTotal()).isEqualTo(8); + assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-6", "file-uuid-5", "file-uuid-4"); + } + + @Test + public void all_children() throws IOException { + userSession.anonymous().login() + .addProjectUuidPermissions(UserRole.USER, "project-uuid"); + + ComponentDto project = newProjectDto("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-uuid-1", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 10), projectSnapshot); + for (int i = 2; i <= 9; i++) { + componentDb.insertComponentAndSnapshot(newFileDto(project, i), moduleSnapshot); + } + SnapshotDto directorySnapshot = componentDb.insertComponentAndSnapshot(newDirectory(project, "directory-path-1"), moduleSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 1), directorySnapshot); + db.commit(); + componentDb.indexProjects(); + + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_STRATEGY, "all") + .setParam(PARAM_BASE_COMPONENT_ID, "module-uuid-1") + .setParam(Param.PAGE, "2") + .setParam(Param.PAGE_SIZE, "3") + .setParam(Param.TEXT_QUERY, "file-name") + .setParam(Param.ASCENDING, "true") + .setParam(Param.SORT, "path") + .execute().getInputStream(); + WsComponents.TreeWsResponse response = WsComponents.TreeWsResponse.parseFrom(responseStream); + + assertThat(response.getComponentsCount()).isEqualTo(3); + assertThat(response.getPaging().getTotal()).isEqualTo(9); + assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-4", "file-uuid-5", "file-uuid-6"); + } + + @Test + public void leaves_children() throws IOException { + ComponentDto project = newProjectDto().setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-uuid-1", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 1), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 2), moduleSnapshot); + SnapshotDto directorySnapshot = componentDb.insertComponentAndSnapshot(newDirectory(project, "directory-path-1"), moduleSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 3), directorySnapshot); + db.commit(); + componentDb.indexProjects(); + + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_STRATEGY, "leaves") + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .execute().getInputStream(); + WsComponents.TreeWsResponse response = WsComponents.TreeWsResponse.parseFrom(responseStream); + + assertThat(response.getComponentsCount()).isEqualTo(3); + assertThat(response.getPaging().getTotal()).isEqualTo(3); + assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-1", "file-uuid-2", "file-uuid-3"); + } + + @Test + public void all_children_by_file_qualifier() throws IOException { + ComponentDto project = newProjectDto().setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + componentDb.insertComponentAndSnapshot(newFileDto(project, 1), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 2), projectSnapshot); + componentDb.insertComponentAndSnapshot(newModuleDto("module-uuid-1", project), projectSnapshot); + db.commit(); + componentDb.indexProjects(); + + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_STRATEGY, "all") + .setParam(PARAM_QUALIFIERS, Qualifiers.FILE) + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .execute().getInputStream(); + WsComponents.TreeWsResponse response = WsComponents.TreeWsResponse.parseFrom(responseStream); + + assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-1", "file-uuid-2"); + } + + @Test + public void all_children_sort_by_qualifier() throws IOException { + ComponentDto project = newProjectDto().setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + componentDb.insertComponentAndSnapshot(newFileDto(project, 2), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, 1), projectSnapshot); + ComponentDto module = newModuleDto("module-uuid-1", project); + componentDb.insertComponentAndSnapshot(module, projectSnapshot); + componentDb.insertComponentAndSnapshot(newDirectory(project, "path/directory/", "directory-uuid-1"), projectSnapshot); + db.commit(); + componentDb.indexProjects(); + + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_STRATEGY, "all") + .setParam(Param.SORT, "qualifier, name") + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .execute().getInputStream(); + WsComponents.TreeWsResponse response = WsComponents.TreeWsResponse.parseFrom(responseStream); + + assertThat(response.getComponentsList()).extracting("id").containsExactly("module-uuid-1", "path/directory/", "file-uuid-1", "file-uuid-2"); + } + + @Test + public void direct_children_of_a_view() throws IOException { + ComponentDto view = newView("view-uuid"); + SnapshotDto viewSnapshot = componentDb.insertViewAndSnapshot(view); + ComponentDto project = newProjectDto("project-uuid-1"); + componentDb.insertProjectAndSnapshot(project); + componentDb.insertComponentAndSnapshot(newProjectCopy("project-uuid-1-copy", project, view), viewSnapshot); + componentDb.insertComponentAndSnapshot(newSubView(view, "sub-view-uuid", "sub-view-key"), viewSnapshot); + db.commit(); + componentDb.indexProjects(); + + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_STRATEGY, "children") + .setParam(PARAM_BASE_COMPONENT_ID, "view-uuid") + .execute().getInputStream(); + WsComponents.TreeWsResponse response = WsComponents.TreeWsResponse.parseFrom(responseStream); + + assertThat(response.getComponentsList()).extracting("id").containsExactly("project-uuid-1-copy", "sub-view-uuid"); + } + + @Test + public void empty_response_for_provisioned_project() throws IOException { + componentDb.insertComponent(newProjectDto("project-uuid")); + db.commit(); + + InputStream responseStream = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .execute().getInputStream(); + WsComponents.TreeWsResponse response = WsComponents.TreeWsResponse.parseFrom(responseStream); + + assertThat(response.getComponentsList()).isEmpty(); + assertThat(response.getPaging().getTotal()).isEqualTo(0); + assertThat(response.getPaging().getPageSize()).isEqualTo(100); + assertThat(response.getPaging().getPageIndex()).isEqualTo(1); + } + + @Test + public void fail_when_not_enough_privileges() { + expectedException.expect(ForbiddenException.class); + userSession.anonymous().login() + .addProjectUuidPermissions(UserRole.CODEVIEWER, "project-uuid"); + componentDb.insertComponent(newProjectDto("project-uuid")); + db.commit(); + + ws.newRequest() + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .execute(); + } + + @Test + public void fail_when_page_size_above_500() { + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("The 'ps' parameter must be less thant 500"); + componentDb.insertComponent(newProjectDto("project-uuid")); + db.commit(); + + ws.newRequest() + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .setParam(Param.PAGE_SIZE, "501") + .execute(); + } + + @Test + public void fail_when_sort_is_unknown() { + expectedException.expect(IllegalArgumentException.class); + componentDb.insertComponent(newProjectDto("project-uuid")); + db.commit(); + + ws.newRequest() + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .setParam(Param.SORT, "unknown-sort") + .execute(); + } + + @Test + public void fail_when_strategy_is_unknown() { + expectedException.expect(IllegalArgumentException.class); + componentDb.insertComponent(newProjectDto("project-uuid")); + db.commit(); + + ws.newRequest() + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .setParam(PARAM_STRATEGY, "unknown-strategy") + .execute(); + } + + @Test + public void fail_when_base_component_not_found() { + expectedException.expect(NotFoundException.class); + + ws.newRequest() + .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid") + .execute(); + } + + @Test + public void fail_when_no_base_component_parameter() { + expectedException.expect(IllegalArgumentException.class); + + ws.newRequest().execute(); + } + + private static ComponentDto newFileDto(ComponentDto parentComponent, int i) { + return ComponentTesting.newFileDto(parentComponent, "file-uuid-" + i) + .setName("file-name-" + i) + .setKey("file-key-" + i) + .setPath("file-path-" + i); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/ws/ActivityActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/ws/ActivityActionTest.java index a3f97031f12..fc7671a6ae7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/ws/ActivityActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/ws/ActivityActionTest.java @@ -189,9 +189,9 @@ public class ActivityActionTest { @Test public void search_activity_by_component_name() throws IOException { - componentDb.insertProjectAndSnapshot(dbTester.getSession(), newProjectDto().setName("apache struts").setUuid("P1")); - componentDb.insertProjectAndSnapshot(dbTester.getSession(), newProjectDto().setName("apache zookeeper").setUuid("P2")); - componentDb.insertProjectAndSnapshot(dbTester.getSession(), newProjectDto().setName("eclipse").setUuid("P3")); + componentDb.insertProjectAndSnapshot(newProjectDto().setName("apache struts").setUuid("P1")); + componentDb.insertProjectAndSnapshot(newProjectDto().setName("apache zookeeper").setUuid("P2")); + componentDb.insertProjectAndSnapshot(newProjectDto().setName("eclipse").setUuid("P3")); dbTester.commit(); componentDb.indexProjects(); userSession.setGlobalPermissions(UserRole.ADMIN); diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java index 7aa2f325cc2..5f261649a37 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java @@ -335,7 +335,7 @@ public class CreateActionTest { @Test public void fail_when_project_id_nor_project_key_provided() throws Exception { expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("The component key or the component id must be provided, not both."); + expectedException.expectMessage("Either 'componentKey' or 'componentId' must be provided, not both"); insertProject(DEFAULT_PROJECT_UUID); MetricDto metric = insertMetric(STRING); @@ -348,7 +348,7 @@ public class CreateActionTest { @Test public void fail_when_project_id_and_project_key_are_provided() throws Exception { expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("The component key or the component id must be provided, not both."); + expectedException.expectMessage("Either 'componentKey' or 'componentId' must be provided, not both"); insertProject(DEFAULT_PROJECT_UUID); MetricDto metric = insertMetric(STRING); diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java index 71fa47a68b5..3e2472abf7e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java @@ -243,7 +243,7 @@ public class SearchActionTest { @Test public void fail_when_project_id_and_project_key_provided() throws Exception { expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("The component key or the component id must be provided, not both."); + expectedException.expectMessage("Either 'componentKey' or 'componentId' must be provided, not both"); newRequest() .setParam(SearchAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) @@ -254,7 +254,7 @@ public class SearchActionTest { @Test public void fail_when_project_id_nor_project_key_provided() throws Exception { expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("The component key or the component id must be provided, not both."); + expectedException.expectMessage("Either 'componentKey' or 'componentId' must be provided, not both"); newRequest().execute(); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java index 26f60a1a93a..a8b8aa4c55f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java @@ -140,7 +140,7 @@ public class LinesActionTest { @Test public void fail_when_no_uuid_or_key_param() throws Exception { thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The component key or the component id must be provided, not both."); + thrown.expectMessage("Either 'componentKey' or 'componentId' must be provided, not both"); WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines"); request.execute(); diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java index 4b8f62b0e06..14371abbcf1 100644 --- a/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java +++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java @@ -41,6 +41,7 @@ import org.sonar.db.RowNotFoundException; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Maps.newHashMapWithExpectedSize; +import static org.sonar.api.utils.Paging.offset; import static org.sonar.db.DatabaseUtils.executeLargeInputs; public class ComponentDao implements Dao { @@ -150,6 +151,24 @@ public class ComponentDao implements Dao { return mapper(session).selectComponentsHavingSameKeyOrderedById(key); } + public List selectDirectChildren(DbSession dbSession, ComponentTreeQuery componentQuery) { + RowBounds rowBounds = new RowBounds(offset(componentQuery.getPage(), componentQuery.getPageSize()), componentQuery.getPageSize()); + return mapper(dbSession).selectDirectChildren(componentQuery, rowBounds); + } + + public List selectAllChildren(DbSession dbSession, ComponentTreeQuery componentQuery) { + RowBounds rowBounds = new RowBounds(offset(componentQuery.getPage(), componentQuery.getPageSize()), componentQuery.getPageSize()); + return mapper(dbSession).selectAllChildren(componentQuery, rowBounds); + } + + public int countDirectChildren(DbSession dbSession, ComponentTreeQuery query) { + return mapper(dbSession).countDirectChildren(query); + } + + public int countAllChildren(DbSession dbSession, ComponentTreeQuery query) { + return mapper(dbSession).countAllChildren(query); + } + private static class KeyToDto implements Function, List> { private final ComponentMapper mapper; diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java index 42a59729461..47e3191cca9 100644 --- a/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java +++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java @@ -59,9 +59,24 @@ public interface ComponentMapper { List selectComponentsByQualifiers(@Param("qualifiers") Collection qualifiers); - List selectByQuery(ComponentQuery query, RowBounds rowBounds); + List selectByQuery(@Param("query") ComponentQuery query, RowBounds rowBounds); - int countByQuery(ComponentQuery query); + int countByQuery(@Param("query") ComponentQuery query); + + /** + * Return direct children components + */ + List selectDirectChildren(@Param("query") ComponentTreeQuery componentTreeQuery, RowBounds rowBounds); + + int countDirectChildren(@Param("query") ComponentTreeQuery componentTreeQuery); + + /** + * Return all children components. + */ + List selectAllChildren(@Param("query") ComponentTreeQuery componentTreeQuery, + RowBounds rowBounds); + + int countAllChildren(@Param("query") ComponentTreeQuery componentTreeQuery); /** * Return all project (PRJ/TRK) uuids @@ -83,7 +98,7 @@ public interface ComponentMapper { * Return all descendant modules (including itself) from a given component uuid and scope */ List selectDescendantModules(@Param("moduleUuid") String moduleUuid, @Param(value = "scope") String scope, - @Param(value = "excludeDisabled") boolean excludeDisabled); + @Param(value = "excludeDisabled") boolean excludeDisabled); /** * Return all files from a given project uuid and scope @@ -94,7 +109,7 @@ public interface ComponentMapper { * Return all descendant files from a given module uuid and scope */ List selectDescendantFiles(@Param("moduleUuid") String moduleUuid, @Param(value = "scope") String scope, - @Param(value = "excludeDisabled") boolean excludeDisabled); + @Param(value = "excludeDisabled") boolean excludeDisabled); /** * Return uuids and project uuids from list of qualifiers @@ -109,7 +124,7 @@ public interface ComponentMapper { * @param scope scope of components to return. If null, all components are returned */ List selectComponentsFromProjectKeyAndScope(@Param("projectKey") String projectKey, @Nullable @Param("scope") String scope, - @Param(value = "excludeDisabled") boolean excludeDisabled); + @Param(value = "excludeDisabled") boolean excludeDisabled); /** * Return technical projects from a view or a sub-view @@ -133,5 +148,4 @@ public interface ComponentMapper { void update(ComponentDto componentDto); void delete(long componentId); - } diff --git a/sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java b/sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java new file mode 100644 index 00000000000..749c21e3aff --- /dev/null +++ b/sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java @@ -0,0 +1,188 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.db.component; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import java.util.Collection; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.sonar.db.WildcardPosition; + +import static com.google.common.collect.FluentIterable.from; +import static java.util.Objects.requireNonNull; +import static org.sonar.db.DatabaseUtils.buildLikeValue; +import static org.sonar.db.WildcardPosition.AFTER; + +public class ComponentTreeQuery { + @CheckForNull + private final String nameOrKeyQuery; + @CheckForNull + private final Collection qualifiers; + @CheckForNull + private final Integer page; + @CheckForNull + private final Integer pageSize; + private final SnapshotDto baseSnapshot; + private final String baseSnapshotPath; + private final String sqlSort; + private final String direction; + + private ComponentTreeQuery(Builder builder) { + this.nameOrKeyQuery = builder.nameOrKeyQuery; + this.qualifiers = builder.qualifiers; + this.page = builder.page; + this.pageSize = builder.pageSize; + this.baseSnapshot = builder.baseSnapshot; + this.baseSnapshotPath = buildLikeValue(baseSnapshot.getPath() + baseSnapshot.getId() + ".", WildcardPosition.AFTER); + this.direction = builder.asc ? "ASC" : "DESC"; + this.sqlSort = sortFieldsToSqlSort(builder.sortFields, direction); + } + + public Collection getQualifiers() { + return qualifiers; + } + + public String getNameOrKeyQuery() { + return nameOrKeyQuery; + } + + @CheckForNull + public String getNameOrKeyQueryToSqlForResourceIndex() { + return nameOrKeyQuery == null ? null : buildLikeValue(nameOrKeyQuery, AFTER).toLowerCase(); + } + + @CheckForNull + public String getNameOrKeyQueryToSqlForProjectKey() { + return nameOrKeyQuery == null ? null : buildLikeValue(nameOrKeyQuery, AFTER); + } + + public Integer getPage() { + return page; + } + + public Integer getPageSize() { + return pageSize; + } + + public SnapshotDto getBaseSnapshot() { + return baseSnapshot; + } + + public String getBaseSnapshotPath() { + return baseSnapshotPath; + } + + public String getSqlSort() { + return sqlSort; + } + + public String getDirection() { + return direction; + } + + public static Builder builder() { + return new Builder(); + } + + private String sortFieldsToSqlSort(List sortFields, String direction) { + List sqlSortFields = from(sortFields) + .transform(new SortFieldToSqlSortFieldFunction(direction)).toList(); + + return Joiner.on(", ").join(sqlSortFields); + } + + public static class Builder { + @CheckForNull + private String nameOrKeyQuery; + @CheckForNull + private Collection qualifiers; + @CheckForNull + private Integer page; + @CheckForNull + private Integer pageSize; + private SnapshotDto baseSnapshot; + private List sortFields; + private boolean asc = true; + + private Builder() { + // private constructor + } + + public ComponentTreeQuery build() { + requireNonNull(baseSnapshot); + return new ComponentTreeQuery(this); + } + + public Builder setNameOrKeyQuery(@Nullable String nameOrKeyQuery) { + this.nameOrKeyQuery = nameOrKeyQuery; + return this; + } + + public Builder setQualifiers(Collection qualifiers) { + this.qualifiers = qualifiers; + return this; + } + + public Builder setPage(int page) { + this.page = page; + return this; + } + + public Builder setPageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + + public Builder setBaseSnapshot(SnapshotDto baseSnapshot) { + this.baseSnapshot = baseSnapshot; + return this; + } + + public Builder setSortFields(List sorts) { + this.sortFields = requireNonNull(sorts); + return this; + } + + public Builder setAsc(boolean asc) { + this.asc = asc; + return this; + } + } + + private static class SortFieldToSqlSortFieldFunction implements Function { + private static final String PATTERN = "LOWER(p.%1$s) %2$s, p.%1$s %2$s"; + + private final String direction; + + private SortFieldToSqlSortFieldFunction(String direction) { + this.direction = direction; + } + + @Nonnull + @Override + public String apply(@Nonnull String input) { + return String.format(PATTERN, input, direction); + } + } +} diff --git a/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml b/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml index c9c9205ec8e..f88928f83ca 100644 --- a/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml @@ -280,25 +280,84 @@ AND p.enabled=${_true} AND p.copy_resource_id is null AND p.qualifier in - + #{qualifier} - + AND (exists ( select 1 from resource_index ri where ri.resource_id=p.id AND ri.qualifier in - + #{qualifier} - AND ri.kee like #{nameOrKeyQueryToSqlForResourceIndex} ESCAPE '/') - OR p.kee like #{nameOrKeyQueryToSqlForProjectKey} ESCAPE '/') + AND ri.kee like #{query.nameOrKeyQueryToSqlForResourceIndex} ESCAPE '/') + OR p.kee like #{query.nameOrKeyQueryToSqlForProjectKey} ESCAPE '/') + + + + + + + + + + + + and s.root_snapshot_id = #{query.baseSnapshot.rootId} + + + and s.root_snapshot_id = #{query.baseSnapshot.id} + + and s.path like #{query.baseSnapshotPath} ESCAPE '/' + + + + from projects p + inner join snapshots s on p.id = s.project_id + where + p.enabled=${_true} + + AND p.qualifier in + + #{qualifier} + + + + AND (exists ( + select 1 + from resource_index ri + where + ri.resource_id=p.id + AND ri.kee like #{query.nameOrKeyQueryToSqlForResourceIndex} ESCAPE '/') + OR p.kee like #{query.nameOrKeyQueryToSqlForProjectKey} ESCAPE '/') + + +