diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/DocumentManagementApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/DocumentManagementApiResource.java new file mode 100755 index 000000000..456c84187 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/DocumentManagementApiResource.java @@ -0,0 +1,187 @@ +package org.mifosng.platform.api; + +import java.io.File; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +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; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.UriInfo; + +import org.mifosng.platform.api.commands.DocumentCommand; +import org.mifosng.platform.api.data.DocumentData; +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.documentmanagement.service.DocumentReadPlatformService; +import org.mifosng.platform.documentmanagement.service.DocumentWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import com.sun.jersey.core.header.FormDataContentDisposition; +import com.sun.jersey.multipart.FormDataBodyPart; +import com.sun.jersey.multipart.FormDataParam; + +@Path("{entityType}/{entityId}/documents") +@Component +@Scope("singleton") +public class DocumentManagementApiResource { + + @Autowired + private DocumentReadPlatformService documentReadPlatformService; + + @Autowired + private DocumentWritePlatformService documentWritePlatformService; + + @Autowired + private ApiJsonSerializerService apiJsonSerializerService; + + private static final Set typicalResponseParameters = new HashSet( + Arrays.asList("id", "parentEntityType", "parentEntityId", "name", + "fileName", "type", "size", "description")); + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retreiveAllDocuments(@Context final UriInfo uriInfo, + @PathParam("entityType") String entityType, + @PathParam("entityId") Long entityId) { + + Set responseParameters = ApiParameterHelper + .extractFieldsForResponseIfProvided(uriInfo + .getQueryParameters()); + if (responseParameters.isEmpty()) { + responseParameters.addAll(typicalResponseParameters); + } + boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo + .getQueryParameters()); + + Collection documentDatas = this.documentReadPlatformService + .retrieveAllDocuments(entityType, entityId); + + return this.apiJsonSerializerService.serializeDocumentDataToJson( + prettyPrint, responseParameters, documentDatas); + } + + @POST + @Consumes({ MediaType.MULTIPART_FORM_DATA }) + @Produces({ MediaType.APPLICATION_JSON }) + public Response createDocument(@PathParam("entityType") String entityType, + @PathParam("entityId") Long entityId, + @FormDataParam("file") InputStream inputStream, + @FormDataParam("file") FormDataContentDisposition fileDetails, + @FormDataParam("file") FormDataBodyPart bodyPart, + @HeaderParam("Content-Length") Long fileSize) { + + // TODO: Using Content-Length gives me size of the entire request, need + // to filter out only the size of the file + + // TODO: Need to add a Fast fail for checking size of file depending on + // the size sent by client, also need to have a backup and stop reading + // from stream after size is reached to protect against malicious + // clients + DocumentCommand documentCommand = new DocumentCommand(null, null, + entityType, entityId, "test", fileDetails.getFileName(), + fileSize, bodyPart.getMediaType().toString(), "description", + null); + + bodyPart.getMediaType(); + + Long documentId = this.documentWritePlatformService.createDocument( + documentCommand, inputStream); + + return Response.ok().entity(new EntityIdentifier(documentId)).build(); + } + + @GET + @Path("{documentId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String getDocument(@PathParam("entityType") String entityType, + @PathParam("entityId") Long entityId, + @PathParam("documentId") Long documentId, + @Context final UriInfo uriInfo) { + + Set responseParameters = ApiParameterHelper + .extractFieldsForResponseIfProvided(uriInfo + .getQueryParameters()); + if (responseParameters.isEmpty()) { + responseParameters.addAll(typicalResponseParameters); + } + + boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo + .getQueryParameters()); + + DocumentData documentData = this.documentReadPlatformService + .retrieveDocument(entityType, entityId, documentId); + + return this.apiJsonSerializerService.serializeDocumentDataToJson( + prettyPrint, responseParameters, documentData); + } + + @GET + @Path("{documentId}/attachment") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public Response downloadFile(@PathParam("entityType") String entityType, + @PathParam("entityId") Long entityId, + @PathParam("documentId") Long documentId) { + + DocumentData documentData = this.documentReadPlatformService + .retrieveDocument(entityType, entityId, documentId); + File file = new File(documentData.getLocation()); + ResponseBuilder response = Response.ok(file); + response.header("Content-Disposition", "attachment; filename=\"" + + documentData.getFileName() + "\""); + response.header("Content-Type", documentData.getType()); + return response.build(); + } + + @PUT + @Path("{documentId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public Response updateDocument(@PathParam("entityType") String entityType, + @PathParam("entityId") Long entityId, + @PathParam("documentId") Long documentId) { + + // TODO + // EntityIdentifier identifier = + // this.documentWritePlatformService.updateDocument(documentCommand, + // inputStream, fileDetails) + + return Response.ok().entity(null).build(); + } + + @DELETE + @Path("{documentId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public Response deleteDocument(@PathParam("entityType") String entityType, + @PathParam("entityId") Long entityId, + @PathParam("documentId") Long documentId) { + DocumentCommand documentCommand = new DocumentCommand(null, documentId, + entityType, entityId, null, null, null, null, null, null); + + EntityIdentifier documentIdentifier = this.documentWritePlatformService + .deleteDocument(documentCommand); + + return Response.ok(documentIdentifier).build(); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DocumentCommand.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DocumentCommand.java new file mode 100755 index 000000000..6677d3769 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DocumentCommand.java @@ -0,0 +1,119 @@ +package org.mifosng.platform.api.commands; + +import java.util.Set; + +/** + * Immutable command for creating or updating details of a client identifier. + */ +public class DocumentCommand { + + private final Long id; + private final String parentEntityType; + private final Long parentEntityId; + private final String name; + private final String description; + + private String fileName; + private Long size; + private String type; + private String location; + + private final Set modifiedParameters; + + public DocumentCommand(final Set modifiedParameters, final Long id, + final String parentEntityType, final Long parentEntityId, + final String name, final String fileName, Long size, String type, + final String description, String location) { + this.modifiedParameters = modifiedParameters; + this.id = id; + this.parentEntityType = parentEntityType; + this.parentEntityId = parentEntityId; + this.name = name; + this.fileName = fileName; + this.size = size; + this.type = type; + this.description = description; + this.location = location; + } + + public Long getId() { + return id; + } + + public String getParentEntityType() { + return parentEntityType; + } + + public Long getParentEntityId() { + return parentEntityId; + } + + public String getName() { + return name; + } + + public String getFileName() { + return fileName; + } + + public Long getSize() { + return size; + } + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public String getLocation() { + return location; + } + + public Set getModifiedParameters() { + return modifiedParameters; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public void setSize(Long size) { + this.size = size; + } + + public void setType(String type) { + this.type = type; + } + + public void setLocation(String location) { + this.location = location; + } + + public boolean isNameChanged() { + return this.modifiedParameters.contains("name"); + } + + public boolean isFileNameChanged() { + return this.modifiedParameters.contains("fileName"); + } + + public boolean isSizeChanged() { + return this.modifiedParameters.contains("size"); + } + + public boolean isFileTypeChanged() { + return this.modifiedParameters.contains("type"); + } + + public boolean isDescriptionChanged() { + return this.modifiedParameters.contains("description"); + } + + public boolean isLocationChanged() { + return this.modifiedParameters.contains("location"); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/DocumentData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/DocumentData.java new file mode 100755 index 000000000..439d24d78 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/DocumentData.java @@ -0,0 +1,67 @@ +package org.mifosng.platform.api.data; + + +public class DocumentData { + + private final Long id; + private final String parentEntityType; + private final Long parentEntityId; + private final String name; + private final String fileName; + private final Long size; + private final String type; + private final String description; + private final String location; + + public DocumentData(final Long id, final String parentEntityType, + final Long parentEntityId, final String name, + final String fileName, final Long size, final String type, + final String description, final String location) { + this.id = id; + this.parentEntityType = parentEntityType; + this.parentEntityId = parentEntityId; + this.name = name; + this.fileName = fileName; + this.size = size; + this.type = type; + this.description = description; + this.location = location; + } + + public Long getId() { + return id; + } + + public String getParentEntityType() { + return parentEntityType; + } + + public Long getParentEntityId() { + return parentEntityId; + } + + public String getName() { + return name; + } + + public String getFileName() { + return fileName; + } + + public Long getSize() { + return size; + } + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public String getLocation() { + return location; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java index d29e696d1..bacaa8995 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java @@ -11,6 +11,7 @@ import org.mifosng.platform.api.commands.DepositAccountWithdrawalCommand; import org.mifosng.platform.api.commands.DepositProductCommand; import org.mifosng.platform.api.commands.DepositStateTransitionApprovalCommand; import org.mifosng.platform.api.commands.DepositStateTransitionCommand; +import org.mifosng.platform.api.commands.DocumentCommand; import org.mifosng.platform.api.commands.FundCommand; import org.mifosng.platform.api.commands.GroupCommand; import org.mifosng.platform.api.commands.LoanApplicationCommand; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java index 8184399cf..303b0326c 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java @@ -1467,4 +1467,5 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService { return new ClientIdentifierCommand(modifiedParameters,resourceIdentifier,clientId,documentTypeId,documentKey,documentDescription); } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiJsonSerializerService.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiJsonSerializerService.java index 6bb839384..ff2d8ab5a 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiJsonSerializerService.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiJsonSerializerService.java @@ -15,6 +15,7 @@ import org.mifosng.platform.api.data.ConfigurationData; import org.mifosng.platform.api.data.DatatableData; import org.mifosng.platform.api.data.DepositAccountData; import org.mifosng.platform.api.data.DepositProductData; +import org.mifosng.platform.api.data.DocumentData; import org.mifosng.platform.api.data.EntityIdentifier; import org.mifosng.platform.api.data.FundData; import org.mifosng.platform.api.data.GenericResultsetData; @@ -122,4 +123,9 @@ public interface ApiJsonSerializerService { String serializeClientIdentifierDataToJson(boolean prettyPrint, Set responseParameters, Collection clientIdentifiers); String serializeClientIdentifierDataToJson(boolean prettyPrint, Set responseParameters, ClientIdentifierData clientIdentifierData); + + String serializeDocumentDataToJson(boolean prettyPrint, Set responseParameters, Collection documentDatas); + + String serializeDocumentDataToJson(boolean prettyPrint, Set responseParameters, DocumentData documentData); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/GoogleGsonApiJsonSerializerService.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/GoogleGsonApiJsonSerializerService.java index 46db3d9d7..851f67c65 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/GoogleGsonApiJsonSerializerService.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/GoogleGsonApiJsonSerializerService.java @@ -17,6 +17,7 @@ import org.mifosng.platform.api.data.ConfigurationData; import org.mifosng.platform.api.data.DatatableData; import org.mifosng.platform.api.data.DepositAccountData; import org.mifosng.platform.api.data.DepositProductData; +import org.mifosng.platform.api.data.DocumentData; import org.mifosng.platform.api.data.EntityIdentifier; import org.mifosng.platform.api.data.FundData; import org.mifosng.platform.api.data.GenericResultsetData; @@ -126,6 +127,10 @@ public class GoogleGsonApiJsonSerializerService implements ApiJsonSerializerServ private static final Set CLIENT_IDENTIFIER_DATA_PARAMETERS = new HashSet( Arrays.asList("id", "clientId", "documentTypeId", "documentKey", "description", "documentTypeName" ,"allowedDocumentTypes")); + + private static final Set DOCUMENT_DATA_PARAMETERS = new HashSet( + Arrays.asList("id", "parentEntityType", "parentEntityId", "name", + "fileName", "type", "size", "description")); private static final Set CLIENT_ACCOUNTS_DATA_PARAMETERS = new HashSet( Arrays.asList("pendingApprovalLoans", "awaitingDisbursalLoans", @@ -652,4 +657,25 @@ public class GoogleGsonApiJsonSerializerService implements ApiJsonSerializerServ CLIENT_IDENTIFIER_DATA_PARAMETERS, prettyPrint, responseParameters); return helper.serializedJsonFrom(gsonDeserializer, clientIdentifierData); } + + @Override + public String serializeDocumentDataToJson(boolean prettyPrint, + Set responseParameters, + Collection documentDatas) { + final Gson gsonDeserializer = helper + .createGsonBuilderWithParameterExclusionSerializationStrategy( + DOCUMENT_DATA_PARAMETERS, prettyPrint, responseParameters); + return helper.serializedJsonFrom(gsonDeserializer, + documentDatas.toArray(new DocumentData[documentDatas.size()])); + } + + @Override + public String serializeDocumentDataToJson(boolean prettyPrint, + Set responseParameters, + DocumentData documentData) { + final Gson gsonDeserializer = helper + .createGsonBuilderWithParameterExclusionSerializationStrategy( + DOCUMENT_DATA_PARAMETERS, prettyPrint, responseParameters); + return helper.serializedJsonFrom(gsonDeserializer, documentData); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/common/ApplicationConstants.java b/mifosng-provider/src/main/java/org/mifosng/platform/common/ApplicationConstants.java index 4cc9567ab..69ceeb597 100755 --- a/mifosng-provider/src/main/java/org/mifosng/platform/common/ApplicationConstants.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/common/ApplicationConstants.java @@ -4,4 +4,13 @@ public class ApplicationConstants { public static final Long CLIENT_IDENTITY_CODE_ID = 9L; + /*** Entities for document Management **/ + public static enum DOCUMENT_MANAGEMENT_ENTITIES { + CLIENTs, STAFF, LOANS; + @Override + public String toString() { + return name().toString().toLowerCase(); + } + } + } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/common/FileUtils.java b/mifosng-provider/src/main/java/org/mifosng/platform/common/FileUtils.java new file mode 100755 index 000000000..7dc985503 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/common/FileUtils.java @@ -0,0 +1,81 @@ +package org.mifosng.platform.common; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Random; + +import org.mifosng.platform.infrastructure.ThreadLocalContextUtil; + +public class FileUtils { + + public static final String MIFOSX_BASE_DIR = System + .getProperty("user.home") + File.separator + ".mifosx"; + + public static Random random = new Random(); + + /** + * Generate a random String + * + * @return + */ + public static String generateRandomString() { + String characters = "abcdefghijklmnopqrstuvwxyz123456789"; + int length = generateRandomNumber(); + char[] text = new char[length]; + for (int i = 0; i < length; i++) { + text[i] = characters.charAt(random.nextInt(characters.length())); + } + return new String(text); + } + + /** + * Generate a random number between 5 to 16 + * + * @return + */ + public static int generateRandomNumber() { + Random randomGenerator = new Random(); + return randomGenerator.nextInt(11) + 5; + } + + /** + * Generate the directory path for storing the new document Type + * + * @param entityType + * @param entityId + * @return + */ + public static String generateFileParentDirectory(String entityType, + Long entityId) { + return FileUtils.MIFOSX_BASE_DIR + + File.separator + + ThreadLocalContextUtil.getTenant().getName() + .replaceAll(" ", "").trim() + File.separator + + entityType + File.separator + entityId + File.separator + + FileUtils.generateRandomString(); + } + + /** + * @param uploadedInputStream + * @param uploadedFileLocation + * @return + * @throws IOException + */ + public static String saveToFileSystem(InputStream uploadedInputStream, + String uploadedFileLocation, String fileName) throws IOException { + String fileLocation = uploadedFileLocation + File.separator + fileName; + OutputStream out = new FileOutputStream(new File(fileLocation)); + int read = 0; + byte[] bytes = new byte[1024]; + + while ((read = uploadedInputStream.read(bytes)) != -1) { + out.write(bytes, 0, read); + } + out.flush(); + out.close(); + return fileLocation; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/domain/Document.java b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/domain/Document.java new file mode 100755 index 000000000..ac822cb80 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/domain/Document.java @@ -0,0 +1,94 @@ +package org.mifosng.platform.documentmanagement.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.platform.api.commands.DocumentCommand; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "m_document") +public class Document extends AbstractAuditableCustom { + + @SuppressWarnings("unused") + @Column(name = "parent_entity_type", length = 50) + private String parentEntityType; + + @SuppressWarnings("unused") + @Column(name = "parent_entity_id", length = 1000) + private Long parentEntityId; + + @SuppressWarnings("unused") + @Column(name = "name", length = 250) + private String name; + + @SuppressWarnings("unused") + @Column(name = "file_name", length = 250) + private String fileName; + + @SuppressWarnings("unused") + @Column(name = "size") + private Long size; + + @SuppressWarnings("unused") + @Column(name = "type", length = 50) + private String type; + + @SuppressWarnings("unused") + @Column(name = "description", length = 1000) + private String description; + + @SuppressWarnings("unused") + @Column(name = "location", length = 500) + private String location; + + public Document() { + } + + public static Document createNew(String parentEntityType, + Long parentEntityId, String name, String fileName, Long size, + String type, String description, String location) { + return new Document(parentEntityType, parentEntityId, name, fileName, + size, type, description, location); + } + + private Document(String parentEntityType, Long parentEntityId, String name, + String fileName, Long size, String type, String description, + String location) { + this.parentEntityType = StringUtils.defaultIfEmpty(parentEntityType, + null); + this.parentEntityId = parentEntityId; + this.name = StringUtils.defaultIfEmpty(name, null); + this.fileName = StringUtils.defaultIfEmpty(fileName, null); + this.size = size; + this.type = StringUtils.defaultIfEmpty(type, null); + this.description = StringUtils.defaultIfEmpty(description, null); + this.location = StringUtils.defaultIfEmpty(location, null); + } + + public void update(final DocumentCommand command) { + if (command.isDescriptionChanged()) { + this.description = command.getDescription(); + } + if (command.isFileNameChanged()) { + this.fileName = command.getFileName(); + } + if (command.isFileTypeChanged()) { + this.type = command.getType(); + } + if (command.isLocationChanged()) { + this.location = command.getLocation(); + } + if (command.isNameChanged()) { + this.name = command.getName(); + } + if (command.isSizeChanged()) { + this.size = command.getSize(); + } + + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/domain/DocumentRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/domain/DocumentRepository.java new file mode 100755 index 000000000..aaa83940c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/domain/DocumentRepository.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.documentmanagement.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface DocumentRepository extends + JpaRepository, JpaSpecificationExecutor { + // no added behaviour +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentCommandValidator.java new file mode 100755 index 000000000..d6fa60b0f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentCommandValidator.java @@ -0,0 +1,84 @@ +package org.mifosng.platform.documentmanagement.service; + +import java.util.ArrayList; +import java.util.List; + +import org.mifosng.platform.DataValidatorBuilder; +import org.mifosng.platform.api.commands.DocumentCommand; +import org.mifosng.platform.api.data.ApiParameterError; +import org.mifosng.platform.exceptions.PlatformApiDataValidationException; + +public class DocumentCommandValidator { + + private final DocumentCommand command; + + public DocumentCommandValidator(DocumentCommand command) { + this.command = command; + } + + public void validateForUpdate() { + List dataValidationErrors = new ArrayList(); + + DataValidatorBuilder baseDataValidator = new DataValidatorBuilder( + dataValidationErrors).resource("document"); + + baseDataValidator.reset().parameter("name").value(command.getName()) + .ignoreIfNull().notBlank(); + /*** TODO:Validate for max length of File to be stored **/ + baseDataValidator.reset().parameter("size").value(command.getSize()) + .ignoreIfNull().integerGreaterThanZero(); + baseDataValidator.reset().parameter("fileName") + .value(command.getFileName()).ignoreIfNull().notBlank() + .integerGreaterThanZero(); + baseDataValidator.reset().parameter("location") + .value(command.getLocation()).ignoreIfNull().notBlank() + .integerGreaterThanZero(); + /*** TODO: Validate for permissable types ***/ + baseDataValidator.reset().parameter("type").value(command.getName()) + .ignoreIfNull().notBlank().notExceedingLengthOf(250); + baseDataValidator.reset().parameter("description") + .value(command.getName()).ignoreIfNull() + .notExceedingLengthOf(250); + + baseDataValidator.reset().anyOfNotNull(command.getName(), + command.getFileName(), command.getDescription(), + command.getLocation(), command.getSize()); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + } + + public void validateForCreate() { + List dataValidationErrors = new ArrayList(); + + DataValidatorBuilder baseDataValidator = new DataValidatorBuilder( + dataValidationErrors).resource("document"); + baseDataValidator.reset().parameter("parentEntityType") + .value(command.getParentEntityType()).notBlank() + .notExceedingLengthOf(50); + baseDataValidator.reset().parameter("parentEntityId") + .value(command.getParentEntityId()).integerGreaterThanZero(); + baseDataValidator.reset().parameter("name").value(command.getName()) + .notBlank().notExceedingLengthOf(250); + /*** TODO:Validate for max length of File to be stored **/ + baseDataValidator.reset().parameter("size").value(command.getSize()) + .integerGreaterThanZero(); + baseDataValidator.reset().parameter("fileName") + .value(command.getFileName()).notBlank() + .notExceedingLengthOf(250); + /*** TODO: Validate for permissable types ***/ + baseDataValidator.reset().parameter("type").value(command.getName()) + .notBlank().notExceedingLengthOf(250); + baseDataValidator.reset().parameter("description") + .value(command.getName()).notExceedingLengthOf(250); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentReadPlatformService.java new file mode 100755 index 000000000..7f0624468 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentReadPlatformService.java @@ -0,0 +1,15 @@ +package org.mifosng.platform.documentmanagement.service; + +import java.util.Collection; + +import org.mifosng.platform.api.data.DocumentData; + +public interface DocumentReadPlatformService { + + Collection retrieveAllDocuments(String entityType, + Long entityId); + + DocumentData retrieveDocument(String entityType, Long entityId, + Long documentId); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentReadPlatformServiceImpl.java new file mode 100755 index 000000000..c72f4aeec --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentReadPlatformServiceImpl.java @@ -0,0 +1,117 @@ +package org.mifosng.platform.documentmanagement.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; + +import org.mifosng.platform.api.data.DocumentData; +import org.mifosng.platform.client.service.ClientReadPlatformService; +import org.mifosng.platform.exceptions.DocumentNotFoundException; +import org.mifosng.platform.infrastructure.JdbcSupport; +import org.mifosng.platform.infrastructure.TenantAwareRoutingDataSource; +import org.mifosng.platform.loan.service.LoanReadPlatformService; +import org.mifosng.platform.organisation.service.OfficeReadPlatformService; +import org.mifosng.platform.security.PlatformSecurityContext; +import org.mifosng.platform.staff.service.StaffReadPlatformService; +import org.mifosng.platform.user.service.AppUserReadPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +public class DocumentReadPlatformServiceImpl implements + DocumentReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + + // TODO: use these services to check if passed n entities for document + // management exists (and ensure appropriate errors are thrown) + private final PlatformSecurityContext context; + private final OfficeReadPlatformService officeReadPlatformService; + private final ClientReadPlatformService clientReadPlatformService; + private final LoanReadPlatformService loanReadPlatformService; + private final StaffReadPlatformService staffReadPlatformService; + private final AppUserReadPlatformService appUserReadPlatformService; + + @Autowired + public DocumentReadPlatformServiceImpl( + final PlatformSecurityContext context, + final TenantAwareRoutingDataSource dataSource, + final OfficeReadPlatformService officeReadPlatformService, + final AppUserReadPlatformService appUserReadPlatformService, + final ClientReadPlatformService clientReadPlatformService, + final LoanReadPlatformService loanReadPlatformService, + final StaffReadPlatformService staffReadPlatformService) { + this.context = context; + this.officeReadPlatformService = officeReadPlatformService; + this.appUserReadPlatformService = appUserReadPlatformService; + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.clientReadPlatformService = clientReadPlatformService; + this.loanReadPlatformService = loanReadPlatformService; + this.staffReadPlatformService = staffReadPlatformService; + } + + @Override + public Collection retrieveAllDocuments(String entityType, + Long entityId) { + // TODO verify if the entities are valid and a user + // has data + // scope for the particular entities + DocumentMapper mapper = new DocumentMapper(); + String sql = "select " + mapper.schema() + " order by d.id"; + return this.jdbcTemplate.query(sql, mapper, new Object[] { entityType, + entityId }); + } + + @Override + public DocumentData retrieveDocument(String entityType, Long entityId, + Long documentId) { + try { + // TODO verify if the entities are valid and a + // user has data + // scope for the particular entities + DocumentMapper mapper = new DocumentMapper(); + String sql = "select " + mapper.schema() + " and d.id=? "; + return this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { + entityType, entityId, documentId }); + } catch (EmptyResultDataAccessException e) { + throw new DocumentNotFoundException(entityType, entityId, + documentId); + } + } + + private static final class DocumentMapper implements + RowMapper { + + public DocumentMapper() { + } + + public String schema() { + return "d.id as id, d.parent_entity_type as parentEntityType, d.parent_entity_id as parentEntityId, d.name as name, " + + " d.file_name as fileName, d.size as fileSize, d.type as fileType, " + + " d.description as description, d.location as location" + + " from m_document d where d.parent_entity_type=? and d.parent_entity_id=?"; + } + + @Override + public DocumentData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = JdbcSupport.getLong(rs, "id"); + Long parentEntityId = JdbcSupport.getLong(rs, "parentEntityId"); + Long fileSize = JdbcSupport.getLong(rs, "fileSize"); + String parentEntityType = rs.getString("parentEntityType"); + String name = rs.getString("name"); + String fileName = rs.getString("fileName"); + String fileType = rs.getString("fileType"); + String description = rs.getString("description"); + String location = rs.getString("location"); + + return new DocumentData(id, parentEntityType, parentEntityId, name, + fileName, fileSize, fileType, description, location); + } + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentWritePlatformService.java new file mode 100755 index 000000000..2ee0f3e7e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentWritePlatformService.java @@ -0,0 +1,21 @@ +package org.mifosng.platform.documentmanagement.service; + +import java.io.InputStream; + +import org.mifosng.platform.api.commands.DocumentCommand; +import org.mifosng.platform.api.data.EntityIdentifier; +import org.springframework.security.access.prepost.PreAuthorize; + +public interface DocumentWritePlatformService { + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')") + Long createDocument(DocumentCommand documentCommand, InputStream inputStream); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')") + EntityIdentifier updateDocument(DocumentCommand documentCommand, + InputStream inputStream); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')") + EntityIdentifier deleteDocument(DocumentCommand documentCommand); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java new file mode 100755 index 000000000..97f668b3e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java @@ -0,0 +1,200 @@ +package org.mifosng.platform.documentmanagement.service; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import org.mifosng.platform.api.commands.DocumentCommand; +import org.mifosng.platform.api.data.EntityIdentifier; +import org.mifosng.platform.client.domain.ClientRepository; +import org.mifosng.platform.common.ApplicationConstants; +import org.mifosng.platform.common.FileUtils; +import org.mifosng.platform.documentmanagement.domain.Document; +import org.mifosng.platform.documentmanagement.domain.DocumentRepository; +import org.mifosng.platform.exceptions.DocumentNotFoundException; +import org.mifosng.platform.exceptions.DocumentSaveException; +import org.mifosng.platform.exceptions.InvalidEntityTypeForDocumentManagementException; +import org.mifosng.platform.exceptions.PlatformDataIntegrityException; +import org.mifosng.platform.loan.domain.LoanRepository; +import org.mifosng.platform.organisation.domain.OfficeRepository; +import org.mifosng.platform.security.PlatformSecurityContext; +import org.mifosng.platform.staff.domain.StaffRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DocumentWritePlatformServiceJpaRepositoryImpl implements + DocumentWritePlatformService { + + private final static Logger logger = LoggerFactory + .getLogger(DocumentWritePlatformServiceJpaRepositoryImpl.class); + + // TODO: Use these services to ensure that the appropriate entities exist, + // are active, check data scope for user etc + private final PlatformSecurityContext context; + private final ClientRepository clientRepository; + private final OfficeRepository officeRepository; + private final LoanRepository loanRepository; + private final StaffRepository staffRepository; + private final DocumentRepository documentRepository; + + @Autowired + public DocumentWritePlatformServiceJpaRepositoryImpl( + final PlatformSecurityContext context, + final ClientRepository clientRepository, + final DocumentRepository documentRepository, + final OfficeRepository officeRepository, + final LoanRepository loanRepository, + final StaffRepository staffRepository) { + this.context = context; + this.clientRepository = clientRepository; + this.documentRepository = documentRepository; + this.officeRepository = officeRepository; + this.loanRepository = loanRepository; + this.staffRepository = staffRepository; + } + + @Transactional + @Override + public Long createDocument(DocumentCommand documentCommand, + InputStream inputStream) { + try { + DocumentCommandValidator validator = new DocumentCommandValidator( + documentCommand); + + validateParentEntityType(documentCommand); + + validator.validateForCreate(); + + String fileUploadLocation = FileUtils.generateFileParentDirectory( + documentCommand.getParentEntityType(), + documentCommand.getParentEntityId()); + + /** Recursively create the directory if it does not exist **/ + if (!new File(fileUploadLocation).isDirectory()) { + new File(fileUploadLocation).mkdirs(); + } + + // TODO check if entity id is valid and within data scope for the + // user + // TODO replace file system appender with an Amazon S3 appender + String fileLocation = FileUtils.saveToFileSystem(inputStream, + fileUploadLocation, documentCommand.getFileName()); + + Document document = Document.createNew( + documentCommand.getParentEntityType(), + documentCommand.getParentEntityId(), + documentCommand.getName(), documentCommand.getFileName(), + documentCommand.getSize(), documentCommand.getType(), + documentCommand.getDescription(), fileLocation); + + this.documentRepository.save(document); + + return document.getId(); + } catch (DataIntegrityViolationException dve) { + logger.error(dve.getMessage(), dve); + throw new PlatformDataIntegrityException( + "error.msg.document.unknown.data.integrity.issue", + "Unknown data integrity issue with resource."); + } catch (IOException ioe) { + logger.error(ioe.getMessage(), ioe); + throw new DocumentSaveException(documentCommand.getName()); + } + } + + @Transactional + @Override + public EntityIdentifier updateDocument(DocumentCommand documentCommand, + InputStream inputStream) { + try { + + DocumentCommandValidator validator = new DocumentCommandValidator( + documentCommand); + validator.validateForUpdate(); + // TODO check if entity id is valid and within data scope for the + // user + Document documentForUpdate = this.documentRepository + .findOne(documentCommand.getId()); + if (documentForUpdate == null) { + throw new DocumentNotFoundException( + documentCommand.getParentEntityType(), + documentCommand.getParentEntityId(), + documentCommand.getId()); + } + + // if a new file is also passed in + if (inputStream != null) { + String fileUploadLocation = FileUtils + .generateFileParentDirectory( + documentCommand.getParentEntityType(), + documentCommand.getParentEntityId()); + + /** Recursively create the directory if it does not exist **/ + if (!new File(fileUploadLocation).isDirectory()) { + new File(fileUploadLocation).mkdirs(); + } + + // TODO replace file system appender with an Amazon S3 appender + // TODO delete the file at the previous location + String fileLocation = FileUtils.saveToFileSystem(inputStream, + fileUploadLocation, documentCommand.getFileName()); + documentCommand.setSize(documentCommand.getSize()); + } + + documentForUpdate.update(documentCommand); + + this.documentRepository.saveAndFlush(documentForUpdate); + + return new EntityIdentifier(documentForUpdate.getId()); + } catch (DataIntegrityViolationException dve) { + logger.error(dve.getMessage(), dve); + throw new PlatformDataIntegrityException( + "error.msg.document.unknown.data.integrity.issue", + "Unknown data integrity issue with resource."); + } catch (IOException ioe) { + logger.error(ioe.getMessage(), ioe); + throw new DocumentSaveException(documentCommand.getName()); + } + } + + @Transactional + @Override + public EntityIdentifier deleteDocument(DocumentCommand documentCommand) { + validateParentEntityType(documentCommand); + // TODO: Check document is present under this entity Id + Document document = this.documentRepository.findOne(documentCommand + .getId()); + if (document == null) { + throw new DocumentNotFoundException( + documentCommand.getParentEntityType(), + documentCommand.getParentEntityId(), + documentCommand.getId()); + } + + this.documentRepository.delete(document); + // TODO: delete the actual file from File system/ Amazon S3 + + return new EntityIdentifier(document.getId()); + } + + private void validateParentEntityType(DocumentCommand documentCommand) { + if (!checkValidEntityType(documentCommand.getParentEntityType())) { + throw new InvalidEntityTypeForDocumentManagementException( + documentCommand.getParentEntityType()); + } + } + + private static boolean checkValidEntityType(String entityType) { + for (ApplicationConstants.DOCUMENT_MANAGEMENT_ENTITIES entities : ApplicationConstants.DOCUMENT_MANAGEMENT_ENTITIES + .values()) { + if (entities.name().equalsIgnoreCase(entityType)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/CodeValueNotFoundException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/CodeValueNotFoundException.java index 7f7bfa84e..8238581ab 100755 --- a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/CodeValueNotFoundException.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/CodeValueNotFoundException.java @@ -6,6 +6,6 @@ package org.mifosng.platform.exceptions; public class CodeValueNotFoundException extends AbstractPlatformResourceNotFoundException { public CodeValueNotFoundException(Long id) { - super("error.msg.client.id.invalid", "Code value with identifier " + id + " does not exist", id); + super("error.msg.codevalue.id.invalid", "Code value with identifier " + id + " does not exist", id); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/DocumentNotFoundException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/DocumentNotFoundException.java new file mode 100755 index 000000000..c337de82d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/DocumentNotFoundException.java @@ -0,0 +1,12 @@ +package org.mifosng.platform.exceptions; + +public class DocumentNotFoundException extends + AbstractPlatformResourceNotFoundException { + + public DocumentNotFoundException(final String entityType, + final Long entityId, final Long id) { + super("error.msg.document.id.invalid", "Document with identifier " + id + + " does not exist for the " + entityType + " with Identifier " + + entityId, id); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/DocumentSaveException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/DocumentSaveException.java new file mode 100755 index 000000000..f11596ba9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/DocumentSaveException.java @@ -0,0 +1,11 @@ +package org.mifosng.platform.exceptions; + +public class DocumentSaveException extends + AbstractPlatformResourceNotFoundException { + + public DocumentSaveException(final String name) { + super("error.msg.document.id.save", + "Unable to save the document with name" + name + + " due to a File system / Amazon S3 issue", name); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidEntityTypeForDocumentManagementException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidEntityTypeForDocumentManagementException.java new file mode 100755 index 000000000..155bc5e41 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidEntityTypeForDocumentManagementException.java @@ -0,0 +1,16 @@ +package org.mifosng.platform.exceptions; + +/** + * A {@link RuntimeException} thrown when document management functionality is + * invoked for invalid Entity Types + */ +public class InvalidEntityTypeForDocumentManagementException extends + AbstractPlatformResourceNotFoundException { + + public InvalidEntityTypeForDocumentManagementException( + final String entityType) { + super("error.documentmanagement.entitytype.invalid", + "Document Management is not support for the Entity Type: " + + entityType, entityType); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/test/resources/META-INF/context.xml b/mifosng-provider/src/test/resources/META-INF/context.xml index d9556190e..17273f12d 100644 --- a/mifosng-provider/src/test/resources/META-INF/context.xml +++ b/mifosng-provider/src/test/resources/META-INF/context.xml @@ -5,7 +5,7 @@ driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/mifosplatform-tenants" username="root" - password="mysql" + password="password" validationQuery="select 1" maxActive="10" maxIdle="4"