diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNamespaceResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNamespaceResource.java new file mode 100644 index 0000000000..5043412319 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNamespaceResource.java @@ -0,0 +1,112 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.api.v2.resources; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.util.function.Predicate; + +import static com.google.common.base.Strings.isNullOrEmpty; + +public class RepositoryNamespaceResource { + + private static final int DEFAULT_PAGE_SIZE = 10; + + private final CollectionResourceManagerAdapter adapter; + private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; + + @Inject + public RepositoryNamespaceResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper) { + this.adapter = new CollectionResourceManagerAdapter<>(manager, Repository.class); + this.repositoryCollectionToDtoMapper = repositoryCollectionToDtoMapper; + } + + /** + * Returns all repositories from a namespace for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). + * + * Note: This method requires "repository" privilege. + * + * @param page the number of the requested page + * @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE}) + * @param sortBy sort parameter (if empty - undefined sorting) + * @param desc sort direction desc or asc + */ + @GET + @Path("") + @Produces(VndMediaType.REPOSITORY_COLLECTION) + @Operation(summary = "List of repositories from a namespace", description = "Returns all repositories from a namespace for a given page number with a given page size.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + public Response getByNamespace(@PathParam("namespace") String namespace, + @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search + ) { + return adapter.getAll(page, pageSize, createSearchPredicate(namespace, search), sortBy, desc, + pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); + } + + private Predicate createSearchPredicate(String namespace, String search) { + if (isNullOrEmpty(search)) { + return repository -> repository.getNamespace().equals(namespace); + } + SearchRequest searchRequest = new SearchRequest(search, true); + return repository -> repository.getNamespace().equals(namespace) + && SearchUtil.matchesOne(searchRequest, repository.getName(), repository.getNamespace(), repository.getDescription()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java index 3250d456f8..b0a5c40850 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import io.swagger.v3.oas.annotations.OpenAPIDefinition; @@ -44,11 +44,13 @@ public class RepositoryRootResource { static final String REPOSITORIES_PATH_V2 = "v2/repositories/"; private final Provider repositoryResource; + private final Provider repositoryNamespaceResource; private final Provider repositoryCollectionResource; @Inject - public RepositoryRootResource(Provider repositoryResource, Provider repositoryCollectionResource) { + public RepositoryRootResource(Provider repositoryResource, Provider repositoryNamespaceResource, Provider repositoryCollectionResource) { this.repositoryResource = repositoryResource; + this.repositoryNamespaceResource = repositoryNamespaceResource; this.repositoryCollectionResource = repositoryCollectionResource; } @@ -57,6 +59,11 @@ public class RepositoryRootResource { return repositoryResource.get(); } + @Path("{namespace}") + public RepositoryNamespaceResource getNamespaceResource() { + return repositoryNamespaceResource.get(); + } + @Path("") public RepositoryCollectionResource getRepositoryCollectionResource() { return repositoryCollectionResource.get(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index d8e22cb33b..8de14b6b8d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -133,6 +133,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { super.manager = repositoryManager; RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer); + super.repositoryNamespaceResource = new RepositoryNamespaceResource(repositoryManager, repositoryCollectionToDtoMapper); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(scmPathInfoStore.get()).thenReturn(uriInfo); @@ -206,6 +207,40 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertFalse(filterCaptor.getValue().test(new Repository("rep", "rep", "x", "x"))); } + @Test + public void shouldCreateFilterForNamespace() throws URISyntaxException { + PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); + when(repositoryManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); + when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "space", "repo"))); + assertFalse(filterCaptor.getValue().test(new Repository("x", "git", "spaceX", "repository"))); + assertFalse(filterCaptor.getValue().test(new Repository("x", "git", "x", "space"))); + } + + @Test + public void shouldCreateFilterForNamespaceWithQuery() throws URISyntaxException { + PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); + when(repositoryManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); + when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space?q=Rep"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "space", "repo"))); + assertFalse(filterCaptor.getValue().test(new Repository("x", "git", "space", "other"))); + assertFalse(filterCaptor.getValue().test(new Repository("x", "git", "Rep", "Repository"))); + } + @Test public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException { URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java index 58ec1ffb86..527e662cad 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -44,6 +44,7 @@ abstract class RepositoryTestBase { FileHistoryRootResource fileHistoryRootResource; IncomingRootResource incomingRootResource; RepositoryCollectionResource repositoryCollectionResource; + RepositoryNamespaceResource repositoryNamespaceResource; AnnotateResource annotateResource; RepositoryRootResource getRepositoryRootResource() { @@ -65,6 +66,7 @@ abstract class RepositoryTestBase { dtoToRepositoryMapper, manager, repositoryBasedResourceProvider)), + of(repositoryNamespaceResource), of(repositoryCollectionResource)); } }