First step for sub resources

This commit is contained in:
René Pfeuffer
2018-05-30 15:40:31 +02:00
parent b9d9d1c907
commit 8770fd2a76
5 changed files with 353 additions and 137 deletions

View File

@@ -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<String, Link> 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<Call> 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<String, Link> 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;
}
}
}

View File

@@ -1,16 +1,15 @@
package sonia.scm.api.rest.resources; package sonia.scm.api.rest.resources;
import org.apache.shiro.SecurityUtils;
import org.mapstruct.AfterMapping; import org.mapstruct.AfterMapping;
import org.mapstruct.Context; import org.mapstruct.Context;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import sonia.scm.security.Role;
import sonia.scm.user.User; import sonia.scm.user.User;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.util.HashMap;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@Mapper @Mapper
@@ -25,43 +24,17 @@ public abstract class User2UserDtoMapper {
@AfterMapping @AfterMapping
void appendLinks(@MappingTarget UserDto target, @Context UriInfo uriInfo) { void appendLinks(@MappingTarget UserDto target, @Context UriInfo uriInfo) {
LinkMapBuilder builder = new LinkMapBuilder(uriInfo); LinkMapBuilder userLinkBuilder = new LinkMapBuilder(uriInfo, UserNewResource.class, UserNewResource.UserSubResource.class);
builder.add("self", "get", target.getName()); LinkMapBuilder collectionLinkBuilder = new LinkMapBuilder(uriInfo, UserNewResource.class, UserNewResource.UsersResource.class);
builder.add("delete", "delete", target.getName()); userLinkBuilder.add("self").method("getUserSubResource").parameters(target.getName()).method("get").parameters();
builder.add("update", "update", target.getName()); if (SecurityUtils.getSubject().hasRole(Role.ADMIN)) {
builder.add("create", "create"); userLinkBuilder.add("delete").method("getUserSubResource").parameters(target.getName()).method("delete").parameters();
target.setLinks(builder.getLinkMap()); userLinkBuilder.add("update").method("getUserSubResource").parameters(target.getName()).method("update").parameters();
} collectionLinkBuilder.add("create").method("getUsersResource").parameters().method("create").parameters();
private static class LinkMapBuilder {
private final UriInfo uriInfo;
private final Map<String, Link> 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<String, Link> 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);
} }
Map<String, Link> join = new HashMap<>();
join.putAll(userLinkBuilder.getLinkMap());
join.putAll(collectionLinkBuilder.getLinkMap());
target.setLinks(join);
} }
} }

View File

@@ -51,105 +51,124 @@ public class UserNewResource extends AbstractManagerResource<User, UserException
return PATH_PART; return PATH_PART;
} }
@GET
@Path("{id}")
@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();
}
}
/**
* Returns all users. <strong>Note:</strong> 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<User> items = fetchItems(sortby, desc, start, limit);
List<UserDto> collect = items.stream().map(user -> userToDtoMapper.userToUserDto(user, uriInfo)).collect(Collectors.toList());
return Response.ok(new GenericEntity<Collection<UserDto>>(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("") @Path("")
@StatusCodes({ public UsersResource getUsersResource()
@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 new UsersResource();
return super.create(uriInfo, user);
} }
@DELETE public class UsersResource
@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)
{ {
return super.delete(name); /**
* Returns all users. <strong>Note:</strong> 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<User> items = fetchItems(sortby, desc, start, limit);
List<UserDto> collect = items.stream().map(user -> userToDtoMapper.userToUserDto(user, uriInfo)).collect(Collectors.toList());
return Response.ok(new GenericEntity<Collection<UserDto>>(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);
}
} }
} }

View File

@@ -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/"));
}
}

View File

@@ -1,5 +1,8 @@
package sonia.scm.api.rest.resources; 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.Before;
import org.junit.Test; import org.junit.Test;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
@@ -10,6 +13,7 @@ import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -17,6 +21,8 @@ public class User2UserDtoMapperTest {
private final User2UserDtoMapper mapper = Mappers.getMapper(User2UserDtoMapper.class); private final User2UserDtoMapper mapper = Mappers.getMapper(User2UserDtoMapper.class);
private final UriInfo uriInfo = mock(UriInfo.class); private final UriInfo uriInfo = mock(UriInfo.class);
private final Subject subject = mock(Subject.class);
private ThreadState subjectThreadState = new SubjectThreadState(subject);
private URI baseUri; private URI baseUri;
@@ -24,12 +30,14 @@ public class User2UserDtoMapperTest {
public void init() throws URISyntaxException { public void init() throws URISyntaxException {
baseUri = new URI("http://example.com/base/"); baseUri = new URI("http://example.com/base/");
when(uriInfo.getBaseUri()).thenReturn(baseUri); when(uriInfo.getBaseUri()).thenReturn(baseUri);
subjectThreadState.bind();
} }
@Test @Test
public void shouldMapLinks() { public void shouldMapLinks_forAdmin() {
User user = new User(); User user = new User();
user.setName("abc"); user.setName("abc");
when(subject.hasRole("admin")).thenReturn(true);
UserDto userDto = mapper.userToUserDto(user, uriInfo); 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()); 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 @Test
public void shouldMapFields() { public void shouldMapFields() {
User user = new User(); User user = new User();