diff --git a/README.md b/README.md index 2cb0898..362a15d 100644 --- a/README.md +++ b/README.md @@ -611,6 +611,7 @@ * **Error response:** will be elaborated at a later time #### Image removal +* **Note:** Image name parameter is not needed when removing user's profile picture. * **URL:** `/image/:resource/:id/:imageName` * **Method:** `DELETE` * **URL paramteres:** `resource = ['user','wish','story']`, `id = String`, `imageName = String` diff --git a/src/main/java/hr/asc/appic/controller/ImageController.java b/src/main/java/hr/asc/appic/controller/ImageController.java index 656de18..4fdf041 100644 --- a/src/main/java/hr/asc/appic/controller/ImageController.java +++ b/src/main/java/hr/asc/appic/controller/ImageController.java @@ -1,19 +1,14 @@ package hr.asc.appic.controller; +import hr.asc.appic.controller.model.ImagePathModel; +import hr.asc.appic.service.image.ImageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.multipart.MultipartFile; -import hr.asc.appic.controller.model.ImagePathModel; -import hr.asc.appic.service.image.ImageService; - @RestController @RequestMapping("/image") public class ImageController { @@ -45,13 +40,12 @@ public DeferredResult> setUserPhoto( } @RequestMapping( - value = "/user/{id}/{imageName}", + value = "/user/{id}", method = RequestMethod.DELETE ) public DeferredResult deleteUserPhoto( - @PathVariable("id") String id, - @PathVariable("imageName") String imageName) { - return imageService.deleteUserPhoto(id, imageName); + @PathVariable("id") String id) { + return imageService.deleteUserPhoto(id); } // ==================== WISH ==================== // diff --git a/src/main/java/hr/asc/appic/controller/model/ImagePathModel.java b/src/main/java/hr/asc/appic/controller/model/ImagePathModel.java index 6c9d2a6..94d5e77 100644 --- a/src/main/java/hr/asc/appic/controller/model/ImagePathModel.java +++ b/src/main/java/hr/asc/appic/controller/model/ImagePathModel.java @@ -1,14 +1,14 @@ package hr.asc.appic.controller.model; -import java.util.Collections; -import java.util.List; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import java.util.Collections; +import java.util.List; + @Getter @Setter @AllArgsConstructor @@ -21,6 +21,11 @@ public class ImagePathModel { public ImagePathModel(String id, String path) { this.id = id; - this.paths = Collections.singletonList(path); + + if (path == null) { + paths = Collections.emptyList(); + } else { + paths = Collections.singletonList(path); + } } } diff --git a/src/main/java/hr/asc/appic/service/image/AmazonS3ImageService.java b/src/main/java/hr/asc/appic/service/image/AmazonS3ImageService.java index 8690903..135c1cf 100644 --- a/src/main/java/hr/asc/appic/service/image/AmazonS3ImageService.java +++ b/src/main/java/hr/asc/appic/service/image/AmazonS3ImageService.java @@ -6,17 +6,25 @@ import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import hr.asc.appic.controller.model.ImagePathModel; -import hr.asc.appic.exception.ContentCheck; import hr.asc.appic.exception.ImageUploadException; +import hr.asc.appic.persistence.model.Story; +import hr.asc.appic.persistence.model.User; +import hr.asc.appic.persistence.model.Wish; import hr.asc.appic.persistence.repository.StoryRepository; import hr.asc.appic.persistence.repository.UserRepository; import hr.asc.appic.persistence.repository.WishRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.multipart.MultipartFile; @@ -29,10 +37,8 @@ @Service public class AmazonS3ImageService implements ImageService { - @Value("${aws-bucket-image}") - private String bucket; @Autowired - private AmazonS3 client; + private ListeningExecutorService listeningExecutorService; @Autowired private UserRepository userRepository; @@ -41,23 +47,28 @@ public class AmazonS3ImageService implements ImageService { @Autowired private StoryRepository storyRepository; + @Value("${aws-bucket-image}") + private String bucket; + @Autowired + private AmazonS3 client; @Autowired private ImagePaths imagePaths; + // ======================================== User ======================================== // + @Override public DeferredResult> getUserPhoto(String id) { DeferredResult> result = new DeferredResult<>(); - userRepository.findById(id).addCallback( - u -> { - ContentCheck.requireNonNull(id, u); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, u.getProfilePicture()))); - }, - e -> { - // TODO + ListenableFuture getUserPhotoJob = listeningExecutorService.submit( + () -> { + User user = userRepository.findById(id).get(); + Assert.notNull(user, "Could not find user with id: " + id); + return new ImagePathModel(id, user.getProfilePicture()); } ); + submitImageJob(getUserPhotoJob, result); return result; } @@ -65,59 +76,77 @@ public DeferredResult> getUserPhoto(String id) { public DeferredResult> setUserPhoto(String id, MultipartFile image) { DeferredResult> result = new DeferredResult<>(); - userRepository.findById(id).addCallback( - u -> { - ContentCheck.requireNonNull(id, u); - String imagePath = imagePaths.uploadUrl(u); + ListenableFuture setUserPhotoJob = listeningExecutorService.submit( + () -> { + User user = userRepository.findById(id).get(); + Assert.notNull(user, "Could not find user with id: " + id); + + if (user.getProfilePicture() != null) { + deleteImage(imagePaths.deleteUrl(user)); + } + + String imagePath = imagePaths.uploadUrl(user); uploadImage(imagePath, image); + String fullImagePath = imagePaths.accessUrl(imagePath); - u.setProfilePicture(fullImagePath); - userRepository.save(u); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, u.getProfilePicture()))); - }, - e -> { - // TODO + user.setProfilePicture(fullImagePath); + userRepository.save(user); + + return new ImagePathModel(id, user.getProfilePicture()); } ); + submitImageJob(setUserPhotoJob, result); return result; } @Override - public DeferredResult deleteUserPhoto(String id, String imagePath) { + public DeferredResult deleteUserPhoto(String id) { DeferredResult result = new DeferredResult<>(); - userRepository.findById(id).addCallback( - u -> { - ContentCheck.requireNonNull(id, u); - String deleteUrl = imagePaths.deleteUrl(u); - deleteImage(deleteUrl); - u.setProfilePicture(null); - userRepository.save(u); - result.setResult(ResponseEntity.ok().build()); - }, - e -> { - // TODO + ListenableFuture deleteUserPhotoJob = listeningExecutorService.submit( + () -> { + User user = userRepository.findById(id).get(); + Assert.notNull(user, "Could not find user with id: " + id); + deleteImage(imagePaths.deleteUrl(user)); + user.setProfilePicture(null); + userRepository.save(user); + return null; } ); + Futures.addCallback(deleteUserPhotoJob, new FutureCallback() { + + @Override + public void onSuccess(Void voidable) { + result.setResult(ResponseEntity.ok().build()); + } + + @Override + public void onFailure(Throwable throwable) { + result.setResult(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build()); + log.error("Error occurred during image manipulation", throwable); + } + }); + return result; } + // ======================================== Wish ======================================== // + @Override public DeferredResult> getWishPhotos(String id) { DeferredResult> result = new DeferredResult<>(); - wishRepository.findById(id).addCallback( - w -> { - ContentCheck.requireNonNull(id, w); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, w.getPictures()))); - }, - e -> { - // TODO + ListenableFuture getWishPhotosJob = listeningExecutorService.submit( + () -> { + Wish wish = wishRepository.findById(id).get(); + Assert.notNull(wish, "Could not find wish with id: " + id); + return new ImagePathModel(id, wish.getPictures()); } ); + submitImageJob(getWishPhotosJob, result); return result; } @@ -125,65 +154,65 @@ public DeferredResult> getWishPhotos(String id) { public DeferredResult> addWishPhoto(String id, MultipartFile image) { DeferredResult> result = new DeferredResult<>(); - wishRepository.findById(id).addCallback( - w -> { - ContentCheck.requireNonNull(id, w); - String imagePath = imagePaths.uploadUrl(w); + ListenableFuture addWishPhotoJob = listeningExecutorService.submit( + () -> { + Wish wish = wishRepository.findById(id).get(); + Assert.notNull(wish, "Could not find wish with id: " + id); + + String imagePath = imagePaths.uploadUrl(wish); uploadImage(imagePath, image); + String fullImagePath = imagePaths.accessUrl(imagePath); - w.getPictures().add(fullImagePath); - wishRepository.save(w); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, w.getPictures()))); - }, - e -> { - // TODO + wish.getPictures().add(fullImagePath); + wishRepository.save(wish); + + return new ImagePathModel(id, wish.getPictures()); } ); + submitImageJob(addWishPhotoJob, result); return result; } @Override - public DeferredResult> deleteWishPhoto(String id, String imagePath) { + public DeferredResult> deleteWishPhoto(String id, String imageName) { DeferredResult> result = new DeferredResult<>(); - wishRepository.findById(id).addCallback( - w -> { - ContentCheck.requireNonNull(id, w); - String deleteUrl = imagePaths.deleteUrl(w, imagePath); - - if (w.getPictures().contains(imagePath)) { - w.getPictures().remove(imagePath); - deleteImage(deleteUrl); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, w.getPictures()))); - } else { - result.setResult(ResponseEntity.badRequest().build()); - } + ListenableFuture deleteWishPhotoJob = listeningExecutorService.submit( + () -> { + Wish wish = wishRepository.findById(id).get(); + Assert.notNull(wish, "Could not find wish with id: " + id); + + String accessUrl = imagePaths.accessUrl(wish, imageName); + String deleteUrl = imagePaths.deleteUrl(wish, imageName); + + wish.getPictures().remove(accessUrl); + deleteImage(deleteUrl); - wishRepository.save(w); - }, - e -> { - // TODO + wishRepository.save(wish); + return new ImagePathModel(id, wish.getPictures()); } ); + submitImageJob(deleteWishPhotoJob, result); return result; } + // ======================================== Story ======================================== // + @Override public DeferredResult> getStoryPhotos(String id) { DeferredResult> result = new DeferredResult<>(); - storyRepository.findById(id).addCallback( - s -> { - ContentCheck.requireNonNull(id, s); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, s.getPictures()))); - }, - e -> { - // TODO + ListenableFuture getStoryPhotoJob = listeningExecutorService.submit( + () -> { + Story story = storyRepository.findById(id).get(); + Assert.notNull(story, "Could not find story with id: " + id); + return new ImagePathModel(id, story.getPictures()); } ); + submitImageJob(getStoryPhotoJob, result); return result; } @@ -191,51 +220,67 @@ public DeferredResult> getStoryPhotos(String id) public DeferredResult> addStoryPhoto(String id, MultipartFile image) { DeferredResult> result = new DeferredResult<>(); - storyRepository.findById(id).addCallback( - s -> { - ContentCheck.requireNonNull(id, s); - String imagePath = imagePaths.uploadUrl(s); + ListenableFuture addStoryPhotoJob = listeningExecutorService.submit( + () -> { + Story story = storyRepository.findById(id).get(); + Assert.notNull(story, "Could not find story with id: " + id); + + String imagePath = imagePaths.uploadUrl(story); uploadImage(imagePath, image); + String fullImagePath = imagePaths.accessUrl(imagePath); - s.getPictures().add(fullImagePath); - storyRepository.save(s); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, s.getPictures()))); - }, - e -> { - // TODO + story.getPictures().add(fullImagePath); + storyRepository.save(story); + + return new ImagePathModel(id, story.getPictures()); } ); + submitImageJob(addStoryPhotoJob, result); return result; } @Override - public DeferredResult> deleteStoryPhoto(String id, String imagePath) { + public DeferredResult> deleteStoryPhoto(String id, String imageName) { DeferredResult> result = new DeferredResult<>(); - storyRepository.findById(id).addCallback( - s -> { - ContentCheck.requireNonNull(id, s); - String deleteUrl = imagePaths.deleteUrl(s, imagePath); - - if (s.getPictures().contains(imagePath)) { - s.getPictures().remove(imagePath); - deleteImage(deleteUrl); - result.setResult(ResponseEntity.ok(new ImagePathModel(id, s.getPictures()))); - } else { - result.setResult(ResponseEntity.badRequest().build()); - } + ListenableFuture deleteStoryPhotoJob = listeningExecutorService.submit( + () -> { + Story story = storyRepository.findById(id).get(); + Assert.notNull(story, "Could not find story with id: " + id); + + String accessUrl = imagePaths.accessUrl(story, imageName); + String deleteUrl = imagePaths.deleteUrl(story, imageName); - storyRepository.save(s); - }, - e -> { - // TODO + story.getPictures().remove(accessUrl); + deleteImage(deleteUrl); + + storyRepository.save(story); + return new ImagePathModel(id, story.getPictures()); } ); + submitImageJob(deleteStoryPhotoJob, result); return result; } + private void submitImageJob(ListenableFuture job, + DeferredResult> result) { + Futures.addCallback(job, new FutureCallback() { + + @Override + public void onSuccess(ImagePathModel model) { + result.setResult(ResponseEntity.ok(model)); + } + + @Override + public void onFailure(Throwable throwable) { + result.setResult(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build()); + log.error("Error occurred during image manipulation", throwable); + } + }); + } + private void uploadImage(String imagePath, MultipartFile image) { File imageFile = null; diff --git a/src/main/java/hr/asc/appic/service/image/ImagePaths.java b/src/main/java/hr/asc/appic/service/image/ImagePaths.java index 81d8a0f..9ff0c96 100644 --- a/src/main/java/hr/asc/appic/service/image/ImagePaths.java +++ b/src/main/java/hr/asc/appic/service/image/ImagePaths.java @@ -29,11 +29,12 @@ public ImagePaths(@Value("${aws-region}") String region, accessRoot = region + "." + url + "/" + bucket + "/"; } - public String accessUrl(String resourceUrl) { return accessRoot + resourceUrl; } + // ======================================== User ======================================== // + public String uploadUrl(User u) { return userDir + "/" + u.getId() + "_" + new Date().getTime(); } @@ -42,28 +43,42 @@ public String deleteUrl(User u) { if (u.getProfilePicture() != null) { return u.getProfilePicture().substring(accessRoot.length()); } - throw new NullPointerException("Image for user is not present"); + throw new IllegalArgumentException("Image for user is not present"); } + // ======================================== Wish ======================================== // + public String uploadUrl(Wish wish) { return wishDir + "/" + wish.getId() + "_" + new Date().getTime(); } - public String deleteUrl(Wish wish, String imagePath) { - if (wish.getPictures().contains(imagePath)) { - return imagePath.substring(accessRoot.length()); + public String accessUrl(Wish wish, String imageName) { + String fullImagePath = accessRoot + wishDir + "/" + imageName; + if (wish.getPictures().contains(fullImagePath)) { + return fullImagePath; } - throw new NullPointerException("Image for wish is not present"); + throw new IllegalArgumentException("Image for wish is not present: " + imageName); + } + + public String deleteUrl(Wish wish, String imageName) { + return accessUrl(wish, imageName).substring(accessRoot.length()); } + // ======================================== Story ======================================== // + public String uploadUrl(Story story) { return storyDir + "/" + story.getId() + "_" + new Date().getTime(); } - public String deleteUrl(Story story, String imagePath) { - if (story.getPictures().contains(imagePath)) { - return imagePath.substring(accessRoot.length()); + public String accessUrl(Story story, String imageName) { + String fullImagePath = accessRoot + storyDir + "/" + imageName; + if (story.getPictures().contains(fullImagePath)) { + return fullImagePath; } - throw new NullPointerException("Image for story is not present"); + throw new IllegalArgumentException("Image for story is not present: " + imageName); + } + + public String deleteUrl(Story story, String imageName) { + return accessUrl(story, imageName).substring(accessRoot.length()); } } diff --git a/src/main/java/hr/asc/appic/service/image/ImageService.java b/src/main/java/hr/asc/appic/service/image/ImageService.java index fd873e8..c8033f4 100644 --- a/src/main/java/hr/asc/appic/service/image/ImageService.java +++ b/src/main/java/hr/asc/appic/service/image/ImageService.java @@ -12,7 +12,7 @@ public interface ImageService { DeferredResult> setUserPhoto(String id, MultipartFile image); - DeferredResult deleteUserPhoto(String id, String imageName); + DeferredResult deleteUserPhoto(String id); DeferredResult> getWishPhotos(String id);