Proof of concept - Image upload

This commit is contained in:
j2blake 2013-04-26 23:50:17 -04:00
parent c59076a19a
commit bf2ed5c339
9 changed files with 248 additions and 63 deletions

View file

@ -36,6 +36,7 @@ import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup;
import edu.cornell.mannlib.vitro.webapp.filestorage.model.FileInfo;
import edu.cornell.mannlib.vitro.webapp.filestorage.model.ImageInfo;
import edu.cornell.mannlib.vitro.webapp.filestorage.uploadrequest.FileUploadServletRequest;
import edu.cornell.mannlib.vitro.webapp.i18n.I18n;
import edu.cornell.mannlib.vitro.webapp.web.images.PlaceholderUtil;
/**
@ -48,6 +49,9 @@ public class ImageUploadController extends FreemarkerHttpServlet {
private static final String ATTRIBUTE_REFERRING_PAGE = "ImageUploadController.referringPage";
private static final String ERROR_CODE_UNRECOGNIZED_URI = "imageUpload.errorUnrecognizedURI";
private static final String ERROR_CODE_NO_URI = "imageUpload.errorNoURI";
/** Limit file size to 6 megabytes. */
public static final int MAXIMUM_FILE_SIZE = 6 * 1024 * 1024;
@ -97,6 +101,14 @@ public class ImageUploadController extends FreemarkerHttpServlet {
private static final String URL_HERE = UrlBuilder.getUrl("/uploadImages");
private static final String TEXT_BUNDLE = "imageUpload";
private static final String TEXT_STRING_UPLOAD_TITLE = "upload_page_title";
private static final String TEXT_STRING_UPLOAD_TITLE_WITH_NAME = "upload_page_title_with_name";
private static final String TEXT_STRING_REPLACE_TITLE = "replace_page_title";
private static final String TEXT_STRING_REPLACE_TITLE_WITH_NAME = "replace_page_title_with_name";
private static final String TEXT_STRING_CROP_TITLE = "crop_page_title";
private static final String TEXT_STRING_CROP_TITLE_WITH_NAME = "crop_page_title_with_name";
private FileStorage fileStorage;
/**
@ -218,7 +230,7 @@ public class ImageUploadController extends FreemarkerHttpServlet {
}
} catch (UserMistakeException e) {
// Can't find the entity? Complain.
return showAddImagePageWithError(vreq, null, e.getMessage());
return showAddImagePageWithError(vreq, null, e.formatMessage(vreq));
} catch (Exception e) {
// We weren't expecting this - log it, and apologize to the user.
return new ExceptionResponseValues(e);
@ -274,7 +286,7 @@ public class ImageUploadController extends FreemarkerHttpServlet {
return showCropImagePage(vreq, entity,
fileInfo.getBytestreamAliasUrl(), size);
} catch (UserMistakeException e) {
return showErrorMessage(vreq, entity, e.getMessage());
return showErrorMessage(vreq, entity, e.formatMessage(vreq));
}
}
@ -284,6 +296,7 @@ public class ImageUploadController extends FreemarkerHttpServlet {
*/
private ResponseValues showErrorMessage(VitroRequest vreq,
Individual entity, String message) {
ImageInfo imageInfo = ImageInfo.instanceFromEntityUri(
vreq.getFullWebappDaoFactory(), entity);
if (imageInfo == null) {
@ -313,7 +326,7 @@ public class ImageUploadController extends FreemarkerHttpServlet {
return showExitPage(vreq, entity);
} catch (UserMistakeException e) {
return showErrorMessage(vreq, entity, e.getMessage());
return showErrorMessage(vreq, entity, e.formatMessage(vreq));
}
}
@ -350,15 +363,14 @@ public class ImageUploadController extends FreemarkerHttpServlet {
throws UserMistakeException {
String entityUri = vreq.getParameter(PARAMETER_ENTITY_URI);
if (entityUri == null) {
throw new UserMistakeException("No entity URI was provided");
throw new UserMistakeException(ERROR_CODE_NO_URI);
}
Individual entity = vreq.getFullWebappDaoFactory().getIndividualDao()
.getIndividualByURI(entityUri);
if (entity == null) {
throw new UserMistakeException(
"This URI is not recognized as belonging to anyone: '"
+ entityUri + "'");
throw new UserMistakeException(ERROR_CODE_UNRECOGNIZED_URI,
entityUri);
}
return entity;
}
@ -416,7 +428,7 @@ public class ImageUploadController extends FreemarkerHttpServlet {
rv.put(BODY_THUMBNAIL_URL, placeholderUrl);
rv.put(BODY_FORM_ACTION, formAction);
rv.put(BODY_CANCEL_URL, cancelUrl);
rv.put(BODY_TITLE, "Upload image" + forName(entity));
rv.put(BODY_TITLE, figureUploadPageTitle(vreq, entity));
rv.put(BODY_MAX_FILE_SIZE, MAXIMUM_FILE_SIZE / (1024 * 1024));
rv.put(BODY_THUMBNAIL_HEIGHT, THUMBNAIL_HEIGHT);
rv.put(BODY_THUMBNAIL_WIDTH, THUMBNAIL_WIDTH);
@ -442,7 +454,7 @@ public class ImageUploadController extends FreemarkerHttpServlet {
rv.put(BODY_DELETE_URL, formAction(entity.getURI(), ACTION_DELETE_EDIT));
rv.put(BODY_FORM_ACTION, formAction(entity.getURI(), ACTION_UPLOAD));
rv.put(BODY_CANCEL_URL, exitPageUrl(vreq, entity.getURI()));
rv.put(BODY_TITLE, "Replace image" + forName(entity));
rv.put(BODY_TITLE, figureReplacePageTitle(vreq, entity));
rv.put(BODY_MAX_FILE_SIZE, MAXIMUM_FILE_SIZE / (1024 * 1024));
rv.put(BODY_THUMBNAIL_HEIGHT, THUMBNAIL_HEIGHT);
rv.put(BODY_THUMBNAIL_WIDTH, THUMBNAIL_WIDTH);
@ -472,7 +484,7 @@ public class ImageUploadController extends FreemarkerHttpServlet {
rv.put(BODY_MAIN_IMAGE_WIDTH, dimensions.width);
rv.put(BODY_FORM_ACTION, formAction(entity.getURI(), ACTION_SAVE));
rv.put(BODY_CANCEL_URL, exitPageUrl(vreq, entity.getURI()));
rv.put(BODY_TITLE, "Crop Photo" + forName(entity));
rv.put(BODY_TITLE, figureCropPageTitle(vreq, entity));
return rv;
}
@ -522,24 +534,59 @@ public class ImageUploadController extends FreemarkerHttpServlet {
}
/**
* Format the entity's name for display as part of the page title.
* Format the title for the Upload page.
*/
private String forName(Individual entity) {
private String figureUploadPageTitle(HttpServletRequest req,
Individual entity) {
return figurePageTitle(req, entity, TEXT_STRING_UPLOAD_TITLE,
TEXT_STRING_UPLOAD_TITLE_WITH_NAME);
}
/**
* Format the title for the Replace page.
*/
private String figureReplacePageTitle(HttpServletRequest req,
Individual entity) {
return figurePageTitle(req, entity, TEXT_STRING_REPLACE_TITLE,
TEXT_STRING_REPLACE_TITLE_WITH_NAME);
}
/**
* Format the title for the Crop page.
*/
private String figureCropPageTitle(HttpServletRequest req, Individual entity) {
return figurePageTitle(req, entity, TEXT_STRING_CROP_TITLE,
TEXT_STRING_CROP_TITLE_WITH_NAME);
}
/**
* Format one of two page titles, depending on whether the entity has a
* name.
*/
private String figurePageTitle(HttpServletRequest req, Individual entity,
String noNameTitleKey, String nameTitleKey) {
if (entity != null) {
String name = entity.getName();
if (name != null) {
return " for " + name;
return I18n.text(req, TEXT_BUNDLE, nameTitleKey, name);
}
}
return "";
return I18n.text(req, TEXT_BUNDLE, noNameTitleKey);
}
/**
* Holds an error message to use as a complaint to the user.
*/
static class UserMistakeException extends Exception {
UserMistakeException(String message) {
private final Object[] parameters;
UserMistakeException(String message, Object... parameters) {
super(message);
this.parameters = parameters;
}
public String formatMessage(HttpServletRequest req) {
return I18n.text(req, getMessage(), parameters);
}
}

View file

@ -49,6 +49,18 @@ import edu.cornell.mannlib.vitro.webapp.filestorage.uploadrequest.FileUploadServ
public class ImageUploadHelper {
private static final Log log = LogFactory.getLog(ImageUploadHelper.class);
/*
* Keys to text strings for error messages.
*/
private static final String ERROR_CODE_NO_IMAGE_TO_CROP = "imageUpload.errorNoImageForCropping";
private static final String ERROR_CODE_IMAGE_TOO_SMALL = "imageUpload.errorImageTooSmall";
private static final String ERROR_CODE_UNKNOWN = "imageUpload.errorUnknown";
private static final String ERROR_CODE_FILE_TOO_BIG = "imageUpload.errorFileTooBig";
private static final String ERROR_CODE_UNRECOGNIZED_FILE_TYPE = "imageUpload.errorUnrecognizedFileType";
private static final String ERROR_CODE_NO_PHOTO_SELECTED = "imageUpload.errorNoPhotoSelected";
private static final String ERROR_CODE_BAD_MULTIPART_REQUEST = "imageUpload.errorBadMultipartRequest";
private static final String ERROR_CODE_FORM_FIELD_MISSING = "imageUpload.errorFormFieldMissing";
/**
* When they upload a new image, store it as this session attribute until
* we're ready to attach it to the Individual.
@ -127,35 +139,31 @@ public class ImageUploadHelper {
Object exception = request.getAttribute(FILE_UPLOAD_EXCEPTION);
if (exception != null) {
int limit = MAXIMUM_FILE_SIZE / (1024 * 1024);
throw new UserMistakeException(
"Please upload an image smaller than " + limit
+ " megabytes");
throw new UserMistakeException(ERROR_CODE_FILE_TOO_BIG, limit);
}
Map<String, List<FileItem>> map = (Map<String, List<FileItem>>) request
.getAttribute(FILE_ITEM_MAP);
if (map == null) {
throw new IllegalStateException("Failed to parse the "
+ "multi-part request for uploading an image.");
throw new IllegalStateException(ERROR_CODE_BAD_MULTIPART_REQUEST);
}
List<FileItem> list = map.get(PARAMETER_UPLOADED_FILE);
if ((list == null) || list.isEmpty()) {
throw new UserMistakeException("The form did not contain a '"
+ PARAMETER_UPLOADED_FILE + "' field.");
throw new UserMistakeException(ERROR_CODE_FORM_FIELD_MISSING,
PARAMETER_UPLOADED_FILE);
}
FileItem file = list.get(0);
if (file.getSize() == 0) {
throw new UserMistakeException("Please browse and select a photo.");
throw new UserMistakeException(ERROR_CODE_NO_PHOTO_SELECTED);
}
String filename = getSimpleFilename(file);
String mimeType = getMimeType(file);
if (!RECOGNIZED_FILE_TYPES.containsValue(mimeType)) {
log.debug("Unrecognized MIME type: '" + mimeType + "'");
throw new UserMistakeException("'" + filename
+ "' is not a recognized image file type. "
+ "Please upload JPEG, GIF, or PNG files only.");
throw new UserMistakeException(ERROR_CODE_UNRECOGNIZED_FILE_TYPE,
filename);
}
return file;
@ -221,10 +229,8 @@ public class ImageUploadHelper {
if ((size.height < THUMBNAIL_HEIGHT)
|| (size.width < THUMBNAIL_WIDTH)) {
throw new UserMistakeException(
"The uploaded image should be at least "
+ THUMBNAIL_HEIGHT + " pixels high and "
+ THUMBNAIL_WIDTH + " pixels wide.");
throw new UserMistakeException(ERROR_CODE_IMAGE_TOO_SMALL,
THUMBNAIL_HEIGHT, THUMBNAIL_WIDTH);
}
return size;
@ -237,8 +243,7 @@ public class ImageUploadHelper {
throw e;
} catch (Exception e) {
log.warn("Unexpected exception in image handling", e);
throw new UserMistakeException("Sorry, we were unable to process "
+ "the photo you provided. Please try another photo.");
throw new UserMistakeException(ERROR_CODE_UNKNOWN);
} finally {
if (source != null) {
try {
@ -261,8 +266,7 @@ public class ImageUploadHelper {
ATTRIBUTE_TEMP_FILE);
if (fileInfo == null) {
throw new UserMistakeException(
"There is no image file to be cropped.");
throw new UserMistakeException(ERROR_CODE_NO_IMAGE_TO_CROP);
}
return fileInfo;

View file

@ -0,0 +1,73 @@
#
# Text strings for the controllers and templates
#
# Default (English)
#
save_changes = Save changes
cancel_link = Cancel
cancel_title = cancel
required_fields = required fields
or = or
alt_error_alert = Error alert icon
alt_confirmation = Confirmation icon
email_address = Email address
first_name = First name
last_name = Last name
roles = Roles
status = Status
ascending_order = ascending order
descending_order = descending order
select_one = Select one
type_more_characters = type more characters
no_match = no match
request_failed = Request failed. Please contact your system administrator.
#
# Image upload pages
#
upload_page_title = Upload image
upload_page_title_with_name = Upload image for {0}
upload_heading = Photo Upload
replace_page_title = Replace image
replace_page_title_with_name = Replace image for {0}
crop_page_title = Crop image
crop_page_title_with_name = Crop image for {0}
current_photo = Current Photo
upload_photo = Upload a photo
replace_photo = Replace Photo
photo_types = (JPEG, GIF or PNG)
maximum_file_size = Maximum file size: {0} megabytes
minimum_image_dimensions = Minimum image dimensions: {0} x {1} pixels
cropping_caption = Your profile photo will look like the image below.
cropping_note = To make adjustments, you can drag around and resize the photo to the right. \
When you are happy with your photo click the "Save Photo" button.
alt_thumbnail_photo = Individual photo
alt_image_to_crop = Image to be cropped
alt_preview_crop = Preview of photo cropped
delete_link = Delete photo
submit_upload = Upload photo
submit_save = Save photo
confirm_delete = Are you sure you want to delete this photo?
imageUpload.errorNoURI = No entity URI was provided
imageUpload.errorUnrecognizedURI = This URI is not recognized as belonging to anyone: ''{0}''
imageUpload.errorNoImageForCropping = There is no image file to be cropped.
imageUpload.errorImageTooSmall = The uploaded image should be at least {0} pixels high and {1} pixels wide.
imageUpload.errorUnknown = Sorry, we were unable to process the photo you provided. Please try another photo.
imageUpload.errorFileTooBig = Please upload an image smaller than {0} megabytes.
imageUpload.errorUnrecognizedFileType = ''{0}'' is not a recognized image file type. Please upload JPEG, GIF, or PNG files only.
imageUpload.errorNoPhotoSelected = Please browse and select a photo.
imageUpload.errorBadMultipartRequest = Failed to parse the multi-part request for uploading an image.
imageUpload.errorFormFieldMissing = The form did not contain a ''{0}'' field."

View file

@ -0,0 +1,43 @@
#
# Text strings for the controllers and templates
#
# Spanish
#
cancel_link = Cancelar
or = o
alt_error_alert = Icono de alerta con error
#
# Image upload pages
#
upload_page_title = Subir foto
upload_page_title_with_name = Subir imagen para {0}
upload_heading = Subir foto
replace_page_title = Reemplazar imagen
replace_page_title_with_name = Cambie la imagen por {0}
crop_page_title = Recortar imagen
crop_page_title_with_name = Recorte imagen para {0}
current_photo = Foto actual
upload_photo = Suba foto
replace_photo = Reemplace foto
photo_types = (JPEG, GIF, o PNG)
maximum_file_size = Tamaño máximo de archivo: {0} megabytes
minimum_image_dimensions = Dimensiones mínimas de imagen: {0} x {1} pixels
cropping_caption = La foto de tu perfil se verá como la imagen de abajo.
cropping_note = Para realizar ajustes, arrastre alrededor y cambie el tamaño de la foto de la derecha. \
Cuando esté satisfecho con su foto, haga clic en el botón "Guardar foto".
alt_thumbnail_photo = Foto de individuo
alt_image_to_crop = Imagen que desea recortar
alt_preview_crop = Vista previa de la foto recortada
delete_link = Borrar foto
submit_upload = Subir foto
submit_save = Guardar foto
confirm_delete = ¿Seguro que quiere borrar esta foto?

View file

@ -4,7 +4,7 @@ $(document).ready(function(){
// Confirmation alert for photo deletion in image upload and individual templates
$('#photoUploadDefaultImage a.thumbnail, a.delete-mainImage').click(function(){
var answer = confirm('Are you sure you want to delete this photo?');
var answer = confirm(i18n_confirmDelete);
return answer;
});
});

View file

@ -21,20 +21,22 @@ ${stylesheets.add('<link rel="stylesheet" href="${urls.base}/css/uploadImages.cs
</#macro>-->
<#assign i18n = i18n() >
<section id="photoCroppingContainer" role="region">
<h2>Photo Upload</h2>
<h2>${i18n.upload_heading}</h2>
<!-- This is the image we're attaching Jcrop to -->
<section id="photoCroppingPreview" role="region">
<p class="photoCroppingTitleBody">Your profile photo will look like the image below. </p>
<p class="photoCroppingTitleBody">${i18n.cropping_caption}</p>
<section class="photoCroppedPreview" role="region">
<img src="${imageUrl}" id="preview" alt="Image to be cropped"/>
<img src="${imageUrl}" id="preview" alt="${i18n.alt_image_to_crop}"/>
</section>
<section id="photoCroppingHowTo" role="region">
<p class="photoCroppingNote">To make adjustments, you can drag around and resize the photo to the right. When you are happy with your photo click the "Save Photo" button. </p>
<p class="photoCroppingNote">${i18n.cropping_note}</p>
<form id="cropImage" action="${formAction}" method="post" role="form">
<!-- Javascript will populate these values -->
@ -43,15 +45,15 @@ ${stylesheets.add('<link rel="stylesheet" href="${urls.base}/css/uploadImages.cs
<input type="hidden" name="w" value="" />
<input type="hidden" name="h" value="" />
<input class="submit" type="submit" value="Save photo">
<input class="submit" type="submit" value="${i18n.submit_save}">
<span class="or"> or <a class="cancel" href="${cancelUrl}" title="cancel">Cancel</a></span>
<span class="or"> ${i18n.or} <a class="cancel" href="${cancelUrl}" title="${i18n.cancel_title}">${i18n.cancel_link}</a></span>
</form>
</section>
</section>
<section id="photoCropping" role="region">
<img src="${imageUrl}" id="cropbox" alt="Preview of photo cropped" />
<img src="${imageUrl}" id="cropbox" alt="${i18n.alt_preview_crop}" />
</section
</section>

View file

@ -7,29 +7,35 @@ ${scripts.add('<script type="text/javascript" src="${urls.base}/js/jquery.js"></
${stylesheets.add('<link rel="stylesheet" href="${urls.base}/css/uploadImages.css" />')}
<#assign i18n = i18n() >
<section id="photoUploadContainer" role="region">
<h2>Photo Upload</h2>
<h2>${i18n.upload_heading}</h2>
<#if errorMessage??>
<section id="error-alert" role="alert"><img src="${urls.images}/iconAlert.png" alt="Error alert icon" />
<section id="error-alert" role="alert"><img src="${urls.images}/iconAlert.png" alt="${i18n.alt_error_alert}" />
<p>${errorMessage}</p>
</section>
</#if>
<section id="photoUploadDefaultImage" role="region">
<h3>Current Photo</h3>
<h3>${i18n.current_photo}</h3>
<img src="${thumbnailUrl}" width="115" height="115" alt="Individual photo" />
<img src="${thumbnailUrl}" width="115" height="115" alt="${i18n.alt_thumbnail_photo}" />
</section>
<form id="photoUploadForm" action="${formAction}" enctype="multipart/form-data" method="post" role="form">
<label>Upload a photo <span> (JPEG, GIF or PNG)</span></label>
<label>${i18n.upload_photo} <span>${i18n.photo_types}</span></label>
<input id="datafile" type="file" name="datafile" size="30" />
<p class="note">Maximum file size: ${maxFileSize} megabytes<br />
Minimum image dimensions: ${thumbnailWidth} x ${thumbnailHeight} pixels</p>
<input class="submit" type="submit" value="Upload photo"/>
<p class="note">${i18n.maximum_file_size(maxFileSize)}<br />
${i18n.minimum_image_dimensions(thumbnailWidth, thumbnailHeight)}</p>
<input class="submit" type="submit" value="${i18n.submit_upload}"/>
<span class="or"> or <a class="cancel" href="${cancelUrl}" title="cancel">Cancel</a></span>
<span class="or"> ${i18n.or} <a class="cancel" href="${cancelUrl}" title="${i18n.cancel_title}">${i18n.cancel_link}</a></span>
</form>
</section>
<script type="text/javascript">
i18n_confirmDelete = ${i18n.confirm_delete}
</script>

View file

@ -7,30 +7,36 @@ ${scripts.add('<script type="text/javascript" src="${urls.base}/js/jquery.js"></
${stylesheets.add('<link rel="stylesheet" href="${urls.base}/css/uploadImages.css" />')}
<#assign i18n = i18n() >
<section id="photoUpload" role="region">
<h2>Photo Upload</h2>
<h2>${i18n.upload_heading}</h2>
<#if errorMessage??>
<section id="error-alert" role="alert"><img src="${urls.images}/iconAlert.png" alt="Error alert icon" />
<section id="error-alert" role="alert"><img src="${urls.images}/iconAlert.png" alt="${i18n.alt_error_alert}" />
<p>${errorMessage}</p>
</section>
</#if>
<section id="photoUploadDefaultImage" role="region">
<h3>Current Photo</h3>
<img src="${thumbnailUrl}" width="115" alt="Individual photo" />
<h3>${i18n.current_photo}</h3>
<img src="${thumbnailUrl}" width="115" alt="${i18n.alt_thumbnail_photo}" />
<a class="thumbnail" href="${deleteUrl}">Delete photo</a>
<a class="thumbnail" href="${deleteUrl}">${i18n.delete_link}</a>
</section>
<form id="photoUploadForm" action="${formAction}" enctype="multipart/form-data" method="post" role="form">
<label>Replace Photo <span> (JPEG, GIF or PNG)</span></label>
<label>${i18n.replace_photo} <span>${i18n.photo_types}</span></label>
<input type="file" name="datafile" size="30">
<p class="note">Maximum file size: ${maxFileSize} megabytes<br />
Minimum image dimensions: ${thumbnailWidth}x${thumbnailHeight} pixels</p>
<input class="submit" type="submit" value="Upload photo">
<p class="note">${i18n.maximum_file_size(maxFileSize)}<br />
${i18n.minimum_image_dimensions(thumbnailWidth, thumbnailHeight)}</p>
<input class="submit" type="submit" value="${i18n.submit_upload}">
<span class="or"> or <a class="cancel" href="${cancelUrl}" title="cancel">Cancel</a></span>
<span class="or"> ${i18n.or} <a class="cancel" href="${cancelUrl}" title="${i18n.cancel_title}">${i18n.cancel_link}</a></span>
</form>
</section>
<script type="text/javascript">
i18n_confirmDelete = "${i18n.confirm_delete}"
</script>

View file

@ -77,3 +77,7 @@ ${headScripts.add('<script type="text/javascript" src="${urls.base}/js/jquery_pl
${scripts.add('<script type="text/javascript" src="${urls.base}/js/imageUpload/imageUploadUtils.js"></script>',
'<script type="text/javascript" src="${urls.base}/js/individual/individualUriRdf.js"></script>')}
<script type="text/javascript">
i18n_confirmDelete = "${i18n().confirm_delete}"
</script>