From 1bcc35d48bff712395b4e5a4f95411b53815ce03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 13:11:18 +0200 Subject: [PATCH] Support reading object by other identifiers than id Therefore split adapter class for single entity and collection handling. --- .../scm/repository/RepositoryManager.java | 14 +++- ... => CollectionResourceManagerAdapter.java} | 33 +------- .../v2/resources/GroupCollectionResource.java | 18 ++++- .../scm/api/v2/resources/GroupResource.java | 4 +- .../resources/IdResourceManagerAdapter.java | 51 ++++++++++++ .../api/v2/resources/RepositoryResource.java | 8 +- .../SingleResourceManagerAdapter.java | 77 +++++++++++++++++++ .../v2/resources/UserCollectionResource.java | 18 ++++- .../scm/api/v2/resources/UserResource.java | 4 +- .../resources/RepositoryRootResourceTest.java | 77 +++++++++++++++++++ 10 files changed, 254 insertions(+), 50 deletions(-) rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{ResourceManagerAdapter.java => CollectionResourceManagerAdapter.java} (65%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 7cbac2b52e..ca1d5ab743 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -38,13 +38,11 @@ package sonia.scm.repository; import sonia.scm.Type; import sonia.scm.TypeManager; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.io.IOException; - import java.util.Collection; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * The central class for managing {@link Repository} objects. @@ -149,4 +147,12 @@ public interface RepositoryManager */ @Override public RepositoryHandler getHandler(String type); + + default Repository getByNamespace(String namespace, String name) { + return getAll() + .stream() + .filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace)) + .findFirst() + .orElse(null); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java similarity index 65% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java index c2aab1a058..925198756a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java @@ -26,43 +26,14 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? -class ResourceManagerAdapter extends AbstractManagerResource { - ResourceManagerAdapter(Manager manager) { + CollectionResourceManagerAdapter(Manager manager) { super(manager); } - /** - * Reads the model object for the given id, transforms it to a dto and returns a corresponding http response. - * This handles all corner cases, eg. no matching object for the id or missing privileges. - */ - Response get(String id, Function mapToDto) { - MODEL_OBJECT modelObject = manager.get(id); - if (modelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - DTO dto = mapToDto.apply(modelObject); - return Response.ok(dto).build(); - } - - /** - * Update the model object for the given id according to the given function and returns a corresponding http response. - * This handles all corner cases, eg. no matching object for the id or missing privileges. - */ - public Response update(String id, Function applyChanges) { - MODEL_OBJECT existingModelObject = manager.get(id); - if (existingModelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); - if (!id.equals(changedModelObject.getId())) { - return Response.status(BAD_REQUEST).entity("illegal change of id").build(); - } - return update(id, changedModelObject); - } - /** * Reads all model objects in a paged way, maps them using the given function and returns a corresponding http response. * This handles all corner cases, eg. missing privileges. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 2d777c7487..195efc56ae 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.group.Group; import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -19,14 +29,14 @@ public class GroupCollectionResource { private final GroupCollectionToDtoMapper groupCollectionToDtoMapper; private final ResourceLinks resourceLinks; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public GroupCollectionResource(GroupManager manager, GroupDtoToGroupMapper dtoToGroupMapper, GroupCollectionToDtoMapper groupCollectionToDtoMapper, ResourceLinks resourceLinks) { this.dtoToGroupMapper = dtoToGroupMapper; this.groupCollectionToDtoMapper = groupCollectionToDtoMapper; this.resourceLinks = resourceLinks; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 3c1f2aeb64..7a490b6d7a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -22,14 +22,14 @@ public class GroupResource { private final GroupToGroupDtoMapper groupToGroupDtoMapper; private final GroupDtoToGroupMapper dtoToGroupMapper; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper, GroupDtoToGroupMapper groupDtoToGroupMapper) { this.groupToGroupDtoMapper = groupToGroupDtoMapper; this.dtoToGroupMapper = groupDtoToGroupMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java new file mode 100644 index 0000000000..c7bcc3b6b1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.Manager; +import sonia.scm.ModelObject; +import sonia.scm.PageResult; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter}. + */ +@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? +class IdResourceManagerAdapter { + + private final Manager manager; + + private final SingleResourceManagerAdapter singleAdapter; + private final CollectionResourceManagerAdapter collectionAdapter; + + IdResourceManagerAdapter(Manager manager) { + this.manager = manager; + singleAdapter = new SingleResourceManagerAdapter<>(manager); + collectionAdapter = new CollectionResourceManagerAdapter<>(manager); + } + + Response get(String id, Function mapToDto) { + return singleAdapter.get(() -> manager.get(id), mapToDto); + } + + public Response update(String id, Function applyChanges) { + return singleAdapter.update(() -> manager.get(id), applyChanges); + } + + public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function, CollectionDto> mapToDto) { + return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto); + } + + public Response create(DTO dto, Supplier modelObjectSupplier, Function uriCreator) throws IOException, EXCEPTION { + return collectionAdapter.create(dto, modelObjectSupplier, uriCreator); + } + + public Response delete(String id) { + return singleAdapter.delete(id); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c171dc5d83..c3cb349529 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -19,12 +19,14 @@ public class RepositoryResource { private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; - private final ResourceManagerAdapter adapter; + private final RepositoryManager manager; + private final SingleResourceManagerAdapter adapter; @Inject public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { + this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new SingleResourceManagerAdapter<>(manager); } @GET @@ -39,6 +41,6 @@ public class RepositoryResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { - return adapter.get("31QwjAKOK2", repositoryToDtoMapper::map); + return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java new file mode 100644 index 0000000000..a56111d893 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -0,0 +1,77 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.Manager; +import sonia.scm.ModelObject; +import sonia.scm.api.rest.resources.AbstractManagerResource; + +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.Response; +import java.util.Collection; +import java.util.function.Function; +import java.util.function.Supplier; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; + +/** + * Adapter from resource http endpoints to managers. + * + * Provides common CRUD operations and DTO to Model Object mapping to keep Resources more DRY. + * + * @param The type of the model object, eg. {@link sonia.scm.user.User}. + * @param The corresponding transport object, eg. {@link UserDto}. + * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. + */ +@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? +class SingleResourceManagerAdapter extends AbstractManagerResource { + + SingleResourceManagerAdapter(Manager manager) { + super(manager); + } + + /** + * Reads the model object for the given id, transforms it to a dto and returns a corresponding http response. + * This handles all corner cases, eg. no matching object for the id or missing privileges. + */ + Response get(Supplier reader, Function mapToDto) { + MODEL_OBJECT modelObject = reader.get(); + if (modelObject == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + DTO dto = mapToDto.apply(modelObject); + return Response.ok(dto).build(); + } + + /** + * Update the model object for the given id according to the given function and returns a corresponding http response. + * This handles all corner cases, eg. no matching object for the id or missing privileges. + */ + public Response update(Supplier reader, Function applyChanges) { + MODEL_OBJECT existingModelObject = reader.get(); + if (existingModelObject == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); + if (!getId(existingModelObject).equals(getId(changedModelObject))) { + return Response.status(BAD_REQUEST).entity("illegal change of id").build(); + } + return update(getId(existingModelObject), changedModelObject); + } + + @Override + protected GenericEntity> createGenericEntity(Collection modelObjects) { + throw new UnsupportedOperationException(); + } + + @Override + protected String getId(MODEL_OBJECT item) { + return item.getId(); + } + + @Override + protected String getPathPart() { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 4b88ea48bf..8daddd67c1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.user.User; import sonia.scm.user.UserException; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -18,14 +28,14 @@ public class UserCollectionResource { private final UserCollectionToDtoMapper userCollectionToDtoMapper; private final ResourceLinks resourceLinks; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper, UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) { this.dtoToUserMapper = dtoToUserMapper; this.userCollectionToDtoMapper = userCollectionToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); this.resourceLinks = resourceLinks; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index bf747bab4d..2cfcdfe772 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -23,13 +23,13 @@ public class UserResource { private final UserDtoToUserMapper dtoToUserMapper; private final UserToUserDtoMapper userToDtoMapper; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) { this.dtoToUserMapper = dtoToUserMapper; this.userToDtoMapper = userToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** 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 new file mode 100644 index 0000000000..74de87eb67 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -0,0 +1,77 @@ +package sonia.scm.api.v2.resources; + +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import java.net.URI; +import java.net.URISyntaxException; + +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class RepositoryRootResourceTest { + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Mock + private RepositoryManager repositoryManager; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ResourceLinks resourceLinks; + @InjectMocks + private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; + + @Before + public void prepareEnvironment() { + initMocks(this); + ResourceLinksMock.initMock(resourceLinks, URI.create("/")); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + } + + @Test + public void shouldFailForNotExistingRepository() throws URISyntaxException { + mockRepository("space", "repo"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NOT_FOUND, response.getStatus()); + } + + @Test + public void shouldFindExistingRepository() throws URISyntaxException { + mockRepository("space", "repo"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); + } + + private Repository mockRepository(String namespace, String name) { + Repository repository = new Repository(); + repository.setNamespace(namespace); + repository.setName(name); + when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository); + return repository; + } +}