]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7298 Deprecate and rewrite WS api/projects/index in Java
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 23 Jan 2017 17:12:30 +0000 (18:12 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 24 Jan 2017 17:36:49 +0000 (18:36 +0100)
24 files changed:
server/sonar-server/src/main/java/org/sonar/server/project/ws/IndexAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWs.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java
server/sonar-server/src/main/java/org/sonar/server/property/ws/IndexAction.java
server/sonar-server/src/main/java/org/sonar/server/ws/DeprecatedRestWebServiceFilter.java
server/sonar-server/src/main/resources/org/sonar/server/project/ws/index-example.json [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/project/ws/projects-example-index.json [deleted file]
server/sonar-server/src/test/java/org/sonar/server/project/ws/DeleteActionTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/IndexActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsTest.java [deleted file]
server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/empty.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/return_only_projects_authorized_for_user.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_id.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_key.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_name.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_projects.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_projects_with_modules.json [new file with mode: 0644]
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/resources/org/sonar/db/component/ComponentMapper.xml
sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RailsHandler.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java

diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/IndexAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/IndexAction.java
new file mode 100644 (file)
index 0000000..fb9ea3c
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.project.ws;
+
+import com.google.common.io.Resources;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+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.utils.text.JsonWriter;
+import org.sonar.core.util.stream.Collectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.user.UserSession;
+
+import static java.util.Optional.ofNullable;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.core.util.stream.Collectors.toList;
+import static org.sonar.core.util.stream.Collectors.uniqueIndex;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_INDEX;
+
+/**
+ * This web service is used by old version of SonarLint.
+ */
+public class IndexAction implements ProjectsWsAction {
+
+  private static final String PARAM_KEY = "key";
+  private static final String PARAM_SEARCH = "search";
+  private static final String PARAM_SUB_PROJECTS = "subprojects";
+  private static final String PARAM_FORMAT = "format";
+
+  private final DbClient dbClient;
+  private final UserSession userSession;
+
+  public IndexAction(DbClient dbClient, UserSession userSession) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction(ACTION_INDEX)
+      .setDescription("This web service is deprecated, please use api/components/search instead")
+      .setSince("2.10")
+      .setDeprecatedSince("6.3")
+      .setHandler(this)
+      .setResponseExample(Resources.getResource(this.getClass(), "index-example.json"));
+    action.createParam(PARAM_KEY)
+      .setDescription("key or id of the project")
+      .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+    action.createParam(PARAM_SEARCH)
+      .setDescription("Substring of project name, case insensitive. Ignored if the parameter key is set")
+      .setExampleValue("Sonar");
+    action.createParam(PARAM_SUB_PROJECTS)
+      .setDescription("Load sub-projects. Ignored if the parameter key is set")
+      .setDefaultValue("false")
+      .setBooleanPossibleValues();
+    action.createParam(PARAM_FORMAT)
+      .setDescription("Only json response format is available")
+      .setPossibleValues("json");
+    addRemovedParameter("desc", action);
+    addRemovedParameter("views", action);
+    addRemovedParameter("libs", action);
+    addRemovedParameter("versions", action);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      List<ComponentDto> projects = getAuthorizedProjects(dbSession, searchProjects(dbSession, request));
+      JsonWriter json = response.newJsonWriter();
+      json.beginArray();
+      for (ComponentDto project : projects) {
+        addProject(json, project);
+      }
+      json.endArray();
+      json.close();
+    }
+  }
+
+  private Optional<ComponentDto> getProjectByKeyOrId(DbSession dbSession, String component) {
+    try {
+      Long componentId = Long.parseLong(component);
+      return ofNullable(dbClient.componentDao().selectById(dbSession, componentId).orNull());
+    } catch (NumberFormatException e) {
+      return ofNullable(dbClient.componentDao().selectByKey(dbSession, component).orNull());
+    }
+  }
+
+  private List<ComponentDto> searchProjects(DbSession dbSession, Request request) {
+    String projectKey = request.param(PARAM_KEY);
+    List<ComponentDto> projects = new ArrayList<>();
+    if (projectKey != null) {
+      getProjectByKeyOrId(dbSession, projectKey).ifPresent(projects::add);
+    } else {
+      String nameQuery = request.param(PARAM_SEARCH);
+      boolean includeModules = request.paramAsBoolean(PARAM_SUB_PROJECTS);
+      projects.addAll(dbClient.componentDao().selectProjectsByNameQuery(dbSession, nameQuery, includeModules));
+    }
+    return projects;
+  }
+
+  private List<ComponentDto> getAuthorizedProjects(DbSession dbSession, List<ComponentDto> projectDtos) {
+    if (projectDtos.isEmpty()) {
+      return Collections.emptyList();
+    }
+    Map<String, Long> projectIdsByUuids = projectDtos.stream().collect(uniqueIndex(ComponentDto::uuid, ComponentDto::getId));
+    Set<Long> authorizedProjectIds = dbClient.authorizationDao().keepAuthorizedProjectIds(dbSession,
+      projectDtos.stream().map(ComponentDto::getId).collect(toList()),
+      userSession.getUserId(), USER);
+    return projectDtos.stream()
+      .filter(c -> authorizedProjectIds.contains(projectIdsByUuids.get(c.projectUuid())))
+      .collect(Collectors.toList());
+  }
+
+  private static void addProject(JsonWriter json, ComponentDto project) {
+    json.beginObject()
+      .prop("id", project.getId())
+      .prop("k", project.getKey())
+      .prop("nm", project.name())
+      .prop("sc", project.scope())
+      .prop("qu", project.qualifier())
+      .endObject();
+  }
+
+  private static void addRemovedParameter(String key, WebService.NewAction action) {
+    action.createParam(key)
+      .setDescription("Since 6.3, this parameter has no effect")
+      .setDeprecatedKey("6.3");
+  }
+
+}
index e6a42102996309816e2577dc82d428c3f1554d71..b15f7362dacf6acd18319fcff2c7235261cd3b57 100644 (file)
  */
 package org.sonar.server.project.ws;
 
-import com.google.common.io.Resources;
 import java.util.Arrays;
-import org.sonar.api.server.ws.RailsHandler;
 import org.sonar.api.server.ws.WebService;
 
-import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.CONTROLLER;
 
 public class ProjectsWs implements WebService {
-  public static final String ENDPOINT = "api/projects";
-  private static final String FALSE = "false";
-  private static final String TRUE = "true";
 
   private final ProjectsWsAction[] actions;
 
@@ -39,56 +34,11 @@ public class ProjectsWs implements WebService {
 
   @Override
   public void define(Context context) {
-    NewController controller = context.createController(ENDPOINT)
+    NewController controller = context.createController(CONTROLLER)
       .setSince("2.10")
       .setDescription("Manage project existence.");
-
-    defineIndexAction(controller);
     Arrays.stream(actions).forEach(action -> action.define(controller));
     controller.done();
   }
 
-  private void defineIndexAction(NewController controller) {
-    WebService.NewAction action = controller.createAction("index")
-      .setDescription("Search for projects")
-      .setSince("2.10")
-      .setHandler(RailsHandler.INSTANCE)
-      .setResponseExample(Resources.getResource(this.getClass(), "projects-example-index.json"));
-
-    action.createParam("key")
-      .setDescription("id or key of the project")
-      .setExampleValue(KEY_PROJECT_EXAMPLE_001);
-
-    action.createParam("search")
-      .setDescription("Substring of project name, case insensitive")
-      .setExampleValue("Sonar");
-
-    action.createParam("desc")
-      .setDescription("Load project description")
-      .setDefaultValue(TRUE)
-      .setBooleanPossibleValues();
-
-    action.createParam("subprojects")
-      .setDescription("Load sub-projects. Ignored if the parameter key is set")
-      .setDefaultValue(FALSE)
-      .setBooleanPossibleValues();
-
-    action.createParam("views")
-      .setDescription("Load views and sub-views. Ignored if the parameter key is set")
-      .setDefaultValue(FALSE)
-      .setBooleanPossibleValues();
-
-    action.createParam("libs")
-      .setDescription("Load libraries. Ignored if the parameter key is set")
-      .setDefaultValue(FALSE)
-      .setBooleanPossibleValues();
-
-    action.createParam("versions")
-      .setDescription("Load version")
-      .setDefaultValue(FALSE)
-      .setPossibleValues(TRUE, FALSE, "last");
-
-    RailsHandler.addFormatParam(action);
-  }
-
 }
index e3e5138bf554a1408b3face3bbe9f5d0e5d682df..8a06f08e34a499662b664b4dc608517b38a705b8 100644 (file)
@@ -27,6 +27,7 @@ public class ProjectsWsModule extends Module {
     add(
       ProjectsWs.class,
       CreateAction.class,
+      IndexAction.class,
       BulkDeleteAction.class,
       DeleteAction.class,
       GhostsAction.class,
index c4205ff3f54252667e25ab09b65084c21f8b5e3c..cdeb026538bece59663764657892532e0d0a42b6 100644 (file)
@@ -49,8 +49,6 @@ import org.sonar.server.ws.WsAction;
 
 import static org.apache.commons.lang.StringUtils.isEmpty;
 import static org.sonar.api.PropertyType.PROPERTY_SET;
-import static org.sonar.api.server.ws.RailsHandler.PARAM_FORMAT;
-import static org.sonar.api.server.ws.RailsHandler.addJsonOnlyFormatParam;
 import static org.sonar.api.web.UserRole.ADMIN;
 import static org.sonar.server.setting.ws.SettingsPermissionPredicates.DOT_LICENSE;
 import static org.sonar.server.setting.ws.SettingsPermissionPredicates.DOT_SECURED;
@@ -64,6 +62,7 @@ public class IndexAction implements WsAction {
 
   public static final String PARAM_ID = "id";
   public static final String PARAM_COMPONENT = "resource";
+  public static final String PARAM_FORMAT = "format";
 
   private final DbClient dbClient;
   private final UserSession userSession;
@@ -89,7 +88,9 @@ public class IndexAction implements WsAction {
     action.createParam(PARAM_COMPONENT)
       .setDescription("Component key or database id")
       .setExampleValue(KEY_PROJECT_EXAMPLE_001);
-    addJsonOnlyFormatParam(action);
+    action.createParam(PARAM_FORMAT)
+      .setDescription("Only json response format is available")
+      .setPossibleValues("json");
   }
 
   @Override
index c3749bd9dc703372cfc8d74bb7fba7342ac6696d..2e54e06f42a9cd1d7f5a55cf0feacbb31852d309 100644 (file)
@@ -42,7 +42,6 @@ import org.sonar.server.property.ws.IndexAction;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.sonar.api.server.ws.RailsHandler.PARAM_FORMAT;
 import static org.sonar.server.property.ws.PropertiesWs.CONTROLLER_PROPERTIES;
 import static org.sonarqube.ws.client.setting.SettingsWsParameters.ACTION_RESET;
 import static org.sonarqube.ws.client.setting.SettingsWsParameters.ACTION_SET;
@@ -172,7 +171,7 @@ public class DeprecatedRestWebServiceFilter extends ServletFilter {
     private void handleGet(Optional<String> key, Optional<String> component) {
       addParameterIfPresent(IndexAction.PARAM_ID, key);
       addParameterIfPresent(IndexAction.PARAM_COMPONENT, component);
-      addParameterIfPresent(PARAM_FORMAT, readParam(PARAM_FORMAT));
+      addParameterIfPresent(IndexAction.PARAM_FORMAT, readParam(IndexAction.PARAM_FORMAT));
       redirectedPath = CONTROLLER_PROPERTIES + "/index";
       redirectedMethod = "GET";
     }
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/project/ws/index-example.json b/server/sonar-server/src/main/resources/org/sonar/server/project/ws/index-example.json
new file mode 100644 (file)
index 0000000..a5d82ab
--- /dev/null
@@ -0,0 +1,23 @@
+[
+  {
+    "id": "5035",
+    "k": "org.jenkins-ci.plugins:sonar",
+    "nm": "Jenkins Sonar Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  },
+  {
+    "id": "5146",
+    "k": "org.codehaus.sonar-plugins:sonar-ant-task",
+    "nm": "Sonar Ant Task",
+    "sc": "PRJ",
+    "qu": "TRK"
+  },
+  {
+    "id": "15964",
+    "k": "org.codehaus.sonar-plugins:sonar-build-breaker-plugin",
+    "nm": "Sonar Build Breaker Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  }
+]
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/project/ws/projects-example-index.json b/server/sonar-server/src/main/resources/org/sonar/server/project/ws/projects-example-index.json
deleted file mode 100644 (file)
index a5d82ab..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-[
-  {
-    "id": "5035",
-    "k": "org.jenkins-ci.plugins:sonar",
-    "nm": "Jenkins Sonar Plugin",
-    "sc": "PRJ",
-    "qu": "TRK"
-  },
-  {
-    "id": "5146",
-    "k": "org.codehaus.sonar-plugins:sonar-ant-task",
-    "nm": "Sonar Ant Task",
-    "sc": "PRJ",
-    "qu": "TRK"
-  },
-  {
-    "id": "15964",
-    "k": "org.codehaus.sonar-plugins:sonar-build-breaker-plugin",
-    "nm": "Sonar Build Breaker Plugin",
-    "sc": "PRJ",
-    "qu": "TRK"
-  }
-]
index 247cd7a185653fc46643171fb9cf61c6c075aa74..52c0c7546647023a9eaf77a57f36e75517ddbedb 100644 (file)
@@ -71,6 +71,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.server.project.ws.DeleteAction.PARAM_ID;
 import static org.sonar.server.project.ws.DeleteAction.PARAM_KEY;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.CONTROLLER;
 
 public class DeleteActionTest {
 
@@ -278,6 +279,6 @@ public class DeleteActionTest {
   }
 
   private WsTester.TestRequest newRequest() {
-    return ws.newPostRequest(ProjectsWs.ENDPOINT, ACTION);
+    return ws.newPostRequest(CONTROLLER, ACTION);
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/IndexActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/IndexActionTest.java
new file mode 100644 (file)
index 0000000..a3be3e0
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.project.ws;
+
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.db.component.ComponentTesting.newModuleDto;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.test.JsonAssert.assertJson;
+
+public class IndexActionTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+
+  private DbClient dbClient = db.getDbClient();
+
+  private UserDto user;
+
+  private WsActionTester ws = new WsActionTester(new IndexAction(dbClient, userSession));
+
+  @Before
+  public void setUp() {
+    user = db.users().insertUser("john");
+    userSession.login(user);
+  }
+
+  @Test
+  public void search_all_projects() throws Exception {
+    insertProjectsAuthorizedForUser(
+      newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-build-breaker-plugin").setName("Sonar Build Breaker Plugin"));
+
+    String result = call(null, null, null);
+
+    verifyResult(result, "search_projects.json");
+  }
+
+  @Test
+  public void search_projects_with_modules() throws Exception {
+    ComponentDto project1 = newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin");
+    ComponentDto project2 = newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task");
+    insertProjectsAuthorizedForUser(project1, project2);
+    db.components().insertComponents(
+      newModuleDto(project1).setKey("org.jenkins-ci.plugins:sonar-common").setName("Common"),
+      newModuleDto(project2).setKey("org.codehaus.sonar-plugins:sonar-ant-db").setName("Ant DB"));
+
+    String result = call(null, null, true);
+
+    verifyResult(result, "search_projects_with_modules.json");
+  }
+
+  @Test
+  public void search_project_by_key() throws Exception {
+    insertProjectsAuthorizedForUser(
+      newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-build-breaker-plugin").setName("Sonar Build Breaker Plugin"));
+
+    String result = call("org.jenkins-ci.plugins:sonar", null, null);
+
+    verifyResult(result, "search_project_by_key.json");
+  }
+
+  @Test
+  public void search_project_by_id() throws Exception {
+    ComponentDto project = newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin");
+    insertProjectsAuthorizedForUser(
+      project,
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-build-breaker-plugin").setName("Sonar Build Breaker Plugin"));
+
+    String result = call(Long.toString(project.getId()), null, null);
+
+    verifyResult(result, "search_project_by_id.json");
+  }
+
+  @Test
+  public void search_project_by_name() throws Exception {
+    insertProjectsAuthorizedForUser(
+      newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-build-breaker-plugin").setName("Sonar Build Breaker Plugin"));
+
+    String result = call(null, "Plu", null);
+
+    verifyResult(result, "search_project_by_name.json");
+  }
+
+  @Test
+  public void return_empty_list_when_no_project_match_search() throws Exception {
+    insertProjectsAuthorizedForUser(
+      newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-build-breaker-plugin").setName("Sonar Build Breaker Plugin"));
+
+    String result = call(null, "Unknown", null);
+
+    verifyResult(result, "empty.json");
+  }
+
+  @Test
+  public void return_only_projects_authorized_for_user() throws Exception {
+    insertProjectsAuthorizedForUser(
+      newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task"));
+    db.components()
+      .insertComponent(newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-build-breaker-plugin").setName("Sonar Build Breaker Plugin"));
+
+    String result = call(null, null, null);
+
+    verifyResult(result, "return_only_projects_authorized_for_user.json");
+  }
+
+  @Test
+  public void test_example() {
+    insertProjectsAuthorizedForUser(
+      newProjectDto(db.getDefaultOrganization()).setKey("org.jenkins-ci.plugins:sonar").setName("Jenkins Sonar Plugin"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-ant-task").setName("Sonar Ant Task"),
+      newProjectDto(db.getDefaultOrganization()).setKey("org.codehaus.sonar-plugins:sonar-build-breaker-plugin").setName("Sonar Build Breaker Plugin"));
+
+    String result = call(null, null, null);
+
+    assertJson(result).ignoreFields("id").isSimilarTo(ws.getDef().responseExampleAsString());
+  }
+
+  @Test
+  public void define_index_action() {
+    WebService.Action action = ws.getDef();
+    assertThat(action).isNotNull();
+    assertThat(action.responseExampleAsString()).isNotEmpty();
+    assertThat(action.params()).hasSize(8);
+  }
+
+  private String call(@Nullable String key, @Nullable String search, @Nullable Boolean subprojects) {
+    TestRequest httpRequest = ws.newRequest();
+    setNullable(key, e -> httpRequest.setParam("key", e));
+    setNullable(search, e -> httpRequest.setParam("search", e));
+    setNullable(subprojects, e -> httpRequest.setParam("subprojects", Boolean.toString(e)));
+    return httpRequest.execute().getInput();
+  }
+
+  private void insertProjectsAuthorizedForUser(ComponentDto... projects) {
+    db.components().insertComponents(projects);
+    setBrowsePermissionOnUser(projects);
+    db.commit();
+  }
+
+  private void setBrowsePermissionOnUser(ComponentDto... projects) {
+    Arrays.stream(projects).forEach(project -> db.users().insertProjectPermissionOnUser(user, UserRole.USER, project));
+    db.getSession().commit();
+  }
+
+  private void verifyResult(String json, String expectedJsonFile) {
+    assertJson(json).ignoreFields("id").isSimilarTo(getClass().getResource(getClass().getSimpleName() + "/" + expectedJsonFile));
+  }
+}
index c2a2dab4b7473be7b8d5104c2272458dd20512de..87215ece5d202d6ddb6a9ff723f64aae6de4de77 100644 (file)
@@ -29,6 +29,6 @@ public class ProjectsWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new ProjectsWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(2 + 8);
+    assertThat(container.size()).isEqualTo(2 + 9);
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsTest.java
deleted file mode 100644 (file)
index 214e9f1..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.project.ws;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.sonar.api.server.ws.RailsHandler;
-import org.sonar.api.server.ws.WebService;
-import org.sonar.db.DbClient;
-import org.sonar.server.component.ComponentCleanerService;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.ws.WsTester;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-
-public class ProjectsWsTest {
-
-  WebService.Controller controller;
-  WsTester ws;
-
-  @Before
-  public void setUp() {
-    ws = new WsTester(new ProjectsWs(
-      new BulkDeleteAction(mock(ComponentCleanerService.class), mock(DbClient.class), mock(UserSession.class)),
-      new GhostsAction(mock(DbClient.class), mock(UserSession.class)),
-      new ProvisionedAction(mock(DbClient.class), mock(UserSession.class))));
-    controller = ws.controller("api/projects");
-  }
-
-  @Test
-  public void define_controller() {
-    assertThat(controller).isNotNull();
-    assertThat(controller.description()).isNotEmpty();
-    assertThat(controller.since()).isEqualTo("2.10");
-    assertThat(controller.actions()).hasSize(4);
-  }
-
-  @Test
-  public void define_index_action() {
-    WebService.Action action = controller.action("index");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).hasSize(8);
-  }
-
-}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/empty.json b/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/empty.json
new file mode 100644 (file)
index 0000000..41b42e6
--- /dev/null
@@ -0,0 +1,3 @@
+[
+
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/return_only_projects_authorized_for_user.json b/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/return_only_projects_authorized_for_user.json
new file mode 100644 (file)
index 0000000..9bc8440
--- /dev/null
@@ -0,0 +1,16 @@
+[
+  {
+    "id": 4,
+    "k": "org.jenkins-ci.plugins:sonar",
+    "nm": "Jenkins Sonar Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  },
+  {
+    "id": 5,
+    "k": "org.codehaus.sonar-plugins:sonar-ant-task",
+    "nm": "Sonar Ant Task",
+    "sc": "PRJ",
+    "qu": "TRK"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_id.json b/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_id.json
new file mode 100644 (file)
index 0000000..4c9d99a
--- /dev/null
@@ -0,0 +1,9 @@
+[
+  {
+    "id": "5035",
+    "k": "org.jenkins-ci.plugins:sonar",
+    "nm": "Jenkins Sonar Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_key.json b/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_key.json
new file mode 100644 (file)
index 0000000..4c9d99a
--- /dev/null
@@ -0,0 +1,9 @@
+[
+  {
+    "id": "5035",
+    "k": "org.jenkins-ci.plugins:sonar",
+    "nm": "Jenkins Sonar Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_name.json b/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_project_by_name.json
new file mode 100644 (file)
index 0000000..bd36e68
--- /dev/null
@@ -0,0 +1,16 @@
+[
+  {
+    "id": 7,
+    "k": "org.jenkins-ci.plugins:sonar",
+    "nm": "Jenkins Sonar Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  },
+  {
+    "id": 9,
+    "k": "org.codehaus.sonar-plugins:sonar-build-breaker-plugin",
+    "nm": "Sonar Build Breaker Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_projects.json b/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_projects.json
new file mode 100644 (file)
index 0000000..a5d82ab
--- /dev/null
@@ -0,0 +1,23 @@
+[
+  {
+    "id": "5035",
+    "k": "org.jenkins-ci.plugins:sonar",
+    "nm": "Jenkins Sonar Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  },
+  {
+    "id": "5146",
+    "k": "org.codehaus.sonar-plugins:sonar-ant-task",
+    "nm": "Sonar Ant Task",
+    "sc": "PRJ",
+    "qu": "TRK"
+  },
+  {
+    "id": "15964",
+    "k": "org.codehaus.sonar-plugins:sonar-build-breaker-plugin",
+    "nm": "Sonar Build Breaker Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_projects_with_modules.json b/server/sonar-server/src/test/resources/org/sonar/server/project/ws/IndexActionTest/search_projects_with_modules.json
new file mode 100644 (file)
index 0000000..51b0c2f
--- /dev/null
@@ -0,0 +1,30 @@
+[
+  {
+    "id": 16,
+    "k": "org.codehaus.sonar-plugins:sonar-ant-db",
+    "nm": "Ant DB",
+    "sc": "PRJ",
+    "qu": "BRC"
+  },
+  {
+    "id": 15,
+    "k": "org.jenkins-ci.plugins:sonar-common",
+    "nm": "Common",
+    "sc": "PRJ",
+    "qu": "BRC"
+  },
+  {
+    "id": 13,
+    "k": "org.jenkins-ci.plugins:sonar",
+    "nm": "Jenkins Sonar Plugin",
+    "sc": "PRJ",
+    "qu": "TRK"
+  },
+  {
+    "id": 14,
+    "k": "org.codehaus.sonar-plugins:sonar-ant-task",
+    "nm": "Sonar Ant Task",
+    "sc": "PRJ",
+    "qu": "TRK"
+  }
+]
index e4d2527af9b0c0ddff613597f1b83407779911b5..2d892055a7991b4ef34a18d08140b2608e8e0ead 100644 (file)
@@ -39,15 +39,16 @@ import org.sonar.db.Dao;
 import org.sonar.db.DatabaseUtils;
 import org.sonar.db.DbSession;
 import org.sonar.db.RowNotFoundException;
-import org.sonar.db.WildcardPosition;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.Maps.newHashMapWithExpectedSize;
 import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNull;
 import static org.apache.commons.lang.StringUtils.isBlank;
+import static org.sonar.db.DatabaseUtils.buildLikeValue;
 import static org.sonar.db.DatabaseUtils.executeLargeInputs;
 import static org.sonar.db.DatabaseUtils.executeLargeUpdates;
+import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER;
 
 public class ComponentDao implements Dao {
 
@@ -239,7 +240,7 @@ public class ComponentDao implements Dao {
     if (isBlank(textQuery)) {
       return null;
     }
-    return DatabaseUtils.buildLikeValue(textQuery.toUpperCase(Locale.ENGLISH), WildcardPosition.BEFORE_AND_AFTER);
+    return DatabaseUtils.buildLikeValue(textQuery.toUpperCase(Locale.ENGLISH), BEFORE_AND_AFTER);
   }
 
   public List<ComponentDto> selectGhostProjects(DbSession session, int offset, int limit, @Nullable String query) {
@@ -288,6 +289,11 @@ public class ComponentDao implements Dao {
     return new HashSet<>(mapper(dbSession).selectComponentsByQualifiers(qualifiers));
   }
 
+  public List<ComponentDto> selectProjectsByNameQuery(DbSession dbSession, @Nullable String nameQuery, boolean includeModules) {
+    String nameQueryForSql = nameQuery == null ? null : buildLikeValue(nameQuery, BEFORE_AND_AFTER).toUpperCase(Locale.ENGLISH);
+    return mapper(dbSession).selectProjectsByNameQuery(nameQueryForSql, includeModules);
+  }
+
   private static void addPartialQueryParameterIfNotNull(Map<String, Object> parameters, @Nullable String keyOrNameFilter) {
     // TODO rely on resource_index table and match exactly the key
     if (keyOrNameFilter != null) {
index 101ac0f26c6abe6817c1c5c1acedbb69a3b1639b..7cf6f163b49d87a86eca5a30afe8eff77ccdad6b 100644 (file)
@@ -117,6 +117,8 @@ public interface ComponentMapper {
 
   List<ComponentDto> selectComponentsHavingSameKeyOrderedById(String key);
 
+  List<ComponentDto> selectProjectsByNameQuery(@Param("nameQuery") @Nullable String nameQuery, @Param("includeModules") boolean includeModules);
+
   long countGhostProjects(Map<String, Object> parameters);
 
   void selectForIndexing(@Param("projectUuid") @Nullable String projectUuid, ResultHandler handler);
index 5813d2aca3b544f7c05ee01037bb9ab062cc62b2..3ff8987b017f7444afa7821573952cdf01a2ebbb 100644 (file)
       </if>
   </select>
 
+  <select id="selectProjectsByNameQuery" resultType="Component">
+    select
+    <include refid="componentColumns"/>
+    from projects p
+    <where>
+      p.enabled=${_true}
+      AND p.copy_component_uuid is null
+      <if test="includeModules == false">
+        AND p.qualifier = 'TRK'
+      </if>
+      <if test="includeModules == true">
+        AND (p.qualifier = 'TRK' OR p.qualifier = 'BRC')
+      </if>
+      <if test="nameQuery != null">
+        AND UPPER(p.name) like #{nameQuery,jdbcType=VARCHAR}
+      </if>
+    </where>
+    ORDER BY p.name
+  </select>
+
   <insert id="insert" parameterType="Component" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
     INSERT INTO projects (
     organization_uuid,
index 9bbaaf20aa736d42471fd63ba03d1aaee738d697..e0b62a5dc030e0689bcb86589b559bd58487e21e 100644 (file)
@@ -720,9 +720,9 @@ public class ComponentDaoTest {
 
   @Test
   public void updateBEnabledToFalse() {
-    ComponentDto dto1 = ComponentTesting.newProjectDto(db.getDefaultOrganization(), "U1");
-    ComponentDto dto2 = ComponentTesting.newProjectDto(db.getDefaultOrganization(), "U2");
-    ComponentDto dto3 = ComponentTesting.newProjectDto(db.getDefaultOrganization(), "U3");
+    ComponentDto dto1 = newProjectDto(db.getDefaultOrganization(), "U1");
+    ComponentDto dto2 = newProjectDto(db.getDefaultOrganization(), "U2");
+    ComponentDto dto3 = newProjectDto(db.getDefaultOrganization(), "U3");
     underTest.insert(dbSession, dto1, dto2, dto3);
 
     underTest.updateBEnabledToFalse(dbSession, asList("U1", "U2"));
@@ -1001,6 +1001,28 @@ public class ComponentDaoTest {
     assertThat(components).extracting("organizationUuid").containsOnly(organizationDto.getUuid());
   }
 
+  @Test
+  public void select_projects_by_name_query() {
+    OrganizationDto organizationDto = db.organizations().insert();
+    ComponentDto project1 = db.components().insertComponent(newProjectDto(organizationDto).setName("project1"));
+    ComponentDto module1 = db.components().insertComponent(newModuleDto(project1).setName("module1"));
+    ComponentDto subModule1 = db.components().insertComponent(newModuleDto(module1).setName("subModule1"));
+    ComponentDto file = db.components().insertComponent(newFileDto(subModule1).setName("file"));
+    ComponentDto project2 = db.components().insertComponent(newProjectDto(organizationDto).setName("project2"));
+    ComponentDto project3 = db.components().insertComponent(newProjectDto(organizationDto).setName("project3"));
+
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, null, false)).extracting(ComponentDto::uuid)
+      .containsOnly(project1.uuid(), project2.uuid(), project3.uuid());
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, null, true)).extracting(ComponentDto::uuid)
+      .containsOnly(project1.uuid(), project2.uuid(), project3.uuid(), module1.uuid(), subModule1.uuid());
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, "project1", false)).extracting(ComponentDto::uuid).containsOnly(project1.uuid());
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, "ct1", false)).extracting(ComponentDto::uuid).containsOnly(project1.uuid());
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, "pro", false)).extracting(ComponentDto::uuid).containsOnly(project1.uuid(), project2.uuid(), project3.uuid());
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, "jec", false)).extracting(ComponentDto::uuid).containsOnly(project1.uuid(), project2.uuid(), project3.uuid());
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, "1", true)).extracting(ComponentDto::uuid).containsOnly(project1.uuid(), module1.uuid(), subModule1.uuid());
+    assertThat(underTest.selectProjectsByNameQuery(dbSession, "unknown", true)).extracting(ComponentDto::uuid).isEmpty();
+  }
+
   private static ComponentTreeQuery.Builder newTreeQuery(String baseUuid) {
     return ComponentTreeQuery.builder()
       .setBaseUuid(baseUuid)
index 5c86c340ecf052b23902b9ec76ba8ec79dddd3db..63ce01080fb1e3ee728285fc1c6eb0d02b39434a 100644 (file)
@@ -27,7 +27,6 @@ package org.sonar.api.server.ws;
 public class RailsHandler implements RequestHandler {
 
   public static final RequestHandler INSTANCE = new RailsHandler();
-  public static final String PARAM_FORMAT = "format";
 
   private RailsHandler() {
     // Nothing
@@ -38,26 +37,4 @@ public class RailsHandler implements RequestHandler {
     throw new UnsupportedOperationException("This web service is implemented in rails");
   }
 
-  public static WebService.NewParam addFormatParam(WebService.NewAction action) {
-    return action.createParam(PARAM_FORMAT)
-      .setDescription("Response format can be set through:" +
-        "<ul>" +
-        "<li>Parameter format: xml | json</li>" +
-        "<li>Or the 'Accept' property in the HTTP header:" +
-        "<ul>" +
-        "<li>Accept:text/xml</li>" +
-        "<li>Accept:application/json</li>" +
-        "</ul></li></ul>" +
-        "If nothing is set, json is used.<br/>" +
-        "Since 6.1, XML format is deprecated, only JSON format should be used.")
-      .setPossibleValues("json", "xml")
-      .setDeprecatedSince("6.1");
-  }
-
-  public static WebService.NewParam addJsonOnlyFormatParam(WebService.NewAction action) {
-    return action.createParam(PARAM_FORMAT)
-      .setDescription("Only json response format is available")
-      .setPossibleValues("json");
-  }
-
 }
index f6ca6e6c0c11202258b68d6345ad21861f166ab2..89ce77b8e5e9495815af47800f2ff7cbde63a819 100644 (file)
@@ -24,6 +24,7 @@ public class ProjectsWsParameters {
   public static final String CONTROLLER = "api/projects";
 
   public static final String ACTION_CREATE = "create";
+  public static final String ACTION_INDEX = "index";
 
   public static final String PARAM_PROJECT = "project";
   public static final String PARAM_NAME = "name";