aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>2015-12-17 16:19:44 +0100
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>2015-12-21 12:27:25 +0100
commit6fe8b0cd64c4b4e2386d994016b465b0386ed09c (patch)
tree73006c620bed41a2849e4d563aa8c1981de7c38d
parent8239ac084eb9aaaba9f661a4e2fe5891bda8d452 (diff)
downloadsonarqube-6fe8b0cd64c4b4e2386d994016b465b0386ed09c.tar.gz
sonarqube-6fe8b0cd64c4b4e2386d994016b465b0386ed09c.zip
SONAR-7129 WS api/components/tree
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java1
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchAction.java8
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java273
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/permission/ws/PermissionsWsParametersBuilder.java66
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsAction.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/SetDefaultTemplateAction.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java5
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java121
-rw-r--r--server/sonar-server/src/main/resources/org/sonar/server/component/ws/tree-example.json90
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java2
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java2
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java359
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/ws/ActivityActionTest.java6
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java4
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java4
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java2
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java19
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java26
-rw-r--r--sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java188
-rw-r--r--sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml69
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java191
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java25
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java3
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java63
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/ResourceTypesRule.java21
-rw-r--r--sonar-db/src/test/java/org/sonar/db/component/SnapshotTesting.java10
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java17
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/utils/Paging.java4
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java (renamed from server/sonar-server/src/main/java/org/sonar/server/component/ws/WsComponentsParameters.java)15
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/client/component/TreeWsRequest.java127
-rw-r--r--sonar-ws/src/main/protobuf/ws-components.proto24
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;
+}