com.github.cherimojava.orchidae.controller.PictureController.java Source code

Java tutorial

Introduction

Here is the source code for com.github.cherimojava.orchidae.controller.PictureController.java

Source

/**
 * Copyright (C) 2014 cherimojava (http://github.com/cherimojava/orchidae)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.cherimojava.orchidae.controller;

import static com.github.cherimojava.orchidae.util.FileUtil.generateId;
import static java.lang.String.format;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import javax.imageio.ImageIO;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.bson.Document;
import org.imgscalr.Scalr;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
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.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import com.github.cherimojava.data.mongo.entity.EntityFactory;
import com.github.cherimojava.orchidae.api.entities.Access;
import com.github.cherimojava.orchidae.api.entities.BatchUpload;
import com.github.cherimojava.orchidae.api.entities.Picture;
import com.github.cherimojava.orchidae.api.entities.User;
import com.github.cherimojava.orchidae.api.hook.UploadHook;
import com.github.cherimojava.orchidae.controller.api.UploadResponse;
import com.github.cherimojava.orchidae.hook.HookHandler;
import com.github.cherimojava.orchidae.util.FileUtil;
import com.github.cherimojava.orchidae.util.UserUtil;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.mongodb.client.FindIterable;

/**
 * Does the handling of uploading and serving pictures
 * 
 * @author philnate
 */
@RestController
@RequestMapping(value = "/picture")
public class PictureController {

    /**
     * URI pattern for pictures
     */
    public static final String PICTURE_URI = "/{user}/{id:[a-f0-9]+}";

    @Value("${limit.latestPictures:30}")
    int latestPictureLimit;

    @Value("${picture.small.maxHeight:300}")
    int maxHeight;

    @Autowired
    EntityFactory factory;

    @Autowired
    FileUtil fileUtil;

    @Autowired
    UserUtil userUtil;

    @Autowired
    protected HookHandler hookHandler;

    /**
     * identifier on clientside for batch
     */
    protected static final String BATCH_IDENTIFIER = "batch";

    private static final Logger LOG = LogManager.getLogger();

