diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadController.java new file mode 100644 index 000000000..e24068c2e --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadController.java @@ -0,0 +1,516 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.freemarker; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.ServletException; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileServingHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; +import edu.cornell.mannlib.vitro.webapp.filestorage.uploadrequest.FileUploadServletRequest; +import freemarker.template.Configuration; + +/** + * Handle adding, replacing or deleting the main image on an Individual. + */ +public class ImageUploadController extends FreeMarkerHttpServlet { + private static final long serialVersionUID = 1L; + private static final Log log = LogFactory + .getLog(ImageUploadController.class); + + private static final String DEFAULT_NAMESPACE = ConfigurationProperties + .getProperty("Vitro.defaultNamespace"); + + public static final String DUMMY_THUMBNAIL_URL = "/images/dummyImages/person.thumbnail.jpg"; + + /** Limit file size to 50 megabytes. */ + public static final int MAXIMUM_FILE_SIZE = 50 * 1024 * 1024; + + /** Generated thumbnails will be this big. */ + public static final int THUMBNAIL_HEIGHT = 115; + public static final int THUMBNAIL_WIDTH = 115; + + public static final String PARAMETER_ACTION = "action"; + public static final String PARAMETER_ENTITY_URI = "entityUri"; + public static final String PARAMETER_UPLOADED_FILE = "datafile"; + + public static final String ACTION_SAVE = "save"; + public static final String ACTION_UPLOAD = "upload"; + public static final String ACTION_DELETE = "delete"; + + public static final String BODY_TITLE = "title"; + public static final String BODY_ENTITY_NAME = "entityName"; + public static final String BODY_MAIN_IMAGE_URL = "imageUrl"; + public static final String BODY_THUMBNAIL_URL = "thumbnailUrl"; + public static final String BODY_CANCEL_URL = "cancelUrl"; + public static final String BODY_DELETE_URL = "deleteUrl"; + public static final String BODY_FORM_ACTION = "formAction"; + public static final String BODY_ERROR_MESSAGE = "errorMessage"; + + public static final String TEMPLATE_NEW = "imageUpload/newImage.ftl"; + public static final String TEMPLATE_REPLACE = "imageUpload/replaceImage.ftl"; + public static final String TEMPLATE_CROP = "imageUpload/cropImage.ftl"; + public static final String TEMPLATE_BOGUS = "imageUpload/bogus.ftl"; // TODO + // This + // is + // BOGUS!! + + private static final String URL_HERE = UrlBuilder.getUrl("/uploadImages"); + + private FileStorage fileStorage; + + /** + * When initialized, get a reference to the File Storage system. Without + * that, we can do nothing. + */ + @Override + public void init() throws ServletException { + super.init(); + Object o = getServletContext().getAttribute( + FileStorageSetup.ATTRIBUTE_NAME); + if (o instanceof FileStorage) { + fileStorage = (FileStorage) o; + } else if (o == null) { + throw new UnavailableException(this.getClass().getSimpleName() + + " could not initialize. Attribute '" + + FileStorageSetup.ATTRIBUTE_NAME + + "' was not set in the servlet context."); + } else { + throw new UnavailableException(this.getClass().getSimpleName() + + " could not initialize. Attribute '" + + FileStorageSetup.ATTRIBUTE_NAME + + "' in the servlet context contained an instance of '" + + o.getClass().getName() + "' instead of '" + + FileStorage.class.getName() + "'"); + } + } + + /** + *

+ * Parse the multi-part request before letting the + * {@link FreeMarkerHttpServlet} do its tricks. + *

+ *

+ * If the request was a multi-part file upload, it will parse to a + * normal-looking request with a "file_item_map" attribute. + *

+ */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + try { + FileUploadServletRequest parsedRequest = FileUploadServletRequest + .parseRequest(request, MAXIMUM_FILE_SIZE); + if (log.isTraceEnabled()) { + dumpRequestDetails(parsedRequest); + } + + super.doGet(parsedRequest, response); + + } catch (FileUploadException e) { + // Swallow throw an exception here. Test for FILE_ITEM_MAP later. + log.error("Failed to parse the multi-part HTTP request", e); + } + } + + protected String getTitle(String siteName) { + return "Photo Upload " + siteName; + } + + /** + * Handle the different possible actions - default action is to show the + * intro screen. + */ + protected String getBody(VitroRequest vreq, Map body, + Configuration config) { + String action = vreq.getParameter(PARAMETER_ACTION); + try { + Individual entity = validateEntityUri(vreq); + + if (ACTION_UPLOAD.equals(action)) { + return doUploadImage(vreq, body, config, entity); + } else if (ACTION_SAVE.equals(action)) { + return doCreateThumbnail(vreq, body, config, entity); + } else if (ACTION_DELETE.equals(action)) { + return doDeleteImage(body, config, entity); + } else { + return doIntroScreen(body, config, entity); + } + } catch (UserMistakeException e) { + return showAddImagePageWithError(body, config, null, e.getMessage()); + } catch (Exception e) { + // We weren't expecting this - dump as much info as possible. + log.error(e, e); + return doError(e.toString(), body, config); + } + } + + /** + * Show the first screen in the upload process: Add or Replace. + */ + private String doIntroScreen(Map body, + Configuration config, Individual entity) { + + String thumbUrl = getThumbnailUrl(entity); + + if (thumbUrl == null) { + return showAddImagePage(body, config, entity); + } else { + return showReplaceImagePage(body, config, entity, thumbUrl); + } + } + + /** + * The user has selected their main image file. Remove any previous main + * image (and thumbnail), and attach the new main image. + */ + private String doUploadImage(VitroRequest vreq, Map body, + Configuration config, Individual entity) { + ImageUploadHelper helper = new ImageUploadHelper(fileStorage, + getWebappDaoFactory()); + + // Did they provide a file to upload? If not, show an error. + FileItem fileItem; + try { + fileItem = helper.validateImageFromRequest(vreq); + } catch (UserMistakeException e) { + String thumbUrl = getThumbnailUrl(entity); + String message = e.getMessage(); + if (thumbUrl == null) { + return showAddImagePageWithError(body, config, entity, message); + } else { + return showReplaceImagePageWithError(body, config, entity, + thumbUrl, message); + } + } + + // Remove the old main image (if any) and store the new one. + helper.removeExistingImage(entity); + helper.storeMainImageFile(entity, fileItem); + + // The entity Individual is stale - get another one; + String entityUri = entity.getURI(); + entity = getWebappDaoFactory().getIndividualDao().getIndividualByURI( + entityUri); + + // Go to the cropping page. + return showCropImagePage(body, config, entity, getMainImageUrl(entity)); + } + + /** + * The user has specified how to crop the thumbnail. Crop it and attach it + * to the main image. + */ + private String doCreateThumbnail(VitroRequest vreq, + Map body, Configuration config, Individual entity) { + ImageUploadHelper helper = new ImageUploadHelper(fileStorage, + getWebappDaoFactory()); + + validateMainImage(entity); + CropRectangle crop = validateCropCoordinates(vreq); + + helper.removeExistingThumbnail(entity); + helper.generateThumbnailAndStore(entity, crop); + + return showIndividualDisplayPage(body, config, entity); + } + + /** + * Delete the main image and the thumbnail from the individual. + */ + private String doDeleteImage(Map body, + Configuration config, Individual entity) { + ImageUploadHelper helper = new ImageUploadHelper(fileStorage, + getWebappDaoFactory()); + + helper.removeExistingImage(entity); + + return showIndividualDisplayPage(body, config, entity); + } + + /** + * Display a error message to the user. + * + * @message The text of the error message. + */ + private String doError(String message, Map body, + Configuration config) { + String bodyTemplate = "errorMessage.ftl"; + body.put("errorMessage", message); + return mergeBodyToTemplate(bodyTemplate, body, config); + } + + /** + * We need to be talking about an actual Individual here. + */ + private Individual validateEntityUri(VitroRequest vreq) + throws UserMistakeException { + String entityUri = vreq.getParameter(PARAMETER_ENTITY_URI); + if (entityUri == null) { + throw new UserMistakeException("No entity URI was provided"); + } + + Individual entity = getWebappDaoFactory().getIndividualDao() + .getIndividualByURI(entityUri); + if (entity == null) { + throw new UserMistakeException( + "This URI is not recognized as belonging to anyone: '" + + entityUri + "'"); + } + return entity; + } + + /** + * We can't do a thumbnail if there is no main image. + */ + private void validateMainImage(Individual entity) { + if (entity.getMainImageUri() == null) { + throw new IllegalStateException("Can't store a thumbnail " + + "on an individual with no main image: '" + + showEntity(entity) + "'"); + } + } + + /** + * Did we get the cropping coordinates? + */ + private CropRectangle validateCropCoordinates(VitroRequest vreq) { + int x = getRequiredIntegerParameter(vreq, "x"); + int y = getRequiredIntegerParameter(vreq, "y"); + int h = getRequiredIntegerParameter(vreq, "h"); + int w = getRequiredIntegerParameter(vreq, "w"); + return new CropRectangle(x, y, h, w); + } + + /** + * We need this parameter on the request, and it must be a valid integer. + */ + private int getRequiredIntegerParameter(HttpServletRequest req, String key) { + String string = req.getParameter(key); + if (string == null) { + throw new IllegalStateException( + "Request did not contain a value for '" + key + "'"); + } + try { + return Integer.parseInt(string); + } catch (NumberFormatException e) { + throw new IllegalStateException("Value for '" + key + + "' was not a valid integer: '" + string + "'"); + } + } + + /** + * Get the URL that will serve this entity's main image, or null. + */ + private String getMainImageUrl(Individual entity) { + String imageUri = FileModelHelper.getMainImageBytestreamUri(entity); + String imageFilename = FileModelHelper.getMainImageFilename(entity); + return FileServingHelper.getBytestreamAliasUrl(imageUri, imageFilename); + } + + /** + * Get the URL that will serve this entity's thumbnail image, or null. + */ + private String getThumbnailUrl(Individual entity) { + String thumbUri = FileModelHelper.getThumbnailBytestreamUri(entity); + String thumbFilename = FileModelHelper.getThumbnailFilename(entity); + return FileServingHelper.getBytestreamAliasUrl(thumbUri, thumbFilename); + } + + /** + * The individual has no image - go to the Add Image page. + * + * @param entity + * if this is null, then all URLs lead to the welcome page. + */ + private String showAddImagePage(Map body, + Configuration config, Individual entity) { + String formAction = (entity == null) ? "/" : formAction( + entity.getURI(), ACTION_UPLOAD); + String cancelUrl = (entity == null) ? "/" : displayPageUrl(entity + .getURI()); + + body.put(BODY_THUMBNAIL_URL, UrlBuilder.getUrl(DUMMY_THUMBNAIL_URL)); + body.put(BODY_FORM_ACTION, formAction); + body.put(BODY_CANCEL_URL, cancelUrl); + body.put(BODY_TITLE, "Upload image" + forName(entity)); + return mergeBodyToTemplate(TEMPLATE_NEW, body, config); + } + + /** + * The individual has no image, but the user did something wrong. + */ + private String showAddImagePageWithError(Map body, + Configuration config, Individual entity, String message) { + body.put(BODY_ERROR_MESSAGE, message); + return showAddImagePage(body, config, entity); + } + + /** + * The individual has an image - go to the Replace Image page. + */ + private String showReplaceImagePage(Map body, + Configuration config, Individual entity, String thumbUrl) { + body.put(BODY_THUMBNAIL_URL, UrlBuilder.getUrl(thumbUrl)); + body.put(BODY_DELETE_URL, formAction(entity.getURI(), ACTION_DELETE)); + body.put(BODY_FORM_ACTION, formAction(entity.getURI(), ACTION_UPLOAD)); + body.put(BODY_CANCEL_URL, displayPageUrl(entity.getURI())); + body.put(BODY_TITLE, "Replace image" + forName(entity)); + return mergeBodyToTemplate(TEMPLATE_REPLACE, body, config); + } + + /** + * The individual has an image, but the user did something wrong. + */ + private String showReplaceImagePageWithError(Map body, + Configuration config, Individual entity, String thumbUrl, + String message) { + body.put(BODY_ERROR_MESSAGE, message); + return showReplaceImagePage(body, config, entity, thumbUrl); + } + + /** + * We got their main image - go to the Crop Image page. + */ + private String showCropImagePage(Map body, + Configuration config, Individual entity, String imageUrl) { + body.put(BODY_MAIN_IMAGE_URL, UrlBuilder.getUrl(imageUrl)); + body.put(BODY_FORM_ACTION, formAction(entity.getURI(), ACTION_SAVE)); + body.put(BODY_CANCEL_URL, displayPageUrl(entity.getURI())); + body.put(BODY_TITLE, "Crop Photo" + forName(entity)); + return mergeBodyToTemplate(TEMPLATE_CROP, body, config); + } + + /** + * All done - go to the individual display page. + */ + private String showIndividualDisplayPage(Map body, + Configuration config, Individual entity) { + return mergeBodyToTemplate(TEMPLATE_BOGUS, body, config); + } + + /** + * When we complete the process, by success or by cancellation, we go to the + * individual display page. + */ + private String displayPageUrl(String entityUri) { + if (DEFAULT_NAMESPACE == null) { + return UrlBuilder.getUrl(""); + } else if (!entityUri.startsWith(DEFAULT_NAMESPACE)) { + return UrlBuilder.getUrl(""); + } else { + String tail = entityUri.substring(DEFAULT_NAMESPACE.length()); + if (!tail.startsWith("/")) { + tail = "/" + tail; + } + return UrlBuilder.getUrl("display" + tail); + } + } + + /** + * The "action" parameter on the HTML "form" tag should include the path + * back to this controller, along with the desired action and the Entity + * URI. + */ + private String formAction(String entityUri, String action) { + UrlBuilder.Params params = new UrlBuilder.Params(PARAMETER_ENTITY_URI, + entityUri, PARAMETER_ACTION, action); + return UrlBuilder.getPath(URL_HERE, params); + } + + /** + * Format an entity for display in a message. + */ + private String showEntity(Individual entity) { + if (entity == null) { + return String.valueOf(null); + } else if (entity.getName() == null) { + return "'no name' (" + entity.getURI() + ")"; + } else { + return "'" + entity.getName() + "' (" + entity.getURI() + ")"; + } + } + + /** + * Format the entity's name for display as part of the page title. + */ + private String forName(Individual entity) { + if (entity != null) { + String name = entity.getName(); + if (name != null) { + return " for " + name; + } + } + return ""; + } + + /** + * Holds an error message to use as a complaint to the user. + */ + static class UserMistakeException extends Exception { + UserMistakeException(String message) { + super(message); + } + } + + /** + * Holds the coordinates that we use to crop the main image. + */ + static class CropRectangle { + final int x; + final int y; + final int height; + final int width; + + private CropRectangle(int x, int y, int height, int width) { + this.x = x; + this.y = y; + this.height = height; + this.width = width; + } + + } + + /** + * For debugging, dump all sorts of information about the request. + * + * WARNING: if this request represents a Multi-part request which has not + * yet been parsed, just reading these parameters will consume them. + */ + @SuppressWarnings("unchecked") + private void dumpRequestDetails(HttpServletRequest req) { + log.trace("Request is " + req.getClass().getName()); + + Map parms = req.getParameterMap(); + for (Entry entry : parms.entrySet()) { + log.trace("Parameter '" + entry.getKey() + "'=" + + Arrays.deepToString(entry.getValue())); + } + + Enumeration attrs = req.getAttributeNames(); + while (attrs.hasMoreElements()) { + String key = attrs.nextElement(); + String valueString = String.valueOf(req.getAttribute(key)); + String valueOneLine = valueString.replace("\n", " | "); + log.trace("Attribute '" + key + "'=" + valueOneLine); + } + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadHelper.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadHelper.java new file mode 100644 index 000000000..904c3bc68 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadHelper.java @@ -0,0 +1,357 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.freemarker; + +import static edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadController.PARAMETER_UPLOADED_FILE; +import static edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadController.THUMBNAIL_HEIGHT; +import static edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadController.THUMBNAIL_WIDTH; + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadController.UserMistakeException; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileAlreadyExistsException; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; +import edu.cornell.mannlib.vitro.webapp.filestorage.uploadrequest.FileUploadServletRequest; + +/** + * Handle the mechanics of validating, storing, and deleting file images. + */ +public class ImageUploadHelper { + private static final Log log = LogFactory.getLog(ImageUploadHelper.class); + + /** Recognized file extensions mapped to MIME-types. */ + private static final Map RECOGNIZED_FILE_TYPES = createFileTypesMap(); + + private static Map createFileTypesMap() { + Map map = new HashMap(); + map.put(".gif", "image/gif"); + map.put(".png", "image/png"); + map.put(".jpg", "image/jpeg"); + map.put(".jpeg", "image/jpeg"); + map.put(".jpe", "image/jpeg"); + return Collections.unmodifiableMap(map); + } + + private final FileModelHelper fileModelHelper; + private final FileStorage fileStorage; + + ImageUploadHelper(FileStorage fileStorage, WebappDaoFactory webAppDaoFactory) { + this.fileModelHelper = new FileModelHelper(webAppDaoFactory); + this.fileStorage = fileStorage; + } + + /** + * The image must be present and non-empty, and must have a mime-type that + * represents an image we support. + * + * We rely on the fact that a {@link FileUploadServletRequest} will always + * have a map of {@link FileItem}s, even if it is empty. However, that map + * may not contain the field that we want, or that field may contain an + * empty file. + * + * @throws UserMistakeException + * if there is no file, if it is empty, or if it is not an image + * file. + */ + @SuppressWarnings("unchecked") + FileItem validateImageFromRequest(HttpServletRequest request) + throws UserMistakeException { + Map> map = (Map>) request + .getAttribute(FileUploadServletRequest.FILE_ITEM_MAP); + if (map == null) { + throw new IllegalStateException("Failed to parse the " + + "multi-part request for uploading an image."); + } + List list = map.get(PARAMETER_UPLOADED_FILE); + if ((list == null) || list.isEmpty()) { + throw new UserMistakeException("The form did not contain a '" + + PARAMETER_UPLOADED_FILE + "' field."); + } + + FileItem file = list.get(0); + if (file.getSize() == 0) { + throw new UserMistakeException("No file was uploaded in '" + + PARAMETER_UPLOADED_FILE + "'"); + } + + String filename = getSimpleFilename(file); + String mimeType = getMimeType(file); + if (!RECOGNIZED_FILE_TYPES.containsValue(mimeType)) { + throw new UserMistakeException("'" + filename + + "' is not a recognized image file type. " + + "These are the recognized types: " + + RECOGNIZED_FILE_TYPES); + } + + return file; + } + + /** + * If this entity already had a main image, remove the connection. If the + * image and the thumbnail are no longer used by anyone, remove them from + * the model, and from the file system. + */ + void removeExistingImage(Individual person) { + Individual mainImage = fileModelHelper.removeMainImage(person); + if (mainImage == null) { + return; + } + + removeExistingThumbnail(person); + + if (!fileModelHelper.isFileReferenced(mainImage)) { + Individual bytes = FileModelHelper.getBytestreamForFile(mainImage); + if (bytes != null) { + try { + fileStorage.deleteFile(bytes.getURI()); + } catch (IOException e) { + throw new IllegalStateException( + "Can't delete the main image file: '" + + bytes.getURI() + "' for '" + + person.getName() + "' (" + + person.getURI() + ")", e); + } + } + fileModelHelper.removeFileFromModel(mainImage); + } + } + + /** + * Store this image in the model and in the file storage system, and set it + * as the main image for this person. + */ + void storeMainImageFile(Individual person, FileItem imageFileItem) { + InputStream inputStream = null; + try { + inputStream = imageFileItem.getInputStream(); + String mimeType = getMimeType(imageFileItem); + String filename = getSimpleFilename(imageFileItem); + + // Create the file individuals in the model + Individual byteStream = fileModelHelper + .createByteStreamIndividual(); + Individual file = fileModelHelper.createFileIndividual(mimeType, + filename, byteStream); + + // Store the file in the FileStorage system. + fileStorage.createFile(byteStream.getURI(), filename, inputStream); + + // Set the file as the main image for the person. + fileModelHelper.setAsMainImageOnEntity(person, file); + } catch (FileAlreadyExistsException e) { + throw new IllegalStateException( + "Can't create the main image file for '" + person.getName() + + "' (" + person.getURI() + ")" + e.getMessage(), e); + } catch (IOException e) { + throw new IllegalStateException( + "Can't create the main image file for '" + person.getName() + + "' (" + person.getURI() + ")", e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * If the entity already has a thumbnail, remove it. If there are no other + * references to the thumbnail, delete it from the model and from the file + * system. + */ + void removeExistingThumbnail(Individual person) { + Individual mainImage = FileModelHelper.getMainImage(person); + Individual thumbnail = FileModelHelper.getThumbnailForImage(mainImage); + if (thumbnail == null) { + return; + } + + fileModelHelper.removeThumbnail(person); + + if (!fileModelHelper.isFileReferenced(thumbnail)) { + Individual bytes = FileModelHelper.getBytestreamForFile(thumbnail); + if (bytes != null) { + try { + fileStorage.deleteFile(bytes.getURI()); + } catch (IOException e) { + throw new IllegalStateException( + "Can't delete the thumbnail file: '" + + bytes.getURI() + "' for '" + + person.getName() + "' (" + + person.getURI() + ")", e); + } + } + fileModelHelper.removeFileFromModel(thumbnail); + } + } + + /** + * Generate a thumbnail from the main image from it, store it in the model + * and in the file storage system, and set it as the thumbnail on the main + * image. + */ + void generateThumbnailAndStore(Individual person, + ImageUploadController.CropRectangle crop) { + String mainBytestreamUri = FileModelHelper + .getMainImageBytestreamUri(person); + String mainFilename = FileModelHelper.getMainImageFilename(person); + if (mainBytestreamUri == null) { + log.warn("Tried to generate a thumbnail on '" + person.getURI() + + "', but there was no main image."); + return; + } + + InputStream mainInputStream = null; + InputStream thumbInputStream = null; + try { + mainInputStream = fileStorage.getInputStream(mainBytestreamUri, + mainFilename); + thumbInputStream = scaleImageForThumbnail(mainInputStream, crop); + String mimeType = RECOGNIZED_FILE_TYPES.get(".jpg"); + String filename = createThumbnailFilename(mainFilename); + + // Create the file individuals in the model + Individual byteStream = fileModelHelper + .createByteStreamIndividual(); + Individual file = fileModelHelper.createFileIndividual(mimeType, + filename, byteStream); + + // Store the file in the FileStorage system. + fileStorage.createFile(byteStream.getURI(), filename, + thumbInputStream); + + // Set the file as the thumbnail on the main image for the person. + fileModelHelper.setThumbnailOnIndividual(person, file); + } catch (FileAlreadyExistsException e) { + throw new IllegalStateException("Can't create the thumbnail file: " + + e.getMessage(), e); + } catch (IOException e) { + throw new IllegalStateException("Can't create the thumbnail file", + e); + } finally { + if (mainInputStream != null) { + try { + mainInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (thumbInputStream != null) { + try { + thumbInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Internet Explorer and Opera will give you the full path along with the + * filename. This will remove the path. + */ + private String getSimpleFilename(FileItem item) { + String fileName = item.getName(); + if (fileName == null) { + return null; + } else { + return FilenameUtils.getName(fileName); + } + } + + /** + * Get the MIME type as supplied by the browser. If none, try to infer it + * from the filename extension and the map of recognized MIME types. + */ + private String getMimeType(FileItem file) { + String mimeType = file.getContentType(); + if (mimeType != null) { + return mimeType; + } + + String filename = getSimpleFilename(file); + int periodHere = filename.lastIndexOf('.'); + if (periodHere == -1) { + return null; + } + + String extension = filename.substring(periodHere); + return RECOGNIZED_FILE_TYPES.get(extension); + } + + /** + * Create a name for the thumbnail from the name of the original file. + * "myPicture.anything" becomes "thumbnail_myPicture.jpg". + */ + private String createThumbnailFilename(String filename) { + String prefix = "thumbnail_"; + String extension = ".jpg"; + int periodHere = filename.lastIndexOf('.'); + if (periodHere == -1) { + return prefix + filename + extension; + } else { + return prefix + filename.substring(0, periodHere) + extension; + } + } + + /** + * Create a thumbnail from a source image, given a cropping rectangle (x, y, + * width, height). + */ + private InputStream scaleImageForThumbnail(InputStream source, + ImageUploadController.CropRectangle crop) throws IOException { + BufferedImage bsrc = ImageIO.read(source); + + // Insure that x and y fall within the image dimensions. + int x = Math.max(0, Math.min(bsrc.getWidth(), crop.x)); + int y = Math.max(0, Math.min(bsrc.getHeight(), crop.y)); + + // Insure that width and height are reasonable. + int w = Math.max(5, Math.min(bsrc.getWidth() - x, crop.width)); + int h = Math.max(5, Math.min(bsrc.getHeight() - y, crop.height)); + + // Figure the scales. + double scaleWidth = ((double) THUMBNAIL_WIDTH) / ((double) w); + double scaleHeight = ((double) THUMBNAIL_HEIGHT) / ((double) h); + + // Create the transform. + AffineTransform at = new AffineTransform(); + at.translate(-x, -y); + at.scale(scaleWidth, scaleHeight); + + // Apply the transform. + BufferedImage bdest = new BufferedImage(crop.width, crop.height, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = bdest.createGraphics(); + g.drawRenderedImage(bsrc, at); + + // Get an input stream. + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ImageIO.write(bdest, "JPG", buffer); + return new ByteArrayInputStream(buffer.toByteArray()); + } +}