package org.fluxtream.core.api; import com.sun.jersey.api.Responses; import com.wordnik.swagger.annotations.*; import org.codehaus.jackson.map.ObjectMapper; import org.fluxtream.core.Configuration; import org.fluxtream.core.api.models.BasicGuestModel; import org.fluxtream.core.api.models.PostCommentModel; import org.fluxtream.core.api.models.PostModel; import org.fluxtream.core.auth.AuthHelper; import org.fluxtream.core.domain.Guest; import org.fluxtream.core.domain.GuestDetails; import org.fluxtream.core.domain.Post; import org.fluxtream.core.domain.PostComment; import org.fluxtream.core.services.GuestService; import org.fluxtream.core.services.PostsService; import org.fluxtream.core.services.exceptions.UnauthorizedAccessException; import org.fluxtream.core.utils.Parse; import org.fluxtream.core.utils.RestCallException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.*; /** * Created by candide on 15/01/15. */ @Path("/v1/posts") @Component("RESTPostsStore") @Scope("request") @Api(value = "/v1/posts", description = "CRUD operations on wall posts") public class PostsStore { @Autowired PostsService postsService; @Autowired Configuration env; @Autowired GuestService guestService; @Autowired Parse parse; ObjectMapper objectMapper = new ObjectMapper(); @POST @Path("/") @ApiOperation(value = "Post a message on another user's wall") @ApiResponses({ @ApiResponse(code = 403, message = "If the calling guest and the post's recipient aren't in a coach/coachee relationship"), @ApiResponse(code = 404, message = "If there is no Guest with username `targetGuestUsername`") }) public Response createPost(@ApiParam(value="The message of the post", required=true) @FormParam("message") final String message, @ApiParam(value="Target guest's username", required=true) @FormParam("to") final String targetGuestUsername) throws URISyntaxException { try { Guest targetGuest = guestService.getGuest(targetGuestUsername); if (targetGuest==null) return Responses.notFound().entity("No such guest: " + targetGuestUsername).build(); Post post = postsService.createPost(message, targetGuest.getId()); URI createdURI = new URI(String.format("%sapi/v1/posts/%s", env.get("homeBaseUrl"), post.getId())); GuestDetails guestDetails = guestService.getGuestDetails(targetGuest.getId()); notifyUser(post, guestDetails, "New Post", AuthHelper.getGuest().getGuestName() + " just sent you a message"); return Response.created(createdURI).build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } private void notifyUser(Post post, GuestDetails guestDetails, String title, String alert) { Map<String,String> notificationParams = new HashMap<String,String>(); notificationParams.put("postId", String.valueOf(post.getId())); notificationParams.put("title", title); try { final List<String> installations = guestService.getDeviceIds(guestDetails.guestId); parse.pushNotification(new HashSet<String>(installations), alert, notificationParams); } catch (RestCallException e) { e.printStackTrace(); } } @PUT @Path("/{postId}") @ApiOperation(value = "Edit (the text message of) an existing post") @ApiResponses({ @ApiResponse(code = 403, message = "If the calling guest is not the original author of the post"), @ApiResponse(code = 404, message = "If there is no post with id `postId`") }) public Response updatePost(@ApiParam(value="Post message", required=true) @FormParam("message") final String body, @ApiParam(value="Post ID", required=true) @PathParam("postId") final long postId) throws URISyntaxException { try { Post post = postsService.updatePost(postId, body); if (post==null) return Responses.notFound().entity("No such post: " + postId).build(); URI createdURI = new URI(String.format("%srest/v1/posts/%s", env.get("homeBaseUrl"), post.getId())); return Response.created(createdURI).build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } @GET @Path("/{postId}") @ApiOperation(value = "Read a post", response = PostModel.class) @Produces(MediaType.APPLICATION_JSON) @ApiResponses({ @ApiResponse(code = 403, message = "If the calling guest is neither the sender nor the recipient of the post"), @ApiResponse(code = 404, message = "If there is no post with id `postId`") }) public Response readPost(@ApiParam(value="Post ID", required=true) @PathParam("postId") final long postId, @ApiParam(value="Include comments?") @QueryParam("includeComments") final boolean includeComments) throws IOException { try { Post post = postsService.getPost(postId); if (post==null) return Responses.notFound().entity("No such post: " + postId).build(); PostModel postModel = getPostModel(post, includeComments); return Response.ok(objectMapper.writeValueAsString(postModel)).build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } private PostModel getPostModel(Post post, boolean includeComments) { final PostModel postModel = new PostModel(post, env.get("referenceTimezone")); // let's locally cache the to and from guests to avoid useless database roundtrips Map<Long, Guest> guestCache = new HashMap<Long,Guest>(); if (post.fromGuestId!= AuthHelper.getGuestId()) { final Guest guest = getGuest(guestCache, post.fromGuestId); final GuestDetails details = guest!=null ? guestService.getGuestDetails(guest.getId()) : null; postModel.from = new BasicGuestModel(guest, details); } if (post.toGuestId!= AuthHelper.getGuestId()) { final Guest guest = getGuest(guestCache, post.toGuestId); final GuestDetails details = guest!=null ? guestService.getGuestDetails(guest.getId()) : null; postModel.to = new BasicGuestModel(guest, details); } if (includeComments) { if (post.comments!=null&&post.comments.size()>0) { postModel.comments = new ArrayList<PostCommentModel>(); for (PostComment postComment : post.comments) { PostCommentModel comment = getPostCommentModel(guestCache, postComment); postModel.comments.add(comment); } } } return postModel; } private PostCommentModel getPostCommentModel(Map<Long, Guest> guestCache, PostComment postComment) { PostCommentModel comment = new PostCommentModel(postComment, env.get("referenceTimezone")); if (postComment.fromGuestId!= AuthHelper.getGuestId()) { final Guest guest = getGuest(guestCache, postComment.fromGuestId); final GuestDetails details = guest!=null ? guestService.getGuestDetails(guest.getId()) : null; comment.from = new BasicGuestModel(guest, details); } if (postComment.toGuestId!= AuthHelper.getGuestId()) { final Guest guest = getGuest(guestCache, postComment.toGuestId); final GuestDetails details = guest!=null ? guestService.getGuestDetails(guest.getId()) : null; comment.to = new BasicGuestModel(guest, details); } return comment; } private Guest getGuest(Map<Long, Guest> guestCache, long id) { if (!guestCache.containsKey(id)) { Guest guest = guestService.getGuestById(id); if (guest==null) return null; guestCache.put(id, guest); } return guestCache.get(id); } @DELETE @Path("/{postId}") @ApiOperation(value = "Delete a post") @ApiResponses({ @ApiResponse(code = 403, message = "If the calling guest is not the original author of the post"), @ApiResponse(code = 404, message = "If there is no post with id `postId`") }) public Response deletePost(@ApiParam(value="Post ID", required=true) @PathParam("postId") final long postId) { try { Post post = postsService.getPost(postId); if (post==null) return Responses.notFound().entity("No such post: " + postId).build(); postsService.deletePost(postId); return Response.noContent().build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } @POST @Path("/{postId}/comments") @ApiOperation(value = "Add a comment on a post") @ApiResponses({ @ApiResponse(code = 403, message = "If the calling guest is neither the sender nor the receiver of the target post"), @ApiResponse(code = 404, message = "If there is no post with id `postId`") }) public Response createComment(@ApiParam(value="Post Id", required=true) @PathParam("postId") final long postId, @ApiParam(value="Comment message", required=true) @FormParam("message") final String message) throws URISyntaxException { try { Post post = postsService.getPost(postId); if (post==null) return Responses.notFound().entity("No such post: " + postId).build(); PostComment postComment = postsService.addComment(postId, message); notifyUser(post, guestService.getGuestDetails(postComment.toGuestId), "New Comment", AuthHelper.getGuest().getGuestName() + " just sent you a message"); URI createdURI = new URI(String.format("%srest/v1/posts/%s/comments/%s", env.get("homeBaseUrl"), postComment.post.getId(), postComment.getId())); return Response.created(createdURI).build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } @PUT @Path("/{postId}/comments/{commentId}") @ApiOperation(value = "Edit an existing comment") @ApiResponses({ @ApiResponse(code = 400, message = "If the comment with id `commentId` doesn't have a post with id `postId`"), @ApiResponse(code = 403, message = "If the calling guest is not the original author of the comment"), @ApiResponse(code = 404, message = "If there is no comment with id `commentId`") }) public Response updateComment(@ApiParam(value="Post Id", required=true) @PathParam("postId") final long postId, @ApiParam(value="Comment ID", required=true) @PathParam("commentId") final long commentId, @ApiParam(value="Comment message", required=true) @FormParam("message") final String message) throws URISyntaxException { try { PostComment postComment = postsService.updatePostComment(commentId, message); if (postComment==null) return Responses.notFound().entity("No such comment: " + commentId).build(); if (postComment.post.getId()!=postId) return Response.status(Response.Status.BAD_REQUEST).build(); URI createdURI = new URI(String.format("%srest/v1/posts/%s/comments/%s", env.get("homeBaseUrl"), postComment.post.getId(), postComment.getId())); return Response.created(createdURI).build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } @GET @Path("/{postId}/comments/{commentId}") @ApiOperation(value = "Read a comment", response = PostCommentModel.class) @Produces(MediaType.APPLICATION_JSON) @ApiResponses({ @ApiResponse(code = 400, message = "If the comment with id `commentId` doesn't have a post with id `postId`"), @ApiResponse(code = 403, message = "If the calling guest is neither the sender nor the receiver of the comment's post"), @ApiResponse(code = 404, message = "If there is no comment with id `commentId`") }) public Response readComment(@ApiParam(value="Post ID", required=true) @PathParam("postId") final long postId, @ApiParam(value="Comment ID", required=true) @PathParam("commentId") final long commentId) throws IOException { try { PostComment postComment = postsService.getPostComment(commentId); if (postComment==null) return Responses.notFound().entity("No such comment: " + commentId).build(); if (postComment.post.getId()!=postId) return Response.status(Response.Status.BAD_REQUEST).build(); PostCommentModel postCommentModel = getPostCommentModel(new HashMap<Long,Guest>(), postComment); return Response.ok(objectMapper.writeValueAsString(postCommentModel)).build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } @DELETE @Path("/{postId}/comments/{commentId}") @ApiOperation(value = "Delete a comment") @ApiResponses({ @ApiResponse(code = 400, message = "If the comment with id `commentId` doesn't have a post with id `postId`"), @ApiResponse(code = 403, message = "If the calling guest is not the original author of the comment"), @ApiResponse(code = 404, message = "If there is no comment with id `commentId`") }) public Response deleteComment(@ApiParam(value="Post ID", required=true) @PathParam("postId") final long postId, @ApiParam(value="Comment ID", required=true) @PathParam("commentId") final long commentId) { try { PostComment postComment = postsService.getPostComment(commentId); if (postComment==null) return Responses.notFound().entity("No such comment: " + commentId).build(); if (postComment.post.getId()!=postId) return Response.status(Response.Status.BAD_REQUEST).build(); postsService.deletePostComment(commentId); return Response.noContent().build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } @GET @Path("/all/{coacheeUsername}") @ApiOperation(value = "Retrieve a coachee's wall. Only the posts to and from the calling coach will be shown.", response = PostModel.class, responseContainer = "Array") @Produces(MediaType.APPLICATION_JSON) @ApiResponses({ @ApiResponse(code = 403, message = "If the calling guest is not a coach of the guest with username `coacheeUsername`"), @ApiResponse(code = 404, message = "If there is no coachee with that username") }) public Response getCoacheeWall(@ApiParam(value="The username of the coachee", required=true) @PathParam("coacheeUsername") final String coacheeUsername, @ApiParam(value="Include comments?") @QueryParam("includeComments") final boolean includeComments, @ApiParam(value="The id of the last post that was part of the previous query", required=false) @QueryParam(value="before") Long before, @ApiParam(value="The wanted number of posts", required=false) @QueryParam(value="count") Integer count) throws IOException { Guest coachee = guestService.getGuest(coacheeUsername); if (coachee==null) return Responses.notFound().entity("No such coachee: " + coacheeUsername).build(); try { List<Post> posts = postsService.getCoacheePosts(coachee.getId(), before, count); if (posts.size()==0) return Response.ok("[]").build(); List<PostModel> postModels = getPostModels(includeComments, posts); return Response.ok(objectMapper.writeValueAsString(postModels)).build(); } catch (UnauthorizedAccessException e) { return Response.status(Response.Status.FORBIDDEN).build(); } } @GET @Path("/all") @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Retrieve the calling guest's wall.", response = PostModel.class, responseContainer="Array") public Response getOwnWall(@ApiParam(value="Include comments?") @QueryParam("includeComments") final boolean includeComments, @ApiParam(value="The id of the last post that was part of the previous query", required=false) @QueryParam(value="before") Long before, @ApiParam(value="The wanted number of posts", required=false) @QueryParam(value="count") Integer count) throws IOException { List<Post> posts = postsService.getOwnPosts(before, count); if (posts.size()==0) return Response.ok("[]").build(); List<PostModel> postModels = getPostModels(includeComments, posts); return Response.ok(objectMapper.writeValueAsString(postModels)).build(); } private List<PostModel> getPostModels(boolean includeComments, List<Post> posts) { List<PostModel> postModels = new ArrayList<PostModel>(); for (Post post : posts) { PostModel model = getPostModel(post, includeComments); postModels.add(model); } return postModels; } }