diff options
author | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2015-12-17 16:19:44 +0100 |
---|---|---|
committer | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2015-12-21 12:27:25 +0100 |
commit | 6fe8b0cd64c4b4e2386d994016b465b0386ed09c (patch) | |
tree | 73006c620bed41a2849e4d563aa8c1981de7c38d | |
parent | 8239ac084eb9aaaba9f661a4e2fe5891bda8d452 (diff) | |
download | sonarqube-6fe8b0cd64c4b4e2386d994016b465b0386ed09c.tar.gz sonarqube-6fe8b0cd64c4b4e2386d994016b465b0386ed09c.zip |
SONAR-7129 WS api/components/tree
32 files changed, 1635 insertions, 124 deletions
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<ComponentDto, WsComponents.SearchWsResponse.Component> { + private enum ComponentDToComponentResponseFunction implements Function<ComponentDto, WsComponents.Component> { 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<String> STRATEGIES = ImmutableSortedSet.of(ALL_STRATEGY, CHILDREN_STRATEGY, LEAVES_STRATEGY); + private static final String NAME_SORT = "name"; + private static final Set<String> SORTS = ImmutableSortedSet.of(NAME_SORT, "path", "qualifier"); + + private 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.<br>" + + "Requires one of the following permissions:" + + "<ul>" + + "<li>'Administer System'</li>" + + "<li>'Administer' rights on the specified project</li>" + + "<li>'Browse' on the specified project</li>" + + "</ul>", + 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:" + + "<ul>" + + "<li>children: return the direct children components of the base component. Grandchildren components are not returned</li>" + + "<li>all: return all the children components of the base component. Grandchildren are returned.</li>" + + "<li>leaves: return all the children components (files, in general) which don't have other children. They are the leaves of the component tree.</li>" + + "</ul>") + .setPossibleValues(STRATEGIES) + .setDefaultValue(ALL_STRATEGY); + } + + @Override + public void handle(Request request, Response response) throws Exception { + 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<ComponentDto> 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<ComponentDto> 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<String> 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<String> childrenQualifiers(TreeWsRequest request, String baseQualifier) { + List<String> requestQualifiers = request.getQualifiers(); + List<String> childrenQualifiers = null; + if (LEAVES_STRATEGY.equals(request.getStrategy())) { + childrenQualifiers = resourceTypes.getLeavesQualifiers(baseQualifier); + } + + if (requestQualifiers == null) { + return childrenQualifiers; + } + + if (childrenQualifiers == null) { + return requestQualifiers; + } + + // intersection of request and children qualifiers + childrenQualifiers.retainAll(requestQualifiers); + + return childrenQualifiers; + } + + private 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<String> 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("<ul>"); - String qualifierPattern = "<li>%s - %s</li>"; - for (String qualifier : getRootQualifiers(context.getResourceTypes())) { - description.append(format(qualifierPattern, qualifier, qualifierLabel(context, qualifier))); - } - description.append("</ul>"); - - 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<T extends 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<T extends 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<String> getRootQualifiers(ResourceTypes resourceTypes) { + return from(resourceTypes.getRoots()) + .transform(RESOURCE_TYPE_TO_QUALIFIER) + .toSortedSet(natural()); + } + + private static Set<String> 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<String> qualifiers) { + StringBuilder description = new StringBuilder(); + description.append("<ul>"); + String qualifierPattern = "<li>%s - %s</li>"; + for (String qualifier : qualifiers) { + description.append(format(qualifierPattern, qualifier, qualifierLabel(context, qualifier))); + } + description.append("</ul>"); + + 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<ComponentDto> selectDirectChildren(DbSession dbSession, ComponentTreeQuery componentQuery) { + RowBounds rowBounds = new RowBounds(offset(componentQuery.getPage(), componentQuery.getPageSize()), componentQuery.getPageSize()); + return mapper(dbSession).selectDirectChildren(componentQuery, rowBounds); + } + + public List<ComponentDto> selectAllChildren(DbSession dbSession, ComponentTreeQuery componentQuery) { + 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<String>, List<ComponentDto>> { 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<ComponentDto> selectComponentsByQualifiers(@Param("qualifiers") Collection<String> qualifiers); - List<ComponentDto> selectByQuery(ComponentQuery query, RowBounds rowBounds); + List<ComponentDto> selectByQuery(@Param("query") ComponentQuery query, RowBounds rowBounds); - int countByQuery(ComponentQuery query); + int countByQuery(@Param("query") ComponentQuery query); + + /** + * Return direct children components + */ + List<ComponentDto> selectDirectChildren(@Param("query") ComponentTreeQuery componentTreeQuery, RowBounds rowBounds); + + int countDirectChildren(@Param("query") ComponentTreeQuery componentTreeQuery); + + /** + * Return all children components. + */ + List<ComponentDto> 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<ComponentDto> 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<FilePathWithHashDto> 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<ComponentDto> 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<String> 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<String> 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<String> sortFields, String direction) { + List<String> sqlSortFields = from(sortFields) + .transform(new SortFieldToSqlSortFieldFunction(direction)).toList(); + + return Joiner.on(", ").join(sqlSortFields); + } + + public static class Builder { + @CheckForNull + private String nameOrKeyQuery; + @CheckForNull + private Collection<String> qualifiers; + @CheckForNull + private Integer page; + @CheckForNull + private Integer pageSize; + private SnapshotDto baseSnapshot; + private List<String> 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<String> 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<String> sorts) { + this.sortFields = requireNonNull(sorts); + return this; + } + + public Builder setAsc(boolean asc) { + this.asc = asc; + return this; + } + } + + private static class SortFieldToSqlSortFieldFunction implements Function<String, String> { + 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 - <foreach collection="qualifiers" item="qualifier" open="(" close=")" separator=","> + <foreach collection="query.qualifiers" item="qualifier" open="(" close=")" separator=","> #{qualifier} </foreach> - <if test="nameOrKeyQuery!=null"> + <if test="query.nameOrKeyQuery!=null"> AND (exists ( select 1 from resource_index ri where ri.resource_id=p.id AND ri.qualifier in - <foreach collection="qualifiers" item="qualifier" open="(" close=")" separator=","> + <foreach collection="query.qualifiers" item="qualifier" open="(" close=")" separator=","> #{qualifier} </foreach> - 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 '/') </if> </where> </sql> + <select id="selectDirectChildren" resultType="Component"> + select + <include refid="componentColumns"/> + <include refid="sqlSelectByTreeQuery"/> + and s.parent_snapshot_id = #{query.baseSnapshot.id} + order by ${query.sqlSort} + </select> + + <select id="countDirectChildren" resultType="int"> + select count(p.id) + <include refid="sqlSelectByTreeQuery"/> + and s.parent_snapshot_id = #{query.baseSnapshot.id} + </select> + + <select id="selectAllChildren" resultType="Component"> + select + <include refid="componentColumns"/> + <include refid="sqlSelectAllChildren" /> + order by ${query.sqlSort} + </select> + + <select id="countAllChildren" resultType="int"> + select count(p.id) + <include refid="sqlSelectAllChildren"/> + </select> + + <sql id="sqlSelectAllChildren"> + <include refid="sqlSelectByTreeQuery"/> + <if test="query.baseSnapshot.rootId!=null"> + and s.root_snapshot_id = #{query.baseSnapshot.rootId} + </if> + <if test="query.baseSnapshot.rootId==null"> + and s.root_snapshot_id = #{query.baseSnapshot.id} + </if> + and s.path like #{query.baseSnapshotPath} ESCAPE '/' + </sql> + + <sql id="sqlSelectByTreeQuery"> + from projects p + inner join snapshots s on p.id = s.project_id + where + p.enabled=${_true} + <if test="query.qualifiers!=null"> + AND p.qualifier in + <foreach collection="query.qualifiers" item="qualifier" open="(" close=")" separator=","> + #{qualifier} + </foreach> + </if> + <if test="query.nameOrKeyQuery!=null"> + 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 '/') + </if> + </sql> + <select id="countRootComponents" resultType="int"> select count(p.id) from projects p diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java index 129a2c4f117..5cf3d42c5f7 100644 --- a/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java @@ -42,6 +42,8 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.guava.api.Assertions.assertThat; import static org.sonar.db.component.ComponentTesting.newDeveloper; +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; @@ -672,11 +674,11 @@ public class ComponentDaoTest { @Test public void select_by_query_with_paging_query_and_qualifiers() { - componentDb.insertProjectAndSnapshot(dbSession, newProjectDto().setName("aaaa-name")); - componentDb.insertProjectAndSnapshot(dbSession, newView()); - componentDb.insertProjectAndSnapshot(dbSession, newDeveloper("project-name")); + componentDb.insertProjectAndSnapshot(newProjectDto().setName("aaaa-name")); + componentDb.insertProjectAndSnapshot(newView()); + componentDb.insertProjectAndSnapshot(newDeveloper("project-name")); for (int i = 9; i >= 1; i--) { - componentDb.insertProjectAndSnapshot(dbSession, newProjectDto().setName("project-" + i)); + componentDb.insertProjectAndSnapshot(newProjectDto().setName("project-" + i)); } db.commit(); componentDb.indexProjects(); @@ -691,7 +693,7 @@ public class ComponentDaoTest { @Test public void select_by_query_name_with_special_characters() { - componentDb.insertProjectAndSnapshot(dbSession, newProjectDto().setName("project-\\_%/-name")); + componentDb.insertProjectAndSnapshot(newProjectDto().setName("project-\\_%/-name")); db.commit(); componentDb.indexProjects(); @@ -704,7 +706,7 @@ public class ComponentDaoTest { @Test public void select_by_query_key_with_special_characters() { - componentDb.insertProjectAndSnapshot(dbSession, newProjectDto() + componentDb.insertProjectAndSnapshot(newProjectDto() .setKey("project-_%-key")); db.commit(); componentDb.indexProjects(); @@ -715,4 +717,181 @@ public class ComponentDaoTest { assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo("project-_%-key"); } + + @Test + public void select_direct_children_of_a_project() { + ComponentDto project = newProjectDto().setKey("project-key").setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-1-uuid", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-1-uuid"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-2-uuid"), moduleSnapshot); + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(projectSnapshot).build(); + + List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query); + int count = underTest.countDirectChildren(dbSession, query); + + assertThat(count).isEqualTo(2); + assertThat(result).extracting("uuid").containsExactly("file-1-uuid", "module-1-uuid"); + } + + @Test + public void select_direct_children_with_name_query() { + ComponentDto project = newProjectDto().setKey("project-key").setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-1-uuid", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-1-uuid").setName("file-name-1"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-2-uuid").setName("file-name-2"), moduleSnapshot); + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(projectSnapshot) + .setNameOrKeyQuery("file-name").build(); + + List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query); + int count = underTest.countDirectChildren(dbSession, query); + + assertThat(count).isEqualTo(1); + assertThat(result).extracting("uuid").containsExactly("file-1-uuid"); + } + + @Test + public void select_direct_children_with_key_query() { + ComponentDto project = newProjectDto().setKey("project-key").setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-1-uuid", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-1-uuid").setKey("file-key-1"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-2-uuid").setKey("file-key-2"), moduleSnapshot); + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(projectSnapshot) + .setNameOrKeyQuery("file-key").build(); + + List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query); + int count = underTest.countDirectChildren(dbSession, query); + + assertThat(count).isEqualTo(1); + assertThat(result).extracting("uuid").containsExactly("file-1-uuid"); + } + + @Test + public void select_direct_children_with_pagination() { + ComponentDto project = newProjectDto().setKey("project-key").setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + for (int i = 1; i <= 9; i++) { + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-uuid-" + i), projectSnapshot); + } + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(projectSnapshot) + .setPage(2) + .setPageSize(3) + .setAsc(false) + .build(); + + List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query); + int count = underTest.countDirectChildren(dbSession, query); + + assertThat(count).isEqualTo(9); + assertThat(result).extracting("uuid").containsExactly("file-uuid-6", "file-uuid-5", "file-uuid-4"); + } + + @Test + public void select_direct_children_with_order_by_path() { + ComponentDto project = newProjectDto(); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-uuid-1").setName("file-name-1").setPath("3"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-uuid-2").setName("file-name-2").setPath("2"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-uuid-3").setName("file-name-3").setPath("1"), projectSnapshot); + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(projectSnapshot) + .setSortFields(singletonList("path")) + .setAsc(true) + .build(); + + List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query); + + assertThat(result).extracting("uuid").containsExactly("file-uuid-3", "file-uuid-2", "file-uuid-1"); + } + + @Test + public void select_direct_children_of_a_module() { + ComponentDto project = newProjectDto(); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-1-uuid", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-1-uuid"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-2-uuid"), moduleSnapshot); + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(moduleSnapshot).build(); + + List<ComponentDto> result = underTest.selectDirectChildren(dbSession, query); + + assertThat(result).extracting("uuid").containsOnly("file-2-uuid"); + } + + @Test + public void select_all_children_of_a_project() { + ComponentDto project = newProjectDto().setKey("project-key").setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-1-uuid", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-1-uuid"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-2-uuid"), moduleSnapshot); + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(projectSnapshot).build(); + + List<ComponentDto> result = underTest.selectAllChildren(dbSession, query); + int count = underTest.countAllChildren(dbSession, query); + + assertThat(count).isEqualTo(3); + assertThat(result).extracting("uuid").containsExactly("file-1-uuid", "file-2-uuid", "module-1-uuid"); + } + + @Test + public void select_all_files_of_a_project_paginated_and_ordered() { + ComponentDto project = newProjectDto().setKey("project-key").setUuid("project-uuid"); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + SnapshotDto moduleSnapshot = componentDb.insertComponentAndSnapshot(newModuleDto("module-1-uuid", project), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-uuid-1").setName("file-name-1"), projectSnapshot); + componentDb.insertComponentAndSnapshot(newFileDto(project, "another-uuid"), projectSnapshot); + for (int i = 2; i <= 9; i++) { + componentDb.insertComponentAndSnapshot(newFileDto(project, "file-uuid-" + i).setName("file-name-" + i), moduleSnapshot); + } + db.commit(); + componentDb.indexProjects(); + + ComponentTreeQuery query = newTreeQuery(projectSnapshot) + .setQualifiers(newArrayList(Qualifiers.FILE)) + .setPage(2) + .setPageSize(3) + .setNameOrKeyQuery("file-name") + .setSortFields(singletonList("name")) + .setAsc(false) + .build(); + + List<ComponentDto> result = underTest.selectAllChildren(dbSession, query); + int count = underTest.countAllChildren(dbSession, query); + + assertThat(count).isEqualTo(9); + assertThat(result).extracting("uuid").containsExactly("file-uuid-6", "file-uuid-5", "file-uuid-4"); + } + + private static ComponentTreeQuery.Builder newTreeQuery(SnapshotDto baseSnapshot) { + return ComponentTreeQuery.builder() + .setPage(1) + .setPageSize(500) + .setBaseSnapshot(baseSnapshot) + .setSortFields(singletonList("name")) + .setAsc(true) + .setQualifiers(newArrayList(Qualifiers.FILE, Qualifiers.MODULE, Qualifiers.DIRECTORY, Qualifiers.PROJECT)); + } } diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java index 731a05a866a..74932cfe65e 100644 --- a/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java +++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java @@ -24,7 +24,9 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import static org.sonar.db.component.SnapshotTesting.createForComponent; import static org.sonar.db.component.SnapshotTesting.newSnapshotForProject; +import static org.sonar.db.component.SnapshotTesting.newSnapshotForView; public class ComponentDbTester { private final DbTester db; @@ -37,9 +39,28 @@ public class ComponentDbTester { this.dbSession = db.getSession(); } - public void insertProjectAndSnapshot(DbSession dbSession, ComponentDto component) { + public SnapshotDto insertProjectAndSnapshot(ComponentDto component) { dbClient.componentDao().insert(dbSession, component); - dbClient.snapshotDao().insert(dbSession, newSnapshotForProject(component)); + SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, newSnapshotForProject(component)); + db.commit(); + + return snapshot; + } + + public SnapshotDto insertViewAndSnapshot(ComponentDto component) { + dbClient.componentDao().insert(dbSession, component); + SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, newSnapshotForView(component)); + db.commit(); + + return snapshot; + } + + public SnapshotDto insertComponentAndSnapshot(ComponentDto component, SnapshotDto parentSnapshot) { + dbClient.componentDao().insert(dbSession, component); + SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, createForComponent(component, parentSnapshot)); + db.commit(); + + return snapshot; } public ComponentDto insertComponent(ComponentDto component) { diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java index 11c3cebbc8c..8ac8cb816b2 100644 --- a/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java +++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java @@ -20,6 +20,7 @@ package org.sonar.db.component; +import java.util.Date; import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Scopes; import org.sonar.core.util.Uuids; @@ -42,6 +43,7 @@ public class ComponentTesting { .setScope(Scopes.FILE) .setQualifier(Qualifiers.FILE) .setPath(path) + .setCreatedAt(new Date()) .setLanguage("xoo"); } @@ -168,6 +170,7 @@ public class ComponentTesting { .setModuleUuid(module.uuid()) .setModuleUuidPath(module.moduleUuidPath()) .setParentProjectId(module.getId()) + .setCreatedAt(new Date()) .setEnabled(true); } } diff --git a/sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java b/sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java new file mode 100644 index 00000000000..c3a1695dac4 --- /dev/null +++ b/sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java @@ -0,0 +1,63 @@ +/* + * 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 java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; + +public class ComponentTreeQueryTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void convert_sorts_in_sql_representation() { + ComponentTreeQuery result = ComponentTreeQuery.builder() + .setBaseSnapshot(new SnapshotDto()) + .setSortFields(newArrayList("name", "path", "qualifier")) + .build(); + + assertThat(result.getSqlSort()).isEqualTo("LOWER(p.name) ASC, p.name ASC, LOWER(p.path) ASC, p.path ASC, LOWER(p.qualifier) ASC, p.qualifier ASC"); + } + + @Test + public void fail_if_no_base_snapshot() { + expectedException.expect(NullPointerException.class); + + ComponentTreeQuery.builder() + .setSortFields(Collections.<String>emptyList()) + .build(); + } + + @Test + public void fail_if_no_sort() { + expectedException.expect(NullPointerException.class); + + ComponentTreeQuery.builder() + .setBaseSnapshot(new SnapshotDto()) + .build(); + } +} diff --git a/sonar-db/src/test/java/org/sonar/db/component/ResourceTypesRule.java b/sonar-db/src/test/java/org/sonar/db/component/ResourceTypesRule.java index e305ca60a63..23d00b9f211 100644 --- a/sonar-db/src/test/java/org/sonar/db/component/ResourceTypesRule.java +++ b/sonar-db/src/test/java/org/sonar/db/component/ResourceTypesRule.java @@ -20,6 +20,8 @@ package org.sonar.db.component; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -31,6 +33,8 @@ import org.sonar.api.resources.ResourceTypes; public class ResourceTypesRule extends ResourceTypes { private final Set<ResourceType> allResourceTypes = new HashSet<>(); private final Set<ResourceType> rootResourceTypes = new HashSet<>(); + private final List<String> childrenQualifiers = new ArrayList<>(); + private final List<String> leavesQualifiers = new ArrayList<>(); @Override public Collection<ResourceType> getAll() { @@ -51,6 +55,19 @@ public class ResourceTypesRule extends ResourceTypes { return this; } + public ResourceTypesRule setLeavesQualifiers(String... qualifiers) { + leavesQualifiers.clear(); + leavesQualifiers.addAll(Arrays.asList(qualifiers)); + return this; + } + + public ResourceTypesRule setChildrenQualifiers(String... qualifiers) { + childrenQualifiers.clear(); + childrenQualifiers.addAll(Arrays.asList(qualifiers)); + + return this; + } + public ResourceTypesRule setAllQualifiers(String... qualifiers) { allResourceTypes.clear(); for (String qualifier : qualifiers) { @@ -82,7 +99,7 @@ public class ResourceTypesRule extends ResourceTypes { @Override public List<String> getChildrenQualifiers(String qualifier) { - throw new UnsupportedOperationException(); + return this.childrenQualifiers; } @Override @@ -92,7 +109,7 @@ public class ResourceTypesRule extends ResourceTypes { @Override public List<String> getLeavesQualifiers(String qualifier) { - throw new UnsupportedOperationException(); + return this.leavesQualifiers; } @Override diff --git a/sonar-db/src/test/java/org/sonar/db/component/SnapshotTesting.java b/sonar-db/src/test/java/org/sonar/db/component/SnapshotTesting.java index 65d2aca5c72..727adc6f0f4 100644 --- a/sonar-db/src/test/java/org/sonar/db/component/SnapshotTesting.java +++ b/sonar-db/src/test/java/org/sonar/db/component/SnapshotTesting.java @@ -20,20 +20,22 @@ package org.sonar.db.component; -import com.google.common.base.Preconditions; import org.assertj.core.util.Strings; +import static com.google.common.base.Preconditions.checkNotNull; + public class SnapshotTesting { /** * Can be used for modules and files */ public static SnapshotDto createForComponent(ComponentDto component, SnapshotDto parentSnapshot) { - Preconditions.checkNotNull(parentSnapshot.getId(), "The parent snapshot need to be persisted before creating this snapshot"); + checkNotNull(parentSnapshot.getId(), "The parent snapshot need to be persisted before creating this snapshot"); Long parentRootId = parentSnapshot.getRootId(); return createBasicSnapshot(component, parentSnapshot.getRootProjectId()) .setRootId(parentRootId != null ? parentRootId : parentSnapshot.getId()) .setParentId(parentSnapshot.getId()) + .setDepth(parentSnapshot.getDepth()+1) .setPath( Strings.isNullOrEmpty(parentSnapshot.getPath()) ? Long.toString(parentSnapshot.getId()) + "." : parentSnapshot.getPath() + Long.toString(parentSnapshot.getId()) + "."); } @@ -57,8 +59,8 @@ public class SnapshotTesting { } private static SnapshotDto createBasicSnapshot(ComponentDto component, Long rootProjectId) { - Preconditions.checkNotNull(component.getId(), "The project need to be persisted before creating this snapshot"); - Preconditions.checkNotNull(rootProjectId, "Root project id is null"); + checkNotNull(component.getId(), "The project need to be persisted before creating this snapshot"); + checkNotNull(rootProjectId, "Root project id is null"); return new SnapshotDto() .setComponentId(component.getId()) .setRootProjectId(rootProjectId) diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java index 0fe59142154..6dd6c57898a 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java @@ -411,8 +411,23 @@ public interface WebService extends Definable<WebService.Context> { * Add predefined parameters related to sorting of results. */ public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { + genericAddSortParam(possibleValues, defaultValue, defaultAscending, "Sort field"); + + return this; + } + + /** + * Add predefined parameters related to sorting of results. Comma-separated list + */ + public <V> NewAction addMultiSortsParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { + genericAddSortParam(possibleValues, defaultValue, defaultAscending, "Comma-separated list of sort fields"); + + return this; + } + + public <V> NewAction genericAddSortParam(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending, String description) { createParam(Param.SORT) - .setDescription("Sort field") + .setDescription(description) .setDeprecatedKey("sort") .setDefaultValue(defaultValue) .setPossibleValues(possibleValues); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/Paging.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/Paging.java index d9439514a3c..f9b49c528c3 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/utils/Paging.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/Paging.java @@ -82,6 +82,10 @@ public class Paging { return (pageIndex - 1) * pageSize; } + public static int offset(int pageIndex, int pageSize) { + return (pageIndex - 1) * pageSize; + } + /** * Number of pages. It is greater than or equal 0. */ diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/WsComponentsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java index f33b3ad9b88..9467eb8adc9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/WsComponentsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java @@ -18,12 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.component.ws; +package org.sonarqube.ws.client.component; -class WsComponentsParameters { - private WsComponentsParameters() { +public class ComponentsWsParameters { + private ComponentsWsParameters() { // static utility class } - static final String PARAM_QUALIFIERS = "qualifiers"; + //actions + public static final String ACTION_TREE = "tree"; + + // parameters + public static final String PARAM_QUALIFIERS = "qualifiers"; + public static final String PARAM_BASE_COMPONENT_ID = "baseComponentId"; + public static final String PARAM_BASE_COMPONENT_KEY = "baseComponentKey"; + public static final String PARAM_STRATEGY = "strategy"; } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/TreeWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/TreeWsRequest.java new file mode 100644 index 00000000000..e3c8ebb468d --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/TreeWsRequest.java @@ -0,0 +1,127 @@ +/* + * 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.sonarqube.ws.client.component; + +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +public class TreeWsRequest { + @CheckForNull + private String baseComponentId; + @CheckForNull + private String baseComponentKey; + @CheckForNull + private String strategy; + @CheckForNull + private List<String> qualifiers; + @CheckForNull + private String query; + @CheckForNull + private List<String> sort; + @CheckForNull + private Boolean asc; + @CheckForNull + private Integer page; + @CheckForNull + private Integer pageSize; + + public String getBaseComponentId() { + return baseComponentId; + } + + public TreeWsRequest setBaseComponentId(@Nullable String baseComponentId) { + this.baseComponentId = baseComponentId; + return this; + } + + public String getBaseComponentKey() { + return baseComponentKey; + } + + public TreeWsRequest setBaseComponentKey(@Nullable String baseComponentKey) { + this.baseComponentKey = baseComponentKey; + return this; + } + + public String getStrategy() { + return strategy; + } + + public TreeWsRequest setStrategy(@Nullable String strategy) { + this.strategy = strategy; + return this; + } + + public List<String> getQualifiers() { + return qualifiers; + } + + public TreeWsRequest setQualifiers(@Nullable List<String> qualifiers) { + this.qualifiers = qualifiers; + return this; + } + + public String getQuery() { + return query; + } + + public TreeWsRequest setQuery(@Nullable String query) { + this.query = query; + return this; + } + + public List<String> getSort() { + return sort; + } + + public TreeWsRequest setSort(@Nullable List<String> sort) { + this.sort = sort; + return this; + } + + public Boolean getAsc() { + return asc; + } + + public TreeWsRequest setAsc(boolean asc) { + this.asc = asc; + return this; + } + + public Integer getPage() { + return page; + } + + public TreeWsRequest setPage(int page) { + this.page = page; + return this; + } + + public Integer getPageSize() { + return pageSize; + } + + public TreeWsRequest setPageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } +} diff --git a/sonar-ws/src/main/protobuf/ws-components.proto b/sonar-ws/src/main/protobuf/ws-components.proto index 2f94e41f670..aad40d8cab2 100644 --- a/sonar-ws/src/main/protobuf/ws-components.proto +++ b/sonar-ws/src/main/protobuf/ws-components.proto @@ -28,13 +28,23 @@ option optimize_for = SPEED; // WS api/components/search message SearchWsResponse { - message Component { - optional string id = 1; - optional string key = 2; - optional string qualifier = 3; - optional string name = 4; - } - optional sonarqube.ws.commons.Paging paging = 1; repeated Component components = 2; } + +// WS api/components/tree +message TreeWsResponse { + optional sonarqube.ws.commons.Paging paging = 1; + optional string projectId = 2; + repeated Component components = 3; +} + +message Component { + optional string id = 1; + optional string key = 2; + optional string projectId = 3; + optional string name = 4; + optional string description = 5; + optional string qualifier = 6; + optional string path = 7; +} |