initials files for client image management.

This commit is contained in:
Vishwas Babu AJ
2012-10-28 14:43:33 +00:00
committed by Keith Woodlock
parent 3d5c0d88c0
commit 2325878e4e
15 changed files with 369 additions and 35 deletions
@@ -0,0 +1,2 @@
ALTER TABLE `m_client`
ADD COLUMN `image_key` VARCHAR(500) NULL DEFAULT NULL AFTER `display_name`;
@@ -1,5 +1,6 @@
package org.mifosng.platform.api;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -9,6 +10,7 @@ import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
@@ -23,8 +25,8 @@ import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.mifosng.platform.api.commands.ClientCommand;
import org.mifosng.platform.api.commands.NoteCommand;
import org.mifosng.platform.api.data.ClientData;
import org.mifosng.platform.api.data.ClientAccountSummaryCollectionData;
import org.mifosng.platform.api.data.ClientData;
import org.mifosng.platform.api.data.EntityIdentifier;
import org.mifosng.platform.api.data.NoteData;
import org.mifosng.platform.api.data.OfficeLookup;
@@ -33,6 +35,10 @@ import org.mifosng.platform.api.infrastructure.ApiJsonSerializerService;
import org.mifosng.platform.api.infrastructure.ApiParameterHelper;
import org.mifosng.platform.client.service.ClientReadPlatformService;
import org.mifosng.platform.client.service.ClientWritePlatformService;
import org.mifosng.platform.common.ApplicationConstants;
import org.mifosng.platform.common.FileUtils;
import org.mifosng.platform.exceptions.ClientNotFoundException;
import org.mifosng.platform.exceptions.ImageNotFoundException;
import org.mifosng.platform.organisation.service.OfficeReadPlatformService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,6 +46,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.lowagie.text.pdf.codec.Base64;
import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataParam;
@Path("/clients")
@Component
@Scope("singleton")
@@ -63,7 +74,7 @@ public class ClientsApiResource {
private ApiJsonSerializerService apiJsonSerializerService;
private static final Set<String> typicalResponseParameters = new HashSet<String>(
Arrays.asList("id", "officeId", "officeName", "externalId", "firstname", "lastname", "joinedDate", "displayName", "clientOrBusinessName")
Arrays.asList("id", "officeId", "officeName", "externalId", "firstname", "lastname", "joinedDate", "displayName", "clientOrBusinessName", "imagePresent")
);
@GET
@@ -313,4 +324,93 @@ public class ClientsApiResource {
return Response.ok().entity(identifier).build();
}
@POST
@Path("{clientId}/image")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
public Response addNewClientImage(
@PathParam("clientId") final Long clientId,
@HeaderParam("Content-Length") Long fileSize,
@FormDataParam("file") InputStream inputStream,
@FormDataParam("file") FormDataContentDisposition fileDetails,
@FormDataParam("file") FormDataBodyPart bodyPart) {
//TODO: vishwas might need more advances validation (like reading magic number) for handling malicious clients
//and clients not setting mime type
FileUtils.validateClientImageNotEmpty(fileDetails.getFileName());
FileUtils.validateImageMimeType(bodyPart.getMediaType().toString());
FileUtils.validateFileSizeWithinPermissibleRange(fileSize,
fileDetails.getFileName(),
ApplicationConstants.MAX_FILE_UPLOAD_SIZE_IN_MB);
System.out.println( bodyPart.getMediaType().toString());
EntityIdentifier entityIdentifier = this.clientWritePlatformService
.saveOrUpdateClientImage(clientId, fileDetails.getFileName(),
inputStream);
// return identifier of the client whose image was updated
return Response.ok().entity(entityIdentifier).build();
}
/**Returns a base 64 encoded client image
* @param clientId
* @return
*/
@GET
@Path("{clientId}/image")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public Response retrieveClientImage(
@PathParam("clientId") final Long clientId) {
ClientData clientData = this.clientReadPlatformService.retrieveIndividualClient(clientId);
//validate if client does not exist or image key is null
if(clientData == null){
throw new ClientNotFoundException(clientId);
}
else if(clientData.getImageKey()== null){
throw new ImageNotFoundException(ApplicationConstants.IMAGE_MANAGEMENT_ENTITY.CLIENTS, clientId);
}
return Response.ok().entity(Base64.encodeFromFile(clientData.getImageKey())).build();
}
/** This method is added only for consistency with other URL patterns
* and for maintaining consistency of usage of the HTTP "verb"
* at the client side
*
* @param clientId
* @param fileSize
* @param inputStream
* @param fileDetails
* @param bodyPart
* @return
*/
@PUT
@Path("{clientId}/image")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
public Response updateClientImage(
@PathParam("clientId") final Long clientId,
@HeaderParam("Content-Length") Long fileSize,
@FormDataParam("file") InputStream inputStream,
@FormDataParam("file") FormDataContentDisposition fileDetails,
@FormDataParam("file") FormDataBodyPart bodyPart) {
return addNewClientImage(clientId, fileSize, inputStream, fileDetails,
bodyPart);
}
@DELETE
@Path("{clientId}/image")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public Response deleteClientImage(@PathParam("clientId") final Long clientId) {
this.clientWritePlatformService.deleteClientImage(clientId);
return Response.ok(new EntityIdentifier(clientId)).build();
}
}
@@ -28,9 +28,9 @@ import org.mifosng.platform.api.data.EntityIdentifier;
import org.mifosng.platform.api.infrastructure.ApiJsonSerializerService;
import org.mifosng.platform.api.infrastructure.ApiParameterHelper;
import org.mifosng.platform.common.ApplicationConstants;
import org.mifosng.platform.common.FileUtils;
import org.mifosng.platform.documentmanagement.service.DocumentReadPlatformService;
import org.mifosng.platform.documentmanagement.service.DocumentWritePlatformService;
import org.mifosng.platform.exceptions.DocumentManagementException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -109,7 +109,7 @@ public class DocumentManagementApiResource {
@FormDataParam("name") String name,
@FormDataParam("description") String description) {
initialFileCheckerFastFail(fileSize, name);
FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApplicationConstants.MAX_FILE_UPLOAD_SIZE_IN_MB);
/**
* TODO: also need to have a backup and stop reading from stream after
@@ -117,7 +117,7 @@ public class DocumentManagementApiResource {
**/
/**
* TODO: need to extract the actual file type and determine fi they are
* TODO: need to extract the actual file type and determine if they are
* permissable
**/
DocumentCommand documentCommand = new DocumentCommand(null, null,
@@ -156,7 +156,7 @@ public class DocumentManagementApiResource {
@FormDataParam("name") String name,
@FormDataParam("description") String description) {
initialFileCheckerFastFail(fileSize, name);
FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApplicationConstants.MAX_FILE_UPLOAD_SIZE_IN_MB);
Set<String> modifiedParams = new HashSet<String>();
modifiedParams.add("name");
@@ -187,22 +187,6 @@ public class DocumentManagementApiResource {
return Response.ok().entity(identifier).build();
}
/**
* @param fileSize
* @param name
*/
private void initialFileCheckerFastFail(Long fileSize, String name) {
/**
* Using Content-Length gives me size of the entire request, which is
* good enough for now for a fast fail as the length of the rest of the
* content i.e name and description while compared to the uploaded file
* size is negligible
**/
if (fileSize != null
&& ((fileSize / (1024 * 1024)) > ApplicationConstants.MAX_FILE_UPLOAD_SIZE_IN_MB)) {
throw new DocumentManagementException(name, fileSize);
}
}
/**
* @param entityType
@@ -20,6 +20,8 @@ public class ClientData {
private final String officeName;
private final String externalId;
private final LocalDate joinedDate;
private final String imageKey;
private boolean imagePresent;
private List<OfficeLookup> allowedOffices = new ArrayList<OfficeLookup>();
@@ -36,13 +38,14 @@ public class ClientData {
}
displayNameBuilder.append(lastname);
return new ClientData(officeId, officeName, id, firstname, lastname, displayNameBuilder.toString(), null, null);
return new ClientData(officeId, officeName, id, firstname, lastname, displayNameBuilder.toString(), null, null, null);
}
public ClientData(final Long officeId, final String officeName,
final Long id, final String firstname, final String lastname,
final String displayName, final String externalId,
final LocalDate joinedDate) {
final LocalDate joinedDate,
final String imageKey) {
this.officeId = officeId;
this.officeName = officeName;
this.id = id;
@@ -60,6 +63,13 @@ public class ClientData {
this.externalId = externalId;
this.joinedDate = joinedDate;
this.imageKey = imageKey;
if (imageKey == null) {
imagePresent = false;
}else{
imagePresent = true;
}
}
public ClientData(final Long officeId, final LocalDate joinedDate,
@@ -74,6 +84,7 @@ public class ClientData {
this.externalId = null;
this.joinedDate = joinedDate;
this.allowedOffices = offices;
this.imageKey = null;
}
public Long getId() {
@@ -127,4 +138,20 @@ public class ClientData {
public String officeName() {
return this.officeName;
}
public String getImageKey() {
return imageKey;
}
public boolean isImagePresent() {
return imagePresent;
}
public void setImagePresent(boolean imagePresent) {
this.imagePresent = imagePresent;
}
}
@@ -122,7 +122,7 @@ public class GoogleGsonApiJsonSerializerService implements ApiJsonSerializerServ
private static final Set<String> CLIENT_DATA_PARAMETERS = new HashSet<String>(
Arrays.asList("id", "officeId", "officeName", "externalId",
"firstname", "lastname", "joinedDate", "displayName",
"clientOrBusinessName", "allowedOffices"));
"clientOrBusinessName", "allowedOffices", "imagePresent"));
private static final Set<String> CLIENT_IDENTIFIER_DATA_PARAMETERS = new HashSet<String>(
Arrays.asList("id", "clientId", "documentTypeId", "documentKey",
@@ -43,6 +43,9 @@ public class Client extends AbstractAuditableCustom<AppUser, Long> {
@Column(name = "is_deleted", nullable=false)
private boolean deleted = false;
@Column(name = "image_key", length=500)
private String imageKey;
public static Client newClient(Office clientOffice, String firstname, String lastname, LocalDate joiningDate, String externalId) {
return new Client(clientOffice, firstname, lastname, joiningDate, externalId);
@@ -207,6 +210,16 @@ public class Client extends AbstractAuditableCustom<AppUser, Long> {
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public String getImageKey() {
return imageKey;
}
public void setImageKey(String imageKey) {
this.imageKey = imageKey;
}
}
@@ -143,7 +143,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
public String clientSchema() {
return "c.office_id as officeId, o.name as officeName, c.id as id, c.firstname as firstname, c.lastname as lastname, c.display_name as displayName, " +
"c.external_id as externalId, c.joining_date as joinedDate from m_client c join m_office o on o.id = c.office_id " +
"c.external_id as externalId, c.joining_date as joinedDate, c.image_key as imagekey from m_client c join m_office o on o.id = c.office_id " +
" where o.hierarchy like ? and c.is_deleted=0 ";
}
@@ -161,11 +161,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
String displayName = rs.getString("displayName");
String externalId = rs.getString("externalId");
LocalDate joinedDate = JdbcSupport.getLocalDate(rs, "joinedDate");
String imageKey = rs.getString("imageKey");
String officeName = rs.getString("officeName");
return new ClientData(officeId, officeName, id, firstname,
lastname, displayName, externalId, joinedDate);
lastname, displayName, externalId, joinedDate,imageKey);
}
}
@@ -1,5 +1,7 @@
package org.mifosng.platform.client.service;
import java.io.InputStream;
import org.mifosng.platform.api.commands.ClientCommand;
import org.mifosng.platform.api.commands.ClientIdentifierCommand;
import org.mifosng.platform.api.commands.NoteCommand;
@@ -13,6 +15,12 @@ public interface ClientWritePlatformService {
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier updateClientDetails(ClientCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier deleteClientImage(Long clientId);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier addClientNote(NoteCommand command);
@@ -1,5 +1,9 @@
package org.mifosng.platform.client.service;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.lang.StringUtils;
import org.mifosng.platform.api.commands.ClientCommand;
import org.mifosng.platform.api.commands.ClientIdentifierCommand;
@@ -11,9 +15,12 @@ import org.mifosng.platform.client.domain.ClientIdentifierRepository;
import org.mifosng.platform.client.domain.ClientRepository;
import org.mifosng.platform.client.domain.Note;
import org.mifosng.platform.client.domain.NoteRepository;
import org.mifosng.platform.common.ApplicationConstants;
import org.mifosng.platform.common.FileUtils;
import org.mifosng.platform.exceptions.ClientIdentifierNotFoundException;
import org.mifosng.platform.exceptions.ClientNotFoundException;
import org.mifosng.platform.exceptions.CodeValueNotFoundException;
import org.mifosng.platform.exceptions.DocumentManagementException;
import org.mifosng.platform.exceptions.DuplicateClientIdentifierException;
import org.mifosng.platform.exceptions.NoteNotFoundException;
import org.mifosng.platform.exceptions.OfficeNotFoundException;
@@ -319,4 +326,65 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
return new EntityIdentifier(clientIdentifierId);
}
@Transactional
@Override
public EntityIdentifier saveOrUpdateClientImage(Long clientId,
String imageName, InputStream inputStream) {
try {
final Client client = this.clientRepository.findOne(clientId);
if (client == null || client.isDeleted()) {
throw new ClientNotFoundException(clientId);
}
String imageUploadLocation = FileUtils
.generateImageParentDirectory(
ApplicationConstants.IMAGE_MANAGEMENT_ENTITY.CLIENTS,
clientId);
// delete previous image from the file system
if (StringUtils.isNotEmpty(client.getImageKey())) {
FileUtils.deleteImage(
ApplicationConstants.IMAGE_MANAGEMENT_ENTITY.CLIENTS,
clientId, client.getImageKey());
}
/** Recursively create the directory if it does not exist **/
if (!new File(imageUploadLocation).isDirectory()) {
new File(imageUploadLocation).mkdirs();
}
String imageLocation = FileUtils.saveToFileSystem(inputStream,
imageUploadLocation, imageName);
client.setImageKey(imageLocation);
this.clientRepository.save(client);
return new EntityIdentifier(clientId);
} catch (IOException ioe) {
logger.error(ioe.getMessage(), ioe);
throw new DocumentManagementException(imageName);
}
}
@Transactional
@Override
public EntityIdentifier deleteClientImage(Long clientId) {
final Client client = this.clientRepository.findOne(clientId);
if (client == null || client.isDeleted()) {
throw new ClientNotFoundException(clientId);
}
// delete image from the file system
if (StringUtils.isNotEmpty(client.getImageKey())) {
FileUtils.deleteImage(
ApplicationConstants.IMAGE_MANAGEMENT_ENTITY.CLIENTS,
clientId, client.getImageKey());
}
client.setImageKey(null);
this.clientRepository.save(client);
return new EntityIdentifier(clientId);
}
}
@@ -7,8 +7,11 @@ public class ApplicationConstants {
// TODO:Vishwas Need to move these settings to the Database
public static final Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5;
// TODO:Vishwas Need to move these settings to the Database
public static final Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1;
/*** Entities for document Management **/
public static enum DOCUMENT_MANAGEMENT_ENTITIES {
public static enum DOCUMENT_MANAGEMENT_ENTITY {
CLIENTS, CLIENT_IDENTIFIERS, STAFF, LOANS, SAVINGS;
@Override
public String toString() {
@@ -16,4 +19,13 @@ public class ApplicationConstants {
}
}
/*** Entities for image Management **/
public static enum IMAGE_MANAGEMENT_ENTITY {
CLIENTS, STAFF;
@Override
public String toString() {
return name().toString().toLowerCase();
}
}
}
@@ -5,12 +5,23 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.mifosng.platform.api.data.ApiParameterError;
import org.mifosng.platform.exceptions.DocumentManagementException;
import org.mifosng.platform.exceptions.ImageUploadException;
import org.mifosng.platform.exceptions.PlatformApiDataValidationException;
import org.mifosng.platform.infrastructure.ThreadLocalContextUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FileUtils {
private final static Logger logger = LoggerFactory
.getLogger(FileUtils.class);
public static final String MIFOSX_BASE_DIR = System
.getProperty("user.home") + File.separator + ".mifosx";
@@ -42,7 +53,7 @@ public class FileUtils {
}
/**
* Generate the directory path for storing the new document Type
* Generate the directory path for storing the new document
*
* @param entityType
* @param entityId
@@ -54,8 +65,26 @@ public class FileUtils {
+ File.separator
+ ThreadLocalContextUtil.getTenant().getName()
.replaceAll(" ", "").trim() + File.separator
+ entityType + File.separator + entityId + File.separator
+ FileUtils.generateRandomString();
+ "documents" + File.separator + entityType + File.separator
+ entityId + File.separator + FileUtils.generateRandomString();
}
/**
* Generate directory path for storing new Image
*
* @param entityType
* @param entityId
* @return
*/
public static String generateImageParentDirectory(
ApplicationConstants.IMAGE_MANAGEMENT_ENTITY entityType,
Long entityId) {
return FileUtils.MIFOSX_BASE_DIR
+ File.separator
+ ThreadLocalContextUtil.getTenant().getName()
.replaceAll(" ", "").trim() + File.separator + "images"
+ File.separator + entityType.toString() + File.separator
+ entityId;
}
/**
@@ -78,4 +107,69 @@ public class FileUtils {
out.close();
return fileLocation;
}
/**
* @param fileSize
* @param name
*/
public static void validateFileSizeWithinPermissibleRange(Long fileSize,
String name, int maxFileSize) {
/**
* Using Content-Length gives me size of the entire request, which is
* good enough for now for a fast fail as the length of the rest of the
* content i.e name and description while compared to the uploaded file
* size is negligible
**/
if (fileSize != null && ((fileSize / (1024 * 1024)) > maxFileSize)) {
throw new DocumentManagementException(name, fileSize, maxFileSize);
}
}
/**
* Validates that passed in Mime type maps to known image mime types
*
* @param mimeType
*/
public static void validateImageMimeType(String mimeType) {
if (!(mimeType.equalsIgnoreCase("image/gif")
|| mimeType.equalsIgnoreCase("image/jpeg") || mimeType
.equalsIgnoreCase("image/png"))) {
throw new ImageUploadException();
}
}
public static void validateClientImageNotEmpty(String imageFileName) {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
if (imageFileName == null) {
StringBuilder validationErrorCode = new StringBuilder(
"validation.msg.clientImage.cannot.be.blank");
StringBuilder defaultEnglishMessage = new StringBuilder(
"The parameter image cannot be blank.");
ApiParameterError error = ApiParameterError.parameterError(
validationErrorCode.toString(),
defaultEnglishMessage.toString(), "image");
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(
"validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors);
}
}
/**
* @param entityType
* @param entityId
* @param location
*/
public static void deleteImage(
ApplicationConstants.IMAGE_MANAGEMENT_ENTITY entityType,
Long entityId, String location) {
File fileToBeDeleted = new File(location);
boolean fileDeleted = fileToBeDeleted.delete();
if (!fileDeleted) {
// no need to throw an Error, simply log a warning
logger.warn("Unable to delete image associated with "
+ entityType.toString() + " with Id " + entityId);
}
}
}
@@ -193,7 +193,7 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements
}
private static boolean checkValidEntityType(String entityType) {
for (ApplicationConstants.DOCUMENT_MANAGEMENT_ENTITIES entities : ApplicationConstants.DOCUMENT_MANAGEMENT_ENTITIES
for (ApplicationConstants.DOCUMENT_MANAGEMENT_ENTITY entities : ApplicationConstants.DOCUMENT_MANAGEMENT_ENTITY
.values()) {
if (entities.name().equalsIgnoreCase(entityType)) {
return true;
@@ -1,6 +1,5 @@
package org.mifosng.platform.exceptions;
import org.mifosng.platform.common.ApplicationConstants;
public class DocumentManagementException extends
AbstractPlatformDomainRuleException {
@@ -10,12 +9,13 @@ public class DocumentManagementException extends
+ name + " due to a File system / Amazon S3 issue", name);
}
public DocumentManagementException(final String name, final Long fileSize) {
public DocumentManagementException(final String name, final Long fileSize,
final int maxFileSize) {
super("error.msg.document.file.too.big",
"Unable to save the document with name" + name
+ " since its file Size of " + fileSize / (1024 * 1024)
+ " MB exceeds the max permissable file size of "
+ ApplicationConstants.MAX_FILE_UPLOAD_SIZE_IN_MB
+ maxFileSize
+ " MB", name, fileSize);
}
@@ -0,0 +1,15 @@
package org.mifosng.platform.exceptions;
import org.mifosng.platform.common.ApplicationConstants;
public class ImageNotFoundException extends AbstractPlatformDomainRuleException {
public ImageNotFoundException(
ApplicationConstants.IMAGE_MANAGEMENT_ENTITY entityType,
Long entityId) {
super("error.msg.entity.image.invalid", "Image for entity "
+ entityType.toString() + " with Identifier " + entityId
+ " does not exist", entityType.toString(), entityId);
}
}
@@ -0,0 +1,10 @@
package org.mifosng.platform.exceptions;
public class ImageUploadException extends AbstractPlatformDomainRuleException {
public ImageUploadException() {
super("error.msg.image.type.upload",
"Only image files of type GIF,PNG and JPG are allowed ");
}
}