]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7129 WS api/components/tree 698/head
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Thu, 17 Dec 2015 15:19:44 +0000 (16:19 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Mon, 21 Dec 2015 11:27:25 +0000 (12:27 +0100)
33 files changed:
server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/component/ws/WsComponentsParameters.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/permission/ws/PermissionsWsParametersBuilder.java
server/sonar-server/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsAction.java
server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/SetDefaultTemplateAction.java
server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java
server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/component/ws/tree-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/ws/ActivityActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java
sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java
sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java [new file with mode: 0644]
sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml
sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java
sonar-db/src/test/java/org/sonar/db/component/ComponentDbTester.java
sonar-db/src/test/java/org/sonar/db/component/ComponentTesting.java
sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/component/ResourceTypesRule.java
sonar-db/src/test/java/org/sonar/db/component/SnapshotTesting.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
sonar-plugin-api/src/main/java/org/sonar/api/utils/Paging.java
sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/component/TreeWsRequest.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-components.proto

index b9454a2545dd2fecb0cecb86cfe753ef125dc556..6c17563e99cf8a12cac29b20e3d33106e34b9f8d 100644 (file)
@@ -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);
index 6e80a568f7a9c7907ca3cc0139b0c36a3f117d5c..2b6896679dca95efb36a1ea735090e439cb315c6 100644 (file)
@@ -32,6 +32,7 @@ public class ComponentsWsModule extends Module {
       // actions
       AppAction.class,
       SearchAction.class,
+      TreeAction.class,
       SearchViewComponentsAction.class);
   }
 }
index 43807a65a3a8a8375ca322214a0e7111387e70c4..ad46f1d3a50ace6ae35eecd30a6d93b483e4388b 100644 (file)
@@ -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 (file)
index 0000000..8a8c4ed
--- /dev/null
@@ -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/component/ws/WsComponentsParameters.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/WsComponentsParameters.java
deleted file mode 100644 (file)
index f33b3ad..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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;
-
-class WsComponentsParameters {
-  private WsComponentsParameters() {
-    // static utility class
-  }
-
-  static final String PARAM_QUALIFIERS = "qualifiers";
-}
index 72d39c950d4cc72491349c13ce1327861f61fa9a..e36d212b4663daa9120c58e5525ffa6195db034e 100644 (file)
 
 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;
-    }
-  }
 }
index 41f5000ea2baa04a00ef0aa9a3a7bdb3accfa235..40b81f067222ee510d55cafe7cbaaff14902ffc8 100644 (file)
@@ -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");
   }
 
index 5d98cfb0b85ceec257e4d1c21c4b994fe9c92525..14735422589eed71b9d3f4bb4e92ae071a83bb89 100644 (file)
@@ -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);
   }
 
index 99b43978e7d34db3fc53ca864c9aadd5aa21cfa3..6fa59c1570e1ea4034351141a57931908c8ff3fc 100644 (file)
@@ -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 (file)
index 0000000..8e36801
--- /dev/null
@@ -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 (file)
index 0000000..e5e4ef1
--- /dev/null
@@ -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"
+    }
+  ]
+}
index d68d694fb36f857eb8932fc02d4e76ef311405fb..5e854bb1f5f92375f912588a5255a7b77987f913 100644 (file)
@@ -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);
   }
 }
index 140843d463e05de02ff4364c120fbeb384afeee5..5d557311a734a9c6d7e5dcaaf8f1df7460097c5d 100644 (file)
@@ -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 (file)
index 0000000..6a979e9
--- /dev/null
@@ -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);
+  }
+}
index a3f97031f1224f2efdfd61ac9392f4ed0ae23cfe..fc7671a6ae7be8ca32461bbc2badee83dc4dd756 100644 (file)
@@ -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);
index 7aa2f325cc27d663404064ce044bdfdcbc89b17b..5f261649a37c3d22368c7c0dc18fae25258166f2 100644 (file)
@@ -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);
 
index 71fa47a68b54d80702048b1c054bc6cd4eca7c72..3e2472abf7e710f1bbb63c43ca04f05da3524911 100644 (file)
@@ -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();
   }
 
index 26f60a1a93adaec9d7f2343b4ad1faf9789a4af0..a8b8aa4c55fcd8b1a49a78dcdca5c61c8d0342b7 100644 (file)
@@ -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();
index 4b8f62b0e065915dbd818116863c2f4951c7a99a..14371abbcf18ef77e1d4932f261b42ca4e22112c 100644 (file)
@@ -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;
 
index 42a59729461380e93ac45ccd7018e72ad9a54934..47e3191cca9c9b0e56cc762d80a912ed04967d7f 100644 (file)
@@ -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 (file)
index 0000000..749c21e
--- /dev/null
@@ -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);
+    }
+  }
+}
index c9c9205ec8e09fd0af06b666f745ab0fb336ee42..f88928f83ca2d3b55bc43af7b24e08a1b3303dbe 100644 (file)
       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
index 129a2c4f117d26acce013afad1462ac5e2f9232f..5cf3d42c5f7f283b10bdb6f8da359ab044756811 100644 (file)
@@ -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));
+  }
 }
index 731a05a866a62fcdaf9b21d8d48b5cd53a98bee4..74932cfe65ed4cbb763f9c931e5645524f4616fb 100644 (file)
@@ -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) {
index 11c3cebbc8c8e577b1bf8dc01cc04b8ee0c690e0..8ac8cb816b26edc4f83d6d3854bf866c16715eec 100644 (file)
@@ -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 (file)
index 0000000..c3a1695
--- /dev/null
@@ -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();
+  }
+}
index e305ca60a6308d5bfbb8f55bb004c6a6defaae2b..23d00b9f211e6957503ef2e193af432a25fa5689 100644 (file)
@@ -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
index 65d2aca5c72c3e7145d6755c21f07a52141a9fd9..727adc6f0f4f90856bad0d62c4ac57c0d04e7fe5 100644 (file)
 
 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)
index 0fe59142154d887f03f6db2981231918d6b384c4..6dd6c57898afbfed9aa11e412bdda190662cb6ec 100644 (file)
@@ -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);
index d9439514a3c458b2231448569074fe62ae7008de..f9b49c528c3668831b0475e375b591252702133b 100644 (file)
@@ -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/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java
new file mode 100644 (file)
index 0000000..9467eb8
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public class ComponentsWsParameters {
+  private ComponentsWsParameters() {
+    // static utility class
+  }
+
+  //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 (file)
index 0000000..e3c8ebb
--- /dev/null
@@ -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;
+  }
+}
index 2f94e41f670dc7655c479647ff81f7160ea89c1e..aad40d8cab27d7e8cccf19f7ff6f33f8264d78dc 100644 (file)
@@ -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;
+}