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());
+ }
+}