From 8770fd2a76e966f8ed722a3d501d7f0da7808e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 30 May 2018 15:40:31 +0200 Subject: [PATCH] First step for sub resources --- .../api/rest/resources/LinkMapBuilder.java | 108 +++++++++ .../rest/resources/User2UserDtoMapper.java | 55 ++--- .../api/rest/resources/UserNewResource.java | 209 ++++++++++-------- .../rest/resources/LinkMapBuilderTest.java | 94 ++++++++ .../resources/User2UserDtoMapperTest.java | 24 +- 5 files changed, 353 insertions(+), 137 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/LinkMapBuilder.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/rest/resources/LinkMapBuilderTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/LinkMapBuilder.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/LinkMapBuilder.java new file mode 100644 index 0000000000..59d8c65ff7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/LinkMapBuilder.java @@ -0,0 +1,108 @@ +package sonia.scm.api.rest.resources; + +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.util.*; + +class LinkMapBuilder { + private final UriInfo uriInfo; + private final Class[] classes; + private final Map links = new LinkedHashMap<>(); + + LinkMapBuilder(UriInfo uriInfo, Class... classes) { + this.uriInfo = uriInfo; + this.classes = classes; + } + + Builder add(String linkName) { + return new ConcreteBuilder(linkName); + } + + interface Builder { + Parameters method(String method); + } + + interface Parameters { + Builder parameters(String... parameters); + } + + private class ConcreteBuilder implements Builder { + + private final String linkName; + private final List calls = new LinkedList<>(); + + private int callCount = 0; + + ConcreteBuilder(String linkName) { + this.linkName = linkName; + } + + public Parameters method(String method) { + return new ParametersImpl(method); + } + + private class ParametersImpl implements Parameters { + + private final String method; + + ParametersImpl(String method) { + this.method = method; + } + + public Builder parameters(String... parameters) { + return ConcreteBuilder.this.add(method, parameters); + } + } + + private Builder add(String method, String[] parameters) { + this.calls.add(new Call(LinkMapBuilder.this.classes[callCount], method, parameters)); + ++callCount; + if (callCount >= classes.length) { + links.put(linkName, createLink()); + return x -> { + throw new IllegalStateException("no more classes for methods"); + }; + } + return this; + } + + private Link createLink() { + URI baseUri = uriInfo.getBaseUri(); + URI relativeUri = createRelativeUri(); + URI absoluteUri = baseUri.resolve(relativeUri); + return new Link(absoluteUri); + } + + private URI createRelativeUri() { + UriBuilder uriBuilder = userUriBuilder(); + calls.forEach(call -> uriBuilder.path(call.clazz, call.method)); + String[] concatenatedParameters = calls + .stream() + .map(call -> call.parameters) + .flatMap(Arrays::stream) + .toArray(String[]::new); + return uriBuilder.build(concatenatedParameters); + } + + private UriBuilder userUriBuilder() { + return UriBuilder.fromResource(classes[0]); + } + } + + Map getLinkMap() { + return Collections.unmodifiableMap(links); + } + + private static class Call { + private final Class clazz; + private final String method; + private final String[] parameters; + + private Call(Class clazz, String method, String[] parameters) { + this.clazz = clazz; + this.method = method; + this.parameters = parameters; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/User2UserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/User2UserDtoMapper.java index 91a4722339..2ecf0f0990 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/User2UserDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/User2UserDtoMapper.java @@ -1,16 +1,15 @@ package sonia.scm.api.rest.resources; +import org.apache.shiro.SecurityUtils; import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; +import sonia.scm.security.Role; import sonia.scm.user.User; -import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.Map; @Mapper @@ -25,43 +24,17 @@ public abstract class User2UserDtoMapper { @AfterMapping void appendLinks(@MappingTarget UserDto target, @Context UriInfo uriInfo) { - LinkMapBuilder builder = new LinkMapBuilder(uriInfo); - builder.add("self", "get", target.getName()); - builder.add("delete", "delete", target.getName()); - builder.add("update", "update", target.getName()); - builder.add("create", "create"); - target.setLinks(builder.getLinkMap()); - } - - private static class LinkMapBuilder { - private final UriInfo uriInfo; - private final Map links = new LinkedHashMap<>(); - - private LinkMapBuilder(UriInfo uriInfo) { - this.uriInfo = uriInfo; - } - - void add(String linkName, String methodName, String... parameters) { - links.put(linkName, createLink(methodName, parameters)); - } - - Map getLinkMap() { - return Collections.unmodifiableMap(links); - } - - private Link createLink(String methodName, String... parameters) { - URI baseUri = uriInfo.getBaseUri(); - URI relativeUri = createRelativeUri(methodName, parameters); - URI absoluteUri = baseUri.resolve(relativeUri); - return new Link(absoluteUri); - } - - private URI createRelativeUri(String methodName, Object[] parameters) { - return userUriBuilder().path(UserNewResource.class, methodName).build(parameters); - } - - private UriBuilder userUriBuilder() { - return UriBuilder.fromResource(UserNewResource.class); + LinkMapBuilder userLinkBuilder = new LinkMapBuilder(uriInfo, UserNewResource.class, UserNewResource.UserSubResource.class); + LinkMapBuilder collectionLinkBuilder = new LinkMapBuilder(uriInfo, UserNewResource.class, UserNewResource.UsersResource.class); + userLinkBuilder.add("self").method("getUserSubResource").parameters(target.getName()).method("get").parameters(); + if (SecurityUtils.getSubject().hasRole(Role.ADMIN)) { + userLinkBuilder.add("delete").method("getUserSubResource").parameters(target.getName()).method("delete").parameters(); + userLinkBuilder.add("update").method("getUserSubResource").parameters(target.getName()).method("update").parameters(); + collectionLinkBuilder.add("create").method("getUsersResource").parameters().method("create").parameters(); } + Map join = new HashMap<>(); + join.putAll(userLinkBuilder.getLinkMap()); + join.putAll(collectionLinkBuilder.getLinkMap()); + target.setLinks(join); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserNewResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserNewResource.java index a058b1ba5e..12d184e144 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserNewResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserNewResource.java @@ -51,105 +51,124 @@ public class UserNewResource extends AbstractManagerResourceNote: This method requires admin privileges. - * - * @param request the current request - * @param start the start value for paging - * @param limit the limit value for paging - * @param sortby sort parameter - * @param desc sort direction desc or aesc - * - * @return - */ - @GET - @TypeHint(User[].class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getAll(@Context Request request, @Context UriInfo uriInfo, @DefaultValue("0") - @QueryParam("start") int start, @DefaultValue("-1") - @QueryParam("limit") int limit, @QueryParam("sortby") String sortby, - @DefaultValue("false") - @QueryParam("desc") boolean desc) - { - Collection items = fetchItems(sortby, desc, start, limit); - List collect = items.stream().map(user -> userToDtoMapper.userToUserDto(user, uriInfo)).collect(Collectors.toList()); - return Response.ok(new GenericEntity>(collect) {}).build(); - } - - @PUT - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response update(@Context UriInfo uriInfo, - @PathParam("id") String name, UserDto userDto) - { - String originalPassword = manager.get(name).getPassword(); - User user = dtoToUserMapper.userDtoToUser(userDto, originalPassword); - return super.update(name, user); - } - - @POST @Path("") - @StatusCodes({ - @ResponseCode(code = 201, condition = "create success", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the created group") - }), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response create(@Context UriInfo uriInfo, UserDto userDto) + public UsersResource getUsersResource() { - User user = dtoToUserMapper.userDtoToUser(userDto, ""); - return super.create(uriInfo, user); + return new UsersResource(); } - @DELETE - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Override - public Response delete(@PathParam("id") String name) + public class UsersResource { - return super.delete(name); + /** + * Returns all users. Note: This method requires admin privileges. + * + * @param request the current request + * @param start the start value for paging + * @param limit the limit value for paging + * @param sortby sort parameter + * @param desc sort direction desc or aesc + * @return + */ + @GET + @Path("") + @TypeHint(User[].class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response getAll(@Context Request request, @Context UriInfo uriInfo, @DefaultValue("0") + @QueryParam("start") int start, @DefaultValue("-1") + @QueryParam("limit") int limit, @QueryParam("sortby") String sortby, + @DefaultValue("false") + @QueryParam("desc") boolean desc) + { + Collection items = fetchItems(sortby, desc, start, limit); + List collect = items.stream().map(user -> userToDtoMapper.userToUserDto(user, uriInfo)).collect(Collectors.toList()); + return Response.ok(new GenericEntity>(collect) + { + }).build(); + } + + @POST + @Path("") + @StatusCodes({ + @ResponseCode(code = 201, condition = "create success", additionalHeaders = { + @ResponseHeader(name = "Location", description = "uri to the created group") + }), + @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response create(@Context UriInfo uriInfo, UserDto userDto) + { + User user = dtoToUserMapper.userDtoToUser(userDto, ""); + return UserNewResource.this.create(uriInfo, user); + } + } + + @Path("{id}") + public UserSubResource getUserSubResource() + { + return new UserSubResource(); + } + + public class UserSubResource + { + @GET + @Path("") + @TypeHint(UserDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), + @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response get(@Context Request request, @Context UriInfo uriInfo, @PathParam("id") String id) + { + if (SecurityUtils.getSubject().hasRole(Role.ADMIN)) + { + User user = manager.get(id); + UserDto userDto = userToDtoMapper.userToUserDto(user, uriInfo); + return Response.ok(userDto).build(); + } + else + { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } + + @PUT + @Path("") + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response update(@Context UriInfo uriInfo, + @PathParam("id") String name, UserDto userDto) + { + String originalPassword = manager.get(name).getPassword(); + User user = dtoToUserMapper.userDtoToUser(userDto, originalPassword); + return UserNewResource.this.update(name, user); + } + + @DELETE + @Path("") + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success"), + @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response delete(@PathParam("id") String name) + { + return UserNewResource.this.delete(name); + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/LinkMapBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/LinkMapBuilderTest.java new file mode 100644 index 0000000000..d0ce279046 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/LinkMapBuilderTest.java @@ -0,0 +1,94 @@ +package sonia.scm.api.rest.resources; + +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.Path; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.net.URISyntaxException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LinkMapBuilderTest { + + @Path("base") + public static class Main { + @Path("main/{x}") + public Sub sub() { + return null; + } + } + + public static class Sub { + @Path("sub/{y}/{z}") + public Object x() { + return null; + } + } + + @Path("base") + public static class NoParam { + @Path("") + public Object get() { + return null; + } + } + + private UriInfo uriInfo = mock(UriInfo.class); + + @Test + public void shouldBuildSimplePath() { + LinkMapBuilder builder = new LinkMapBuilder(uriInfo, Main.class); + builder + .add("link") + .method("sub") + .parameters("param_x"); + + URI actual = builder.getLinkMap().get("link").getHref(); + assertEquals("http://example.com/base/main/param_x", actual.toString()); + } + + @Test + public void shouldBuildPathOverSubResources() { + LinkMapBuilder builder = new LinkMapBuilder(uriInfo, Main.class, Sub.class); + builder + .add("link") + .method("sub") + .parameters("param_x") + .method("x") + .parameters("param_y", "param_z"); + + URI actual = builder.getLinkMap().get("link").getHref(); + assertEquals("http://example.com/base/main/param_x/sub/param_y/param_z", actual.toString()); + } + + @Test + public void shouldBuildPathWithoutParameters() { + LinkMapBuilder builder = new LinkMapBuilder(uriInfo, NoParam.class); + builder + .add("link") + .method("get") + .parameters(); + + URI actual = builder.getLinkMap().get("link").getHref(); + assertEquals("http://example.com/base", actual.toString()); + } + + @Test(expected = IllegalStateException.class) + public void shouldFailForTooManyMethods() { + LinkMapBuilder builder = new LinkMapBuilder(uriInfo, Main.class); + builder + .add("link") + .method("sub") + .parameters("param_x") + .method("x"); + } + + @Before + public void setBaseUri() throws URISyntaxException { + when(uriInfo.getBaseUri()).thenReturn(new URI("http://example.com/")); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/User2UserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/User2UserDtoMapperTest.java index 860d8afcf4..ea6075059c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/User2UserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/User2UserDtoMapperTest.java @@ -1,5 +1,8 @@ package sonia.scm.api.rest.resources; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadState; import org.junit.Before; import org.junit.Test; import org.mapstruct.factory.Mappers; @@ -10,6 +13,7 @@ import java.net.URI; import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -17,6 +21,8 @@ public class User2UserDtoMapperTest { private final User2UserDtoMapper mapper = Mappers.getMapper(User2UserDtoMapper.class); private final UriInfo uriInfo = mock(UriInfo.class); + private final Subject subject = mock(Subject.class); + private ThreadState subjectThreadState = new SubjectThreadState(subject); private URI baseUri; @@ -24,12 +30,14 @@ public class User2UserDtoMapperTest { public void init() throws URISyntaxException { baseUri = new URI("http://example.com/base/"); when(uriInfo.getBaseUri()).thenReturn(baseUri); + subjectThreadState.bind(); } @Test - public void shouldMapLinks() { + public void shouldMapLinks_forAdmin() { User user = new User(); user.setName("abc"); + when(subject.hasRole("admin")).thenReturn(true); UserDto userDto = mapper.userToUserDto(user, uriInfo); @@ -39,6 +47,20 @@ public class User2UserDtoMapperTest { assertEquals("expected map with create baseUri", baseUri.resolve("usersnew"), userDto.getLinks().get("create").getHref()); } + @Test + public void shouldMapLinks_forNormalUser() { + User user = new User(); + user.setName("abc"); + when(subject.hasRole("user")).thenReturn(true); + + UserDto userDto = mapper.userToUserDto(user, uriInfo); + + assertEquals("expected map with self baseUri", baseUri.resolve("usersnew/abc"), userDto.getLinks().get("self").getHref()); + assertNull("expected map without delete baseUri", userDto.getLinks().get("delete")); + assertNull("expected map without update baseUri", userDto.getLinks().get("update")); + assertNull("expected map without create baseUri", userDto.getLinks().get("create")); + } + @Test public void shouldMapFields() { User user = new User();