    /**
     * Returns a list (json) with the {number} most recent photos of the given {user}.
     *
     * @param user
     *            to retrieve pictures from
     * @param number
     *            (optional) number of pictures to ask for. Number is constrained by {@link #latestPictureLimit}
     * @param skip
     *            number of pictures to skip. Must be non negativ
     * @return picture json list with the latest pictures
     * @since 1.0.0
     */
    @RequestMapping(value = "/{user}/latest", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @PostFilter("@paa.hasAccess(filterObject.id)")
    public List<Picture> latestPicturesMetaByUserLimit(@PathVariable("user") String user,
            @RequestParam(value = "n", required = false) Integer number,
            @RequestParam(value = "s", required = false) Integer skip) {
        if (number == null || number > latestPictureLimit) {
            LOG.info("latest picture request was ({}) greater than max allowed {}. Only returning max", number,
                    latestPictureLimit);
            number = latestPictureLimit;
        }

        FindIterable<Picture> it = factory.getCollection(Picture.class)
                .find(new BsonDocument("user", new BsonString(user)), Picture.class).limit(number)
                .sort(new Document("order", -1));
        if (skip != null && skip > 0) {
            it.skip(skip);
        }
        return Lists.newArrayList(it);
    }

    /**
     * returns the total number of pictures for this user according to the permissions of the requester. E.g. the owner
     * will get private pictures included, while everyone else doesn't
     * 
     * @param user
     *            to retrieve the count of pictures from
     * @return
     * @since 1.0.0
     */
    @RequestMapping(value = "/{user}/count", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> latestPicturesMetaByUserLimit(@PathVariable("user") String user) {
        BsonDocument query = new BsonDocument("user", new BsonString(user));
        String curUser = UserUtil.getLoggedInUser();
        if (!StringUtils.equals(curUser, user)) {
            query.append("access", new BsonString(Access.PUBLIC.toString()));
        }
        return new ResponseEntity<>(format("{\"count\":%d}", factory.getCollection(Picture.class).count(query)),
                HttpStatus.OK);
    }

    /**
     * serves the requested picture {id} and size {f} for the given {user}
     * 
     * @param user
     *            user to lookup picture
     * @param id
     *            id of the picture to load
     * @param format
     *            the format/Size of the picture to return
     * @return the picture which belongs to the given id, or {@link org.springframework.http.HttpStatus#NOT_FOUND} if no
     *         such picture exists
     * @throws IOException
     * @since 1.0.0
     * @see {@link #_getPicture(String, String)}
     */
    @ResponseBody
    @RequestMapping(value = PICTURE_URI, method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE, params = "f")
    @PreAuthorize("@paa.hasAccess(#id)")
    public ResponseEntity<Resource> getPicture(@PathVariable("user") String user, @PathVariable("id") String id,
            @RequestParam(value = "f") String format) throws IOException {
        ResponseEntity resp;
        switch (format) {
        case "s":// small image
            return _getPicture(id, "_s");
        case "o":// Original
            return _getPicture(id, "");
        default:// All unknown garbage
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    /**
     * serves the requested picture {id} metadata for the given {user}
     * 
     * @param user
     *            user to lookup picture
     * @param id
     *            picture id to lookup
     * @return picture metadata
     */
    @ResponseBody
    @RequestMapping(value = PICTURE_URI, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE, params = "!f")
    @PreAuthorize("@paa.hasAccess(#id)")
    public ResponseEntity<Picture> getPictureMeta(@PathVariable("user") String user,
            @PathVariable("id") String id) {
        Picture pic = factory.load(Picture.class, id);
        return (pic != null) ? new ResponseEntity<>(pic, HttpStatus.OK)
                : new ResponseEntity<Picture>(HttpStatus.NOT_FOUND);
    }

    /**
     * deletes the given picture {id} and all related data for the given {user}
     * 
     * @param user
     *            user to lookup picture
     * @param id
     *            picture to delete
     * @return appropriate HTTP code
     */
    @ResponseBody
    @RequestMapping(value = PICTURE_URI, method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
    @PreAuthorize("@paa.canDelete(#id)")
    public ResponseEntity<String> deletePicture(@PathVariable("user") String user, @PathVariable("id") String id) {
        Picture pic = factory.load(Picture.class, id);
        if (pic != null) {
            File f = fileUtil.getFileHandle(pic.getId());
            f.delete();
            new File(f.getAbsolutePath() + "_s").delete();
            pic.drop();
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

    /**
     * actual method retrieving the picture from disk. Requested picture is only returned if the current user is allowed
     * to view it
     * 
     * @param id
     *            identification of picture
     * @param type
     *            type of the picture eg _t for thumbnail etc.
     * @return ResponseEntity containing the resource to the picture or NOT_FOUND
     * @throws IOException
     */
    private ResponseEntity<Resource> _getPicture(String id, String type) throws IOException {
        File picture = fileUtil.getFileHandle(id + type);
        if (picture.exists()) {
            return new ResponseEntity<Resource>(new InputStreamResource(FileUtils.openInputStream(picture)),
                    HttpStatus.OK);
        } else {
            LOG.debug("Could not find picture with id {}", id);
            // picture doesn't exist so return 404
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    /**
     * uploads multiple files into the system for the current user
     * 
     * @param request
     *            request with pictures to store
     * @return {@link org.springframework.http.HttpStatus#CREATED} if the upload was successful or
     *         {@link org.springframework.http.HttpStatus#OK} if some of the pictures couldn't be uploaded together with
     *         information which pictures couldn't be uploaded
     * @since 1.0.0
     */
    @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<UploadResponse> handleFileUpload(MultipartHttpServletRequest request,
            @RequestParam(value = "batch", required = false) String batchId) {
        List<String> badFiles = Lists.newArrayList();
        UploadResponse response = factory.create(UploadResponse.class);
        User user = userUtil
                .getUser((String) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        for (Iterator<String> it = request.getFileNames(); it.hasNext();) {
            MultipartFile file = request.getFile(it.next());

            String type = StringUtils.substringAfterLast(file.getOriginalFilename(), ".");

            try {
                // Create uuid and Picture entity
                Picture picture = EntityFactory.instantiate(Picture.class);
                picture.setId(generateId());

                // save picture
                File storedPicture = fileUtil.getFileHandle(picture.getId());
                file.transferTo(storedPicture);

                BufferedImage image = ImageIO.read(FileUtils.openInputStream(storedPicture));

                // Call all hooks
                UploadHook.UploadInfo ui = new UploadHook.UploadInfo();
                ui.pictureUploaded = picture;
                ui.uploadedFile = file;
                ui.uploadingUser = user;
                ui.storedImage = image;

                hookHandler.callHook(UploadHook.class).callAll().upload(ui);

                // todo, would be good if this could be moved into hook as well
                createSmall(picture.getId(), image, type);
                checkBatch(picture, batchId);
                // save picture
                factory.save(picture);
                LOG.info("Uploaded {} and assigned id {}", file.getOriginalFilename(), picture.getId());
                // after the picture is saved we can add it to the response
                response.addIds(picture.getId());
            } catch (Exception e) {
                LOG.warn("failed to store picture", e);
                badFiles.add(file.getOriginalFilename());
            }
        }
        user.save();// We should persist this information? Or should we rely on the persistence magic?
        if (badFiles.isEmpty()) {
            return new ResponseEntity<>(response, HttpStatus.CREATED);
        } else {
            response.setIds(Lists.<String>newArrayList());
            return new ResponseEntity<>(
                    response.setMsg(
                            "Could not upload all files. Failed to upload: " + Joiner.on(",").join(badFiles)),
                    HttpStatus.OK);
        }
    }

    /**
     * check if batching should be applied to the current picture upload
     * 
     * @param pic
     * @param batchId
     */
    private void checkBatch(Picture pic, String batchId) {
        if (StringUtils.isNotEmpty(batchId)) {
            if (!FileUtil.validateId(batchId)) {
                // ignore the batching if the id isn't valid
                return;
            }
            BatchUpload batch = factory.load(BatchUpload.class, batchId);
            // if the batch doesn't exist, create it
            if (batch == null) {
                batch = factory.create(BatchUpload.class);
                batch.setUploadDate(DateTime.now()).setId(batchId);
            }
            batch.addPictures(pic);
            pic.setBatchUpload(batch);
            batch.save();
        }
    }

    /**
     * creates the Thumbnail for the given picture and stores it on the disk
     * 
     * @param id
     * @param image
     * @param type
     */
    private void createSmall(String id, BufferedImage image, String type) {
        int height = image.getHeight();
        int width = image.getWidth();
        double scale = maxHeight / (double) height;
        BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.ULTRA_QUALITY,
                ((Double) (width * scale)).intValue(), ((Double) (height * scale)).intValue(), Scalr.OP_ANTIALIAS);
        try {
            ImageIO.write(thumbnail, type, fileUtil.getFileHandle(id + "_s"));
        } catch (IOException e) {
            LOG.error("failed to create thumbnail for picture", e);
        }
    }
}