diff options
20 files changed, 571 insertions, 220 deletions
diff --git a/it/it-tests/src/test/java/it/analysis/FavoriteTest.java b/it/it-tests/src/test/java/it/analysis/FavoriteTest.java index f066888a61a..c8602a377a6 100644 --- a/it/it-tests/src/test/java/it/analysis/FavoriteTest.java +++ b/it/it-tests/src/test/java/it/analysis/FavoriteTest.java @@ -28,9 +28,9 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; -import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.Favorites; +import org.sonarqube.ws.Favorites.Favorite; import org.sonarqube.ws.WsPermissions; -import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.WsClient; import org.sonarqube.ws.client.permission.AddProjectCreatorToTemplateWsRequest; import org.sonarqube.ws.client.permission.RemoveProjectCreatorFromTemplateWsRequest; @@ -71,8 +71,8 @@ public class FavoriteTest { orchestrator.executeBuild(sampleProject); - String response = adminWsClient.wsConnector().call(new GetRequest("api/favourites").setMediaType(MediaTypes.JSON)).content(); - assertThat(response).contains(PROJECT_KEY); + Favorites.SearchResponse response = adminWsClient.favorites().search(null, null); + assertThat(response.getFavoritesList()).extracting(Favorite::getKey).contains(PROJECT_KEY); } @Test @@ -81,8 +81,8 @@ public class FavoriteTest { orchestrator.executeBuild(sampleProject); - String response = adminWsClient.wsConnector().call(new GetRequest("api/favourites").setMediaType(MediaTypes.JSON)).content(); - assertThat(response).doesNotContain(PROJECT_KEY); + Favorites.SearchResponse response = adminWsClient.favorites().search(null, null); + assertThat(response.getFavoritesList()).extracting(Favorite::getKey).doesNotContain(PROJECT_KEY); } @Test @@ -94,8 +94,8 @@ public class FavoriteTest { orchestrator.executeBuild(sampleProject); - String response = adminWsClient.wsConnector().call(new GetRequest("api/favourites").setMediaType(MediaTypes.JSON)).content(); - assertThat(response).doesNotContain(PROJECT_KEY); + Favorites.SearchResponse response = adminWsClient.favorites().search(null, null); + assertThat(response.getFavoritesList()).extracting(Favorite::getKey).doesNotContain(PROJECT_KEY); } private static SonarScanner createScannerWithUserCredentials() { diff --git a/it/it-tests/src/test/java/it/user/FavoritesWsTest.java b/it/it-tests/src/test/java/it/user/FavoritesWsTest.java index 62ec54251e8..357d1fa3bf2 100644 --- a/it/it-tests/src/test/java/it/user/FavoritesWsTest.java +++ b/it/it-tests/src/test/java/it/user/FavoritesWsTest.java @@ -26,9 +26,7 @@ import java.util.List; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; -import org.sonar.wsclient.Sonar; -import org.sonar.wsclient.services.Favourite; -import org.sonar.wsclient.services.FavouriteQuery; +import org.sonarqube.ws.Favorites.Favorite; import org.sonarqube.ws.client.WsClient; import static org.assertj.core.api.Assertions.assertThat; @@ -53,24 +51,22 @@ public class FavoritesWsTest { @Test public void favorites_web_service() { - Sonar oldWsClient = orchestrator.getServer().getAdminWsClient(); - // GET (nothing) - List<Favourite> favourites = oldWsClient.findAll(new FavouriteQuery()); - assertThat(favourites).isEmpty(); + List<Favorite> favorites = adminClient.favorites().search(null, null).getFavoritesList(); + assertThat(favorites).isEmpty(); // POST (create favorites) adminClient.favorites().add("sample"); adminClient.favorites().add("sample:src/main/xoo/sample/Sample.xoo"); // GET (created favorites) - favourites = oldWsClient.findAll(new FavouriteQuery()); - assertThat(favourites.stream().map(Favourite::getKey)).containsOnly("sample", "sample:src/main/xoo/sample/Sample.xoo"); + favorites = adminClient.favorites().search(null, null).getFavoritesList(); + assertThat(favorites.stream().map(Favorite::getKey)).containsOnly("sample", "sample:src/main/xoo/sample/Sample.xoo"); // DELETE (a favorite) adminClient.favorites().remove("sample"); - favourites = oldWsClient.findAll(new FavouriteQuery()); - assertThat(favourites.stream().map(Favourite::getKey)).containsOnly("sample:src/main/xoo/sample/Sample.xoo"); + favorites = adminClient.favorites().search(null, null).getFavoritesList(); + assertThat(favorites.stream().map(Favorite::getKey)).containsOnly("sample:src/main/xoo/sample/Sample.xoo"); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java index 89bb917aef7..88c2180a190 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java @@ -44,7 +44,6 @@ public class JwtCsrfVerifier { private static final String API_URL = "/api"; private static final Set<String> RAILS_UPDATE_API_URLS = ImmutableSet.of( "/api/events", - "/api/favourites", "/api/issues/add_comment", "/api/issues/delete_comment", "/api/issues/edit_comment", diff --git a/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteFinder.java b/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteFinder.java new file mode 100644 index 00000000000..290e845abbe --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteFinder.java @@ -0,0 +1,67 @@ +/* + * 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.favorite; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.property.PropertyDto; +import org.sonar.db.property.PropertyQuery; +import org.sonar.server.user.UserSession; + +import static java.util.Collections.emptyList; +import static org.sonar.core.util.stream.Collectors.toList; +import static org.sonar.server.favorite.FavoriteUpdater.PROP_FAVORITE_KEY; + +public class FavoriteFinder { + private final DbClient dbClient; + private final UserSession userSession; + + public FavoriteFinder(DbClient dbClient, UserSession userSession) { + this.dbClient = dbClient; + this.userSession = userSession; + } + + /** + * @return the list of favorite components of the authenticated user. Empty list if the user is not authenticated + */ + public List<ComponentDto> list() { + if (!userSession.isLoggedIn()) { + return emptyList(); + } + + try (DbSession dbSession = dbClient.openSession(false)) { + PropertyQuery dbQuery = PropertyQuery.builder() + .setKey(PROP_FAVORITE_KEY) + .setUserId(userSession.getUserId()) + .build(); + Set<Long> componentIds = dbClient.propertiesDao().selectByQuery(dbQuery, dbSession).stream().map(PropertyDto::getResourceId).collect(Collectors.toSet()); + + return dbClient.componentDao().selectByIds(dbSession, componentIds).stream() + .sorted(Comparator.comparing(ComponentDto::name)) + .collect(toList()); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteModule.java b/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteModule.java index 0a0da3ecedd..414aa027e69 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteModule.java @@ -24,16 +24,19 @@ import org.sonar.core.platform.Module; import org.sonar.server.favorite.ws.AddAction; import org.sonar.server.favorite.ws.FavoritesWs; import org.sonar.server.favorite.ws.RemoveAction; +import org.sonar.server.favorite.ws.SearchAction; public class FavoriteModule extends Module { @Override protected void configureModule() { add( + FavoriteFinder.class, FavoriteUpdater.class, FavoritesWs.class, AddAction.class, - RemoveAction.class); + RemoveAction.class, + SearchAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteUpdater.java index 540e20fdb7f..067ca40bdb8 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/favorite/FavoriteUpdater.java @@ -30,7 +30,7 @@ import org.sonar.server.user.UserSession; import static org.sonar.server.ws.WsUtils.checkRequest; public class FavoriteUpdater { - private static final String PROP_FAVORITE_KEY = "favourite"; + static final String PROP_FAVORITE_KEY = "favourite"; private final DbClient dbClient; private final UserSession userSession; diff --git a/server/sonar-server/src/main/java/org/sonar/server/favorite/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/favorite/ws/SearchAction.java new file mode 100644 index 00000000000..0976cee4d87 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/favorite/ws/SearchAction.java @@ -0,0 +1,191 @@ +/* + * 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.favorite.ws; + +import com.google.common.collect.ImmutableSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +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.util.stream.Collectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.favorite.FavoriteFinder; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Favorites.Favorite; +import org.sonarqube.ws.Favorites.SearchResponse; + +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.core.util.stream.Collectors.toOneElement; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.favorite.FavoritesWsParameters.ACTION_SEARCH; + +public class SearchAction implements FavoritesWsAction { + private final FavoriteFinder favoriteFinder; + private final DbClient dbClient; + private final UserSession userSession; + + public SearchAction(FavoriteFinder favoriteFinder, DbClient dbClient, UserSession userSession) { + this.favoriteFinder = favoriteFinder; + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_SEARCH) + .setDescription("Search for the authenticated user favorites.<br>" + + "Requires authentication.") + .setSince("6.3") + .setResponseExample(getClass().getResource("search-example.json")) + .setHandler(this); + + action.addPagingParams(100, 500); + } + + @Override + public void handle(Request request, Response response) throws Exception { + SearchResponse wsResponse = Stream.of(request) + .map(search()) + .map(new ResponseBuilder()) + .collect(Collectors.toOneElement()); + writeProtobuf(wsResponse, request, response); + } + + private Function<Request, SearchResults> search() { + return request -> { + try (DbSession dbSession = dbClient.openSession(false)) { + return Stream.of(request) + .peek(checkAuthentication(userSession)) + .map(SearchResults.builder(dbSession)) + .peek(addAuthorizedProjectUuids()) + .peek(addFavorites()) + .map(SearchResults.Builder::build) + .collect(Collectors.toOneElement()); + } + }; + } + + private Consumer<SearchResults.Builder> addFavorites() { + return results -> results.allFavorites = favoriteFinder.list(); + } + + private Consumer<SearchResults.Builder> addAuthorizedProjectUuids() { + return results -> results.authorizedProjectUuids = ImmutableSet + .copyOf(dbClient.authorizationDao().selectAuthorizedRootProjectsUuids(results.dbSession, userSession.getUserId(), UserRole.USER)); + } + + private static Consumer<Request> checkAuthentication(UserSession userSession) { + return r -> userSession.checkLoggedIn(); + } + + private static class SearchResults { + private final List<ComponentDto> favorites; + private final Paging paging; + + private SearchResults(Builder builder) { + Predicate<ComponentDto> authorizedProjects = c -> builder.authorizedProjectUuids.contains(c.projectUuid()); + int total = (int) builder.allFavorites.stream().filter(authorizedProjects).count(); + this.paging = Paging.forPageIndex(builder.page).withPageSize(builder.pageSize).andTotal(total); + this.favorites = builder.allFavorites.stream() + .filter(authorizedProjects) + .skip(paging.offset()) + .limit(paging.pageSize()) + .collect(Collectors.toList()); + } + + static Function<Request, Builder> builder(DbSession dbSession) { + return request -> new Builder(dbSession, request); + } + + private static class Builder { + private final DbSession dbSession; + private final int page; + private final int pageSize; + private Set<String> authorizedProjectUuids; + private List<ComponentDto> allFavorites; + + private Builder(DbSession dbSession, Request request) { + this.dbSession = dbSession; + this.page = request.mandatoryParamAsInt(Param.PAGE); + this.pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE); + } + + public SearchResults build() { + return new SearchResults(this); + } + } + } + + private static class ResponseBuilder implements Function<SearchResults, SearchResponse> { + private final SearchResponse.Builder response; + private final Favorite.Builder favorite; + + private ResponseBuilder() { + this.response = SearchResponse.newBuilder(); + this.favorite = Favorite.newBuilder(); + } + + @Override + public SearchResponse apply(SearchResults searchResults) { + return Stream.of(searchResults) + .peek(addPaging()) + .peek(addFavorites()) + .map(results -> response.build()) + .collect(toOneElement()); + } + + private Consumer<SearchResults> addPaging() { + return results -> response.setPaging(Common.Paging.newBuilder() + .setPageIndex(results.paging.pageIndex()) + .setPageSize(results.paging.pageSize()) + .setTotal(results.paging.total())); + } + + private Consumer<SearchResults> addFavorites() { + return results -> results.favorites.stream() + .map(toWsFavorite()) + .forEach(response::addFavorites); + } + + private Function<ComponentDto, Favorite> toWsFavorite() { + return componentDto -> { + favorite + .clear() + .setKey(componentDto.key()); + setNullable(componentDto.name(), favorite::setName); + setNullable(componentDto.qualifier(), favorite::setQualifier); + return favorite.build(); + }; + } + + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/FavouritesWs.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/FavouritesWs.java deleted file mode 100644 index 543ee6fd39f..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/FavouritesWs.java +++ /dev/null @@ -1,48 +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.user.ws; - -import org.sonar.api.server.ws.RailsHandler; -import org.sonar.api.server.ws.WebService; - -import static org.sonar.api.server.ws.RailsHandler.addFormatParam; - -public class FavouritesWs implements WebService { - - @Override - public void define(Context context) { - NewController controller = context.createController("api/favourites"); - controller.setDescription("Manage user favorites."); - controller.setSince("2.6"); - - defineIndexAction(controller); - - controller.done(); - } - - private void defineIndexAction(NewController controller) { - NewAction action = controller.createAction("index") - .setDescription("Documentation of this web service is available <a href=\"http://redirect.sonarsource.com/doc/old-web-service-api.html\">here</a>") - .setResponseExample(getClass().getResource("favourites-index-example.xml")) - .setSince("2.6") - .setHandler(RailsHandler.INSTANCE); - addFormatParam(action); - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWsModule.java index a568fb8c10a..18a35ad7741 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWsModule.java @@ -35,7 +35,6 @@ public class UsersWsModule extends Module { SearchAction.class, GroupsAction.class, IdentityProvidersAction.class, - FavouritesWs.class, UserPropertiesWs.class, UserJsonWriter.class); } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/favorite/ws/search-example.json b/server/sonar-server/src/main/resources/org/sonar/server/favorite/ws/search-example.json new file mode 100644 index 00000000000..74d0909f625 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/favorite/ws/search-example.json @@ -0,0 +1,24 @@ +{ + "paging": { + "pageIndex": 1, + "pageSize": 100, + "total": 3 + }, + "favorites": [ + { + "key": "K2", + "name": "Apache HBase", + "qualifier": "TRK" + }, + { + "key": "K3", + "name": "JDK9", + "qualifier": "TRK" + }, + { + "key": "K1", + "name": "Samba", + "qualifier": "TRK" + } + ] +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/user/ws/favourites-index-example.xml b/server/sonar-server/src/main/resources/org/sonar/server/user/ws/favourites-index-example.xml deleted file mode 100644 index a5390359be2..00000000000 --- a/server/sonar-server/src/main/resources/org/sonar/server/user/ws/favourites-index-example.xml +++ /dev/null @@ -1,18 +0,0 @@ -<favourites> - <favourite> - <id>2865</id> - <key>org.sonarsource.sonarqube:sonarqube</key> - <name>SonarQube</name> - <lname>SonarQube</lname> - <scope>PRJ</scope> - <qualifier>TRK</qualifier> - </favourite> - <favourite> - <id>34830</id> - <key>DEV:george.orwell@1984.com</key> - <name>George Orwell</name> - <lname>George Orwell</lname> - <scope>PRJ</scope> - <qualifier>DEV</qualifier> - </favourite> -</favourites> diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java index 61babbeab1c..942ee39420a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java @@ -149,7 +149,6 @@ public class JwtCsrfVerifierTest { @Test public void ignore_rails_ws_requests() throws Exception { executeVerifyStateDoesNotFailOnRequest("/api/events", "POST"); - executeVerifyStateDoesNotFailOnRequest("/api/favourites", "POST"); executeVerifyStateDoesNotFailOnRequest("/api/issues/add_comment?key=ABCD", "POST"); executeVerifyStateDoesNotFailOnRequest("/api/issues/delete_comment?key=ABCD", "POST"); executeVerifyStateDoesNotFailOnRequest("/api/issues/edit_comment?key=ABCD", "POST"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/favorite/FavoriteModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/favorite/FavoriteModuleTest.java index 1affa2ca117..64086da9ce7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/favorite/FavoriteModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/favorite/FavoriteModuleTest.java @@ -30,6 +30,6 @@ public class FavoriteModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new FavoriteModule().configure(container); - assertThat(container.size()).isEqualTo(4 + 2); + assertThat(container.size()).isEqualTo(6 + 2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/favorite/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/favorite/ws/SearchActionTest.java new file mode 100644 index 00000000000..817f726f97b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/favorite/ws/SearchActionTest.java @@ -0,0 +1,210 @@ +/* + * 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.favorite.ws; + +import com.google.common.base.Throwables; +import java.io.IOException; +import java.io.InputStream; +import java.util.stream.IntStream; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.permission.UserPermissionDto; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.favorite.FavoriteFinder; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.Common.Paging; +import org.sonarqube.ws.Favorites.Favorite; +import org.sonarqube.ws.Favorites.SearchResponse; +import org.sonarqube.ws.MediaTypes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.api.resources.Qualifiers.FILE; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.client.WsRequest.Method.POST; + +public class SearchActionTest { + private static final int USER_ID = 123; + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone().login().setUserId(USER_ID); + @Rule + public DbTester db = DbTester.create(); + private DbClient dbClient = db.getDbClient(); + private DbSession dbSession = db.getSession(); + + private FavoriteFinder favoriteFinder = new FavoriteFinder(dbClient, userSession); + + private WsActionTester ws = new WsActionTester(new SearchAction(favoriteFinder, dbClient, userSession)); + + @Test + public void return_favorites() { + ComponentDto project = newProjectDto("P1").setKey("K1").setName("N1"); + addComponent(project); + addComponent(newFileDto(project).setKey("K11").setName("N11")); + addComponent(newProjectDto("P2").setKey("K2").setName("N2")); + + SearchResponse result = call(); + + assertThat(result.getPaging()) + .extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal) + .containsExactly(1, 100, 3); + assertThat(result.getFavoritesList()) + .extracting(Favorite::getKey, Favorite::getName, Favorite::getQualifier) + .containsOnly( + tuple("K1", "N1", PROJECT), + tuple("K11", "N11", FILE), + tuple("K2", "N2", PROJECT)); + } + + @Test + public void empty_list() { + SearchResponse result = call(); + + assertThat(result.getFavoritesCount()).isEqualTo(0); + assertThat(result.getFavoritesList()).isEmpty(); + } + + @Test + public void filter_authorized_components() { + addComponent(newProjectDto().setKey("K1")); + ComponentDto unauthorizedProject = db.components().insertComponent(newProjectDto()); + db.favorites().add(unauthorizedProject, USER_ID); + + SearchResponse result = call(); + + assertThat(result.getFavoritesCount()).isEqualTo(1); + assertThat(result.getFavorites(0).getKey()).isEqualTo("K1"); + } + + @Test + public void paginate_results() { + IntStream.rangeClosed(1, 9) + .forEach(i -> addComponent(newProjectDto().setKey("K" + i).setName("N" + i))); + ComponentDto unauthorizedProject = db.components().insertComponent(newProjectDto()); + db.favorites().add(unauthorizedProject, USER_ID); + + SearchResponse result = call(2, 3); + + assertThat(result.getFavoritesCount()).isEqualTo(3); + assertThat(result.getFavoritesList()) + .extracting(Favorite::getKey) + .containsExactly("K4", "K5", "K6"); + + } + + @Test + public void return_only_users_favorite() { + addComponent(newProjectDto().setKey("K1")); + ComponentDto otherUserFavorite = newProjectDto().setKey("K42"); + db.components().insertComponent(otherUserFavorite); + db.favorites().add(otherUserFavorite, 42L); + dbClient.userPermissionDao().insert(dbSession, new UserPermissionDto("O1", UserRole.USER, 42L, otherUserFavorite.getId())); + db.commit(); + + SearchResponse result = call(); + + assertThat(result.getFavoritesList()).extracting(Favorite::getKey).containsExactly("K1"); + } + + @Test + public void favorites_ordered_by_name() { + addComponent(newProjectDto().setName("N2")); + addComponent(newProjectDto().setName("N3")); + addComponent(newProjectDto().setName("N1")); + + SearchResponse result = call(); + + assertThat(result.getFavoritesList()).extracting(Favorite::getName) + .containsExactly("N1", "N2", "N3"); + } + + @Test + public void json_example() { + addComponent(newProjectDto().setKey("K1").setName("Samba")); + addComponent(newProjectDto().setKey("K2").setName("Apache HBase")); + addComponent(newProjectDto().setKey("K3").setName("JDK9")); + + String result = ws.newRequest().execute().getInput(); + + assertJson(result).isSimilarTo(getClass().getResource("search-example.json")); + } + + @Test + public void definition() { + WebService.Action definition = ws.getDef(); + + assertThat(definition.key()).isEqualTo("search"); + assertThat(definition.responseExampleAsString()).isNotEmpty(); + } + + @Test + public void fail_if_not_authenticated() { + userSession.anonymous(); + + expectedException.expect(UnauthorizedException.class); + + call(); + } + + private void addComponent(ComponentDto component) { + db.components().insertComponent(component); + db.favorites().add(component, USER_ID); + dbClient.userPermissionDao().insert(dbSession, new UserPermissionDto("O1", UserRole.USER, USER_ID, component.getId())); + db.commit(); + } + + private SearchResponse call(@Nullable Integer page, @Nullable Integer pageSize) { + TestRequest request = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setMethod(POST.name()); + setNullable(page, p -> request.setParam(Param.PAGE, p.toString())); + setNullable(pageSize, ps -> request.setParam(Param.PAGE_SIZE, ps.toString())); + + InputStream response = request.execute().getInputStream(); + + try { + return SearchResponse.parseFrom(response); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private SearchResponse call() { + return call(null, null); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/FavouritesWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/FavouritesWsTest.java deleted file mode 100644 index b3b82202ea6..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/FavouritesWsTest.java +++ /dev/null @@ -1,39 +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.user.ws; - -import org.junit.Test; -import org.sonar.api.server.ws.WebService; -import org.sonar.server.ws.WsTester; - -import static org.assertj.core.api.Assertions.assertThat; - -public class FavouritesWsTest { - - WsTester tester = new WsTester(new FavouritesWs()); - - @Test - public void define_ws() { - WebService.Controller controller = tester.controller("api/favourites"); - assertThat(controller).isNotNull(); - assertThat(controller.description()).isNotEmpty(); - assertThat(controller.actions()).hasSize(1); - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsModuleTest.java index acf5f1c6fdb..845e0ef9137 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsModuleTest.java @@ -30,6 +30,6 @@ public class UsersWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new UsersWsModule().configure(container); - assertThat(container.size()).isEqualTo(2 + 12); + assertThat(container.size()).isEqualTo(2 + 11); } } diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/favourites_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/favourites_controller.rb deleted file mode 100644 index c1c3984b027..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/favourites_controller.rb +++ /dev/null @@ -1,89 +0,0 @@ -# -# SonarQube, open source software quality management tool. -# Copyright (C) 2008-2016 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. -# - -require 'json' - -class Api::FavouritesController < Api::ApiController - - before_filter :login_required - - # - # GET /api/favourites - # curl http://localhost:9000/api/favourites -v -u admin:admin - # - def index - respond_to do |format| - format.json { render :json => jsonp(favourites_to_json(current_user.favourites)) } - format.xml { render :xml => favourites_to_xml(current_user.favourites) } - format.text { render :text => text_not_supported } - end - end - - def favourites_to_json(favourites=[]) - json=[] - favourites.each do |f| - json<<favourite_to_json(f) - end - json - end - - def favourite_to_json(favourite) - hash={} - hash['id']=favourite.id - hash['key']=favourite.key - hash['name']=favourite.name - hash['scope']=favourite.scope - hash['branch']=favourite.branch if favourite.branch - hash['lname']=favourite.long_name if favourite.long_name - hash['lang']=favourite.language if favourite.language - hash['qualifier']=favourite.qualifier - hash - end - - def favourites_to_xml(favourites, xml=Builder::XmlMarkup.new(:indent => 0)) - xml.favourites do - favourites.each do |f| - xml.favourite do - xml.id(f.id) - xml.key(f.key) - xml.name(f.name) - xml.lname(f.long_name) if f.long_name - xml.branch(f.branch) if f.branch - xml.scope(f.scope) - xml.qualifier(f.qualifier) - xml.lang(f.language) if f.language - end - end - end - end - - def favourite_to_xml(favourite, xml=Builder::XmlMarkup.new(:indent => 0)) - xml.favourite do - xml.id(f.id) - xml.key(f.key) - xml.name(f.name) - xml.lname(f.long_name) if f.long_name - xml.branch(f.branch) if f.branch - xml.scope(f.scope) - xml.qualifier(f.qualifier) - xml.lang(f.language) if f.language - end - end -end diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesService.java index 91eede2140b..c24e8c3f7a1 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesService.java @@ -20,12 +20,17 @@ package org.sonarqube.ws.client.favorite; +import javax.annotation.Nullable; +import org.sonar.api.server.ws.WebService.Param; +import org.sonarqube.ws.Favorites.SearchResponse; import org.sonarqube.ws.client.BaseService; +import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.PostRequest; import org.sonarqube.ws.client.WsConnector; import static org.sonarqube.ws.client.favorite.FavoritesWsParameters.ACTION_ADD; import static org.sonarqube.ws.client.favorite.FavoritesWsParameters.ACTION_REMOVE; +import static org.sonarqube.ws.client.favorite.FavoritesWsParameters.ACTION_SEARCH; import static org.sonarqube.ws.client.favorite.FavoritesWsParameters.CONTROLLER_FAVORITES; import static org.sonarqube.ws.client.favorite.FavoritesWsParameters.PARAM_COMPONENT; @@ -45,4 +50,16 @@ public class FavoritesService extends BaseService { call(post); } + + public SearchResponse search(@Nullable Integer page, @Nullable Integer pageSize) { + GetRequest get = new GetRequest(path(ACTION_SEARCH)); + if (page != null) { + get.setParam(Param.PAGE, page); + } + if (pageSize != null) { + get.setParam(Param.PAGE_SIZE, pageSize); + } + + return call(get, SearchResponse.parser()); + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesWsParameters.java index 3c39c735ede..bca2767e415 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/favorite/FavoritesWsParameters.java @@ -25,6 +25,7 @@ public class FavoritesWsParameters { public static final String ACTION_ADD = "add"; public static final String ACTION_REMOVE = "remove"; + public static final String ACTION_SEARCH = "search"; public static final String PARAM_COMPONENT = "component"; diff --git a/sonar-ws/src/main/protobuf/ws-favorites.proto b/sonar-ws/src/main/protobuf/ws-favorites.proto new file mode 100644 index 00000000000..64a75848238 --- /dev/null +++ b/sonar-ws/src/main/protobuf/ws-favorites.proto @@ -0,0 +1,39 @@ +// SonarQube, open source software quality management tool. +// Copyright (C) 2008-2016 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. + +syntax = "proto2"; + +package sonarqube.ws.favorite; + +import "ws-commons.proto"; + +option java_package = "org.sonarqube.ws"; +option java_outer_classname = "Favorites"; +option optimize_for = SPEED; + +// WS api/favorites/search +message SearchResponse { + optional sonarqube.ws.commons.Paging paging = 1; + repeated Favorite favorites = 2; +} + +message Favorite { + optional string key = 1; + optional string name = 2; + optional string qualifier = 3; +} |