From cba5bf7ba4dcff6edabe2a0f07e5f7aaad50c39b Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Wed, 27 Jun 2018 14:30:24 +0200 Subject: [PATCH] Assimilates User and Group REST Resource Annotations and docs --- .../v2/resources/GroupCollectionResource.java | 28 +++++---- .../scm/api/v2/resources/GroupResource.java | 61 ++++++++++++++++--- .../api/v2/resources/GroupRootResource.java | 5 +- .../v2/resources/ResourceManagerAdapter.java | 8 ++- .../v2/resources/UserCollectionResource.java | 23 ++++--- .../scm/api/v2/resources/UserResource.java | 57 +++++++++-------- .../api/v2/resources/UserRootResource.java | 1 + 7 files changed, 126 insertions(+), 57 deletions(-) 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 056d8a7857..c0c8079b57 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 @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; 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; @@ -27,7 +28,8 @@ import static sonia.scm.api.v2.resources.ResourceLinks.group; @Produces(VndMediaType.GROUP_COLLECTION) public class GroupCollectionResource { - public static final int DEFAULT_PAGE_SIZE = 10; + + private static final int DEFAULT_PAGE_SIZE = 10; private final GroupDtoToGroupMapper dtoToGroupMapper; private final GroupCollectionToDtoMapper groupCollectionToDtoMapper; @@ -42,21 +44,22 @@ public class GroupCollectionResource { /** * Returns all groups for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). - * Note: This method requires admin privileges. + * + * Note: This method requires "group" privilege. * * @param request the current request * @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 * @param desc sort direction desc or aesc - * @return */ @GET @Path("") @TypeHint(GroupDto[].class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) public Response getAll(@Context Request request, @@ -65,7 +68,8 @@ public class GroupCollectionResource { @QueryParam("sortby") String sortBy, @DefaultValue("false") @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult)); + return adapter.getAll(page, pageSize, sortBy, desc, + pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult)); } /** @@ -75,17 +79,19 @@ public class GroupCollectionResource { */ @POST @Path("") + @Consumes(VndMediaType.GROUP) @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 = 201, condition = "create success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"), @ResponseCode(code = 409, condition = "conflict, a group with this name already exists"), @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes(VndMediaType.GROUP) + @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group")) public Response create(@Context UriInfo uriInfo, GroupDto groupDto) throws IOException, GroupException { - return adapter.create(groupDto, () -> dtoToGroupMapper.map(groupDto), group -> group(uriInfo).self(group.getName())); + return adapter.create(groupDto, + () -> dtoToGroupMapper.map(groupDto), + group -> group(uriInfo).self(group.getName())); } } 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 4872b2ab27..3dc41a1a59 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 @@ -9,6 +9,7 @@ import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -20,7 +21,6 @@ import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -@Produces(VndMediaType.GROUP) public class GroupResource { private final GroupToGroupDtoMapper groupToGroupDtoMapper; @@ -32,12 +32,23 @@ public class GroupResource { this.adapter = new ResourceManagerAdapter<>(manager); } - @Path("") + /** + * Returns a group. + * + * Note: This method requires "group" privilege. + * + * @param request the current request + * @param id the id/name of the group + * + */ @GET + @Path("") + @Produces(VndMediaType.GROUP) @TypeHint(GroupDto.class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no privileges to read the group"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the group"), @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), @ResponseCode(code = 500, condition = "internal server error") }) @@ -45,15 +56,47 @@ public class GroupResource { return adapter.get(id, groupToGroupDtoMapper::map); } - @Path("") + /** + * Deletes a group. + * + * Note: This method requires "group" privilege. + * + * @param name the name of the group to delete. + * + */ @DELETE - public Response delete(@PathParam("id") String id) { - throw new RuntimeException(); + @Path("") + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response delete(@PathParam("id") String name) { + throw new UnsupportedOperationException("Not yet implemented"); } - @Path("") + /** + * Modifies the given group. + * + * Note: This method requires "group" privilege. + * + * @param name name of the group to be modified + * @param groupDto group object to modify + */ @PUT - public Response update(@PathParam("id") String id) { - throw new RuntimeException(); + @Path("") + @Consumes(VndMediaType.GROUP) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"), + @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(@Context UriInfo uriInfo, @PathParam("id") String name, GroupDto groupDto) { + throw new UnsupportedOperationException("Not yet implemented"); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupRootResource.java index f18df472a2..0d98d6239c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupRootResource.java @@ -4,10 +4,13 @@ import javax.inject.Inject; import javax.inject.Provider; import javax.ws.rs.Path; +/** + * RESTful Web Service Resource to manage groups and their members. + */ @Path(GroupRootResource.GROUPS_PATH_V2) public class GroupRootResource { - public static final String GROUPS_PATH_V2 = "v2/groups/"; + static final String GROUPS_PATH_V2 = "v2/groups/"; private final Provider groupCollectionResource; private final Provider groupResource; 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/ResourceManagerAdapter.java index 2bc2193561..ee4e105a51 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/ResourceManagerAdapter.java @@ -16,11 +16,17 @@ import java.util.function.Supplier; /** * 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}. */ -class ResourceManagerAdapter extends AbstractManagerResource { +@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? +class ResourceManagerAdapter extends AbstractManagerResource { ResourceManagerAdapter(Manager manager) { super(manager); 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 980ed9ce06..61e71fd10e 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 @@ -11,6 +11,7 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -45,7 +46,7 @@ public class UserCollectionResource { /** * Returns all users for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). * - * Note: This method requires "user" privileges. + * Note: This method requires "user" privilege. * * @param request the current request * @param page the number of the requested page @@ -58,7 +59,8 @@ public class UserCollectionResource { @TypeHint(UserDto[].class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user does not have the \"user\" privilege"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) public Response getAll(@Context Request request, @@ -66,30 +68,33 @@ public class UserCollectionResource { @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortby") String sortBy, @DefaultValue("false") @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); + return adapter.getAll(page, pageSize, sortBy, desc, + pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); } /** * Creates a new user. * - * Note: This method requires "user" privileges. + * Note: This method requires "user" privilege. * * @param userDto The user to be created. * @return A response with the link to the new user (if created successfully). */ @POST @Path("") + @Consumes(VndMediaType.USER) @StatusCodes({ - @ResponseCode(code = 201, condition = "create success", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the created user") - }), - @ResponseCode(code = 403, condition = "forbidden, the current user does not have the \"user\" privilege"), + @ResponseCode(code = 201, condition = "create success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"), @ResponseCode(code = 409, condition = "conflict, a user with this name already exists"), @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user")) public Response create(@Context UriInfo uriInfo, UserDto userDto) throws IOException, UserException { - return adapter.create(userDto, () -> dtoToUserMapper.map(userDto, ""), user -> user(uriInfo).self(user.getName())); + return adapter.create(userDto, + () -> dtoToUserMapper.map(userDto, ""), + user -> user(uriInfo).self(user.getName())); } } 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 07008e8e4c..a7a6bcc28f 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 @@ -9,6 +9,7 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -38,7 +39,7 @@ public class UserResource { /** * Returns a user. * - * Note: This method requires "user" privileges. + * Note: This method requires "user" privilege. * * @param request the current request * @param id the id/name of the user @@ -49,7 +50,8 @@ public class UserResource { @TypeHint(UserDto.class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no privileges to read the user"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the user"), @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), @ResponseCode(code = 500, condition = "internal server error") }) @@ -57,31 +59,10 @@ public class UserResource { return adapter.get(id, userToDtoMapper::map); } - /** - * Modifies the given user. - * - * Note: This method requires "user" privileges. - * - * @param name name of the user to be modified - * @param userDto user object to modify - */ - @PUT - @Path("") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 403, condition = "forbidden, the current user does not have the \"user\" privilege"), - @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - public Response update(@Context UriInfo uriInfo, @PathParam("id") String name, UserDto userDto) { - return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword())); - } - /** * Deletes a user. * - * Note: This method requires "user" privileges. + * Note: This method requires "user" privilege. * * @param name the name of the user to delete. * @@ -89,12 +70,36 @@ public class UserResource { @DELETE @Path("") @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success"), - @ResponseCode(code = 403, condition = "forbidden, the current user does not have the \"user\" privilege"), + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) public Response delete(@PathParam("id") String name) { return adapter.delete(name); } + + /** + * Modifies the given user. + * + * Note: This method requires "user" privilege. + * + * @param name name of the user to be modified + * @param userDto user object to modify + */ + @PUT + @Path("") + @Consumes(VndMediaType.USER) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"), + @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(@Context UriInfo uriInfo, @PathParam("id") String name, UserDto userDto) { + return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword())); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserRootResource.java index 779f6323e1..9652a97404 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserRootResource.java @@ -11,6 +11,7 @@ import javax.ws.rs.Path; public class UserRootResource { static final String USERS_PATH_V2 = "v2/users/"; + private final Provider userCollectionResource; private final Provider userResource;