Assimilates User and Group REST Resource Annotations and docs

This commit is contained in:
Johannes Schnatterer
2018-06-27 14:30:24 +02:00
parent 21a6b0050a
commit cba5bf7ba4
7 changed files with 126 additions and 57 deletions

View File

@@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader; 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.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.group.Group; import sonia.scm.group.Group;
@@ -27,7 +28,8 @@ import static sonia.scm.api.v2.resources.ResourceLinks.group;
@Produces(VndMediaType.GROUP_COLLECTION) @Produces(VndMediaType.GROUP_COLLECTION)
public class GroupCollectionResource { 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 GroupDtoToGroupMapper dtoToGroupMapper;
private final GroupCollectionToDtoMapper groupCollectionToDtoMapper; 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}). * Returns all groups for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}).
* <strong>Note:</strong> This method requires admin privileges. *
* <strong>Note:</strong> This method requires "group" privilege.
* *
* @param request the current request * @param request the current request
* @param page the number of the requested page * @param page the number of the requested page
* @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE}) * @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE})
* @param sortBy sort parameter * @param sortBy sort parameter
* @param desc sort direction desc or aesc * @param desc sort direction desc or aesc
* @return
*/ */
@GET @GET
@Path("") @Path("")
@TypeHint(GroupDto[].class) @TypeHint(GroupDto[].class)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 200, condition = "success"), @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") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response getAll(@Context Request request, public Response getAll(@Context Request request,
@@ -65,7 +68,8 @@ public class GroupCollectionResource {
@QueryParam("sortby") String sortBy, @QueryParam("sortby") String sortBy,
@DefaultValue("false") @DefaultValue("false")
@QueryParam("desc") boolean desc) { @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 @POST
@Path("") @Path("")
@Consumes(VndMediaType.GROUP)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 201, condition = "create success", additionalHeaders = { @ResponseCode(code = 201, condition = "create success"),
@ResponseHeader(name = "Location", description = "uri to the created group") @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 = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 409, condition = "conflict, a group with this name already exists"), @ResponseCode(code = 409, condition = "conflict, a group with this name already exists"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
@TypeHint(TypeHint.NO_CONTENT.class) @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 { 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()));
} }
} }

View File

@@ -9,6 +9,7 @@ import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.PUT; 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.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
@Produces(VndMediaType.GROUP)
public class GroupResource { public class GroupResource {
private final GroupToGroupDtoMapper groupToGroupDtoMapper; private final GroupToGroupDtoMapper groupToGroupDtoMapper;
@@ -32,12 +32,23 @@ public class GroupResource {
this.adapter = new ResourceManagerAdapter<>(manager); this.adapter = new ResourceManagerAdapter<>(manager);
} }
@Path("") /**
* Returns a group.
*
* <strong>Note:</strong> This method requires "group" privilege.
*
* @param request the current request
* @param id the id/name of the group
*
*/
@GET @GET
@Path("")
@Produces(VndMediaType.GROUP)
@TypeHint(GroupDto.class) @TypeHint(GroupDto.class)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 200, condition = "success"), @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 = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
@@ -45,15 +56,47 @@ public class GroupResource {
return adapter.get(id, groupToGroupDtoMapper::map); return adapter.get(id, groupToGroupDtoMapper::map);
} }
@Path("") /**
* Deletes a group.
*
* <strong>Note:</strong> This method requires "group" privilege.
*
* @param name the name of the group to delete.
*
*/
@DELETE @DELETE
public Response delete(@PathParam("id") String id) { @Path("")
throw new RuntimeException(); @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.
*
* <strong>Note:</strong> This method requires "group" privilege.
*
* @param name name of the group to be modified
* @param groupDto group object to modify
*/
@PUT @PUT
public Response update(@PathParam("id") String id) { @Path("")
throw new RuntimeException(); @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");
} }
} }

View File

@@ -4,10 +4,13 @@ import javax.inject.Inject;
import javax.inject.Provider; import javax.inject.Provider;
import javax.ws.rs.Path; import javax.ws.rs.Path;
/**
* RESTful Web Service Resource to manage groups and their members.
*/
@Path(GroupRootResource.GROUPS_PATH_V2) @Path(GroupRootResource.GROUPS_PATH_V2)
public class GroupRootResource { public class GroupRootResource {
public static final String GROUPS_PATH_V2 = "v2/groups/"; static final String GROUPS_PATH_V2 = "v2/groups/";
private final Provider<GroupCollectionResource> groupCollectionResource; private final Provider<GroupCollectionResource> groupCollectionResource;
private final Provider<GroupResource> groupResource; private final Provider<GroupResource> groupResource;

View File

@@ -16,11 +16,17 @@ import java.util.function.Supplier;
/** /**
* Adapter from resource http endpoints to managers. * Adapter from resource http endpoints to managers.
*
* Provides common CRUD operations and DTO to Model Object mapping to keep Resources more DRY.
*
* @param <MODEL_OBJECT> The type of the model object, eg. {@link sonia.scm.user.User}. * @param <MODEL_OBJECT> The type of the model object, eg. {@link sonia.scm.user.User}.
* @param <DTO> The corresponding transport object, eg. {@link UserDto}. * @param <DTO> The corresponding transport object, eg. {@link UserDto}.
* @param <EXCEPTION> The exception type for the model object, eg. {@link sonia.scm.user.UserException}. * @param <EXCEPTION> The exception type for the model object, eg. {@link sonia.scm.user.UserException}.
*/ */
class ResourceManagerAdapter<MODEL_OBJECT extends ModelObject, DTO extends HalRepresentation, EXCEPTION extends Exception> extends AbstractManagerResource<MODEL_OBJECT, EXCEPTION> { @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class ResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation,
EXCEPTION extends Exception> extends AbstractManagerResource<MODEL_OBJECT, EXCEPTION> {
ResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager) { ResourceManagerAdapter(Manager<MODEL_OBJECT, EXCEPTION> manager) {
super(manager); super(manager);

View File

@@ -11,6 +11,7 @@ import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; 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}). * Returns all users for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}).
* *
* <strong>Note:</strong> This method requires "user" privileges. * <strong>Note:</strong> This method requires "user" privilege.
* *
* @param request the current request * @param request the current request
* @param page the number of the requested page * @param page the number of the requested page
@@ -58,7 +59,8 @@ public class UserCollectionResource {
@TypeHint(UserDto[].class) @TypeHint(UserDto[].class)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 200, condition = "success"), @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") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response getAll(@Context Request request, public Response getAll(@Context Request request,
@@ -66,30 +68,33 @@ public class UserCollectionResource {
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
@QueryParam("sortby") String sortBy, @QueryParam("sortby") String sortBy,
@DefaultValue("false") @QueryParam("desc") boolean desc) { @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. * Creates a new user.
* *
* <strong>Note:</strong> This method requires "user" privileges. * <strong>Note:</strong> This method requires "user" privilege.
* *
* @param userDto The user to be created. * @param userDto The user to be created.
* @return A response with the link to the new user (if created successfully). * @return A response with the link to the new user (if created successfully).
*/ */
@POST @POST
@Path("") @Path("")
@Consumes(VndMediaType.USER)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 201, condition = "create success", additionalHeaders = { @ResponseCode(code = 201, condition = "create success"),
@ResponseHeader(name = "Location", description = "uri to the created user") @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 = 403, condition = "forbidden, the current user does not have the \"user\" privilege"),
@ResponseCode(code = 409, condition = "conflict, a user with this name already exists"), @ResponseCode(code = 409, condition = "conflict, a user with this name already exists"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
@TypeHint(TypeHint.NO_CONTENT.class) @TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user")) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
public Response create(@Context UriInfo uriInfo, UserDto userDto) throws IOException, UserException { 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()));
} }
} }

View File

@@ -9,6 +9,7 @@ import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
@@ -38,7 +39,7 @@ public class UserResource {
/** /**
* Returns a user. * Returns a user.
* *
* <strong>Note:</strong> This method requires "user" privileges. * <strong>Note:</strong> This method requires "user" privilege.
* *
* @param request the current request * @param request the current request
* @param id the id/name of the user * @param id the id/name of the user
@@ -49,7 +50,8 @@ public class UserResource {
@TypeHint(UserDto.class) @TypeHint(UserDto.class)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 200, condition = "success"), @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 = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
@@ -57,31 +59,10 @@ public class UserResource {
return adapter.get(id, userToDtoMapper::map); return adapter.get(id, userToDtoMapper::map);
} }
/**
* Modifies the given user.
*
* <strong>Note:</strong> 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. * Deletes a user.
* *
* <strong>Note:</strong> This method requires "user" privileges. * <strong>Note:</strong> This method requires "user" privilege.
* *
* @param name the name of the user to delete. * @param name the name of the user to delete.
* *
@@ -89,12 +70,36 @@ public class UserResource {
@DELETE @DELETE
@Path("") @Path("")
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 204, condition = "delete success"), @ResponseCode(code = 204, condition = "delete success or nothing to delete"),
@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") @ResponseCode(code = 500, condition = "internal server error")
}) })
@TypeHint(TypeHint.NO_CONTENT.class) @TypeHint(TypeHint.NO_CONTENT.class)
public Response delete(@PathParam("id") String name) { public Response delete(@PathParam("id") String name) {
return adapter.delete(name); return adapter.delete(name);
} }
/**
* Modifies the given user.
*
* <strong>Note:</strong> 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()));
}
} }

View File

@@ -11,6 +11,7 @@ import javax.ws.rs.Path;
public class UserRootResource { public class UserRootResource {
static final String USERS_PATH_V2 = "v2/users/"; static final String USERS_PATH_V2 = "v2/users/";
private final Provider<UserCollectionResource> userCollectionResource; private final Provider<UserCollectionResource> userCollectionResource;
private final Provider<UserResource> userResource; private final Provider<UserResource> userResource;