diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 2e591724f..15a554d15 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -203,6 +203,26 @@ Update a Client Identifier Delete a Client Identifier + + Client Images + clients/{clientId}/images + Upload an Image for a Client (as DATA URI) + Get Client Image (DATA URI) + Update Client Image (DATA URI) + Delete Client Image + + + + + + Upload an Image for a Client (Multi-part form data) + + Get Client Image (Binary file) + + Update Client Image (Multi-part form data) + + + @@ -2297,6 +2317,225 @@ No Request Body: + + +   +
+
+

Client Images

+

The current API provides support for the addition of a single + image for a Client
+ Allowed formats: JPEG (.jpg or .jpeg), GIF (.gif) and PNG (.png) +

+

The API supports two different Approaches for manipulating Client Images +

+
+ +   +
+
+

Get Client Image (DATA URI)

+

Example Requests:

+
clients/1/images
+
+
+
+ +GET https://DomainName/api/v1/clients/{clientId}/images + + +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ +bWFnZVJlYWR5ccllPAAAAJ1JREFUeNpi+P//PwMIA4E9EG8E4idQDGLbw+WhiiqA+D8OXAFVAzbp +DxBvB2JLIGaGYkuoGEjOhhFIHAbij0BdPgxYACMj42ogJQpifwBiXSDeC8JIbt4LxSC5DyxQjTeB ++BeaYb+Q5EBOAVutCzMJHUNNPADzzDokiYdAfAmJvwLkGeTgWQfyKZICS6hYBTwc0QL8ORSjBDhA +gAEAOg13B6R/SAgAAAAASUVORK5CYII= + +
+
+ +   +
+
+

Get Client Image (Binary file)

+
+
+ +GET https://DomainName/api/v1/clients/{clientId}/images + + +POST clients/1/images +Accept: application/octet-stream + + +Not shown: The corresponding binary (image) file + +
+
+ +   +
+
+

Upload an Image for a Client (Data URI)

+
+
+ +POST https://DomainName/api/v1/clients/{clientId}/images + + +POST clients/1/images +Content-Type: text/plain +Request Body: +data:image/png;base64,iVBORw0KGgoAA +AANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0l +EQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6 +P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC + + +{ + "resourceId": 1, + "changes": {}, + "resourceIdentifier": "1" +} + +
+
+ +   +
+
+

Upload an Image for a Client (Multi-part Form data)

+ + + + + + + +
Mandatory Fields
file +
+

+ The form should contain a required named body part with the + name "file".

+ If you are using a HTML form, a snippet like + <input type="file" name="file"></input> + can be used for uploading the image file +

+
+
+ +POST https://DomainName/api/v1/clients/{clientId}/images + + +POST clients/1/images +Content-Type: multipart/form-data +Request Body: Not shown + + +{ + "resourceId": 1, + "changes": {}, + "resourceIdentifier": "1" +} + +
+
+ +   +
+
+

Update a Client Image (Data URI)

+
+
+ +PUT https://DomainName/api/v1/clients/{clientId}/images + + +PUT clients/1/images +Content-Type: text/plain +Request Body: +data:image/png;base64,iVBORw0KGgoAA +AANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0l +EQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6 +P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC + + +{ + "resourceId": 1, + "changes": {}, + "resourceIdentifier": "1" +} + +
+
+ +   +
+
+

Update a Client Image (Multi-part Form data)

+ + + + + + + +
Mandatory Fields
file +
+

+ The form should contain a required named body part with the + name "file".

+ If you are using a HTML form, a snippet like + <input type="file" name="file"></input> + can be used for uploading the image file +

+
+
+ +PUT https://DomainName/api/v1/clients/{clientId}/images + + +PUT clients/1/images +Content-Type: multipart/form-data +Request Body: Not shown + +
+
+ +   +
+
+

Delete a Client Image

+
+
+ +DELETE https://DomainName/api/v1/clients/{clientId}/images + + +DELETE clients/1/images +Content-Type: application/json +No Request Body: + + +{ + "resourceId": 1, + "changes": {}, + "resourceIdentifier": "1" +} + +
+
+   @@ -11619,6 +11858,26 @@ Content-Type: application/json Update a Client Identifier Delete a Client Identifier + + Client Images + clients/{clientId}/images + Upload an Image for a Client (as DATA URI) + Get Client Image (DATA URI) + Update Client Image (DATA URI) + Delete Client Image + + + + + + Upload an Image for a Client (Multi-part form data) + + Get Client Image (Binary file) + + Update Client Image (Multi-part form data) + + + Loans diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java index 369a5e1cb..5f464a226 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; +import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.exception.ImageDataURLNotValidException; @@ -31,6 +32,49 @@ public class FileUtils { public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx"; + public static enum IMAGE_MIME_TYPE { + GIF("image/gif"), JPEG("image/jpeg"), PNG("image/png"); + + private final String value; + + private IMAGE_MIME_TYPE(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + }; + + public static enum IMAGE_FILE_EXTENSION { + GIF(".gif"), JPEG(".jpeg"), JPG(".jpg"), PNG(".png"); + + private final String value; + + private IMAGE_FILE_EXTENSION(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + }; + + public static enum IMAGE_DATA_URI_SUFFIX { + GIF("data:" + IMAGE_MIME_TYPE.GIF.getValue() + ";base64,"), JPEG("data:" + IMAGE_MIME_TYPE.JPEG.getValue() + ";base64,"), PNG( + "data:" + IMAGE_MIME_TYPE.PNG.getValue() + ";base64,"); + + private final String value; + + private IMAGE_DATA_URI_SUFFIX(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + }; + public static Random random = new Random(); /** @@ -138,7 +182,8 @@ public class FileUtils { * @param mimeType */ public static void validateImageMimeType(String mimeType) { - if (!(mimeType.equalsIgnoreCase("image/gif") || mimeType.equalsIgnoreCase("image/jpeg") || mimeType.equalsIgnoreCase("image/png"))) { throw new ImageUploadException(); } + if (!(mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.GIF.getValue()) || mimeType.equalsIgnoreCase(IMAGE_MIME_TYPE.JPEG.getValue()) || mimeType + .equalsIgnoreCase(IMAGE_MIME_TYPE.PNG.getValue()))) { throw new ImageUploadException(); } } /** @@ -149,19 +194,15 @@ public class FileUtils { public static Base64EncodedImage extractImageFromDataURL(String dataURL) { String fileExtension = ""; String base64EncodedString = null; - String gifDataURLSnippet = "data:image/gif;base64,"; - String pngDataURLSnippet = "data:image/png;base64,"; - String jpegDataURLSnippet = "data:image/jpeg;base64,"; - - if (dataURL.contains(gifDataURLSnippet)) { - base64EncodedString = dataURL.replaceAll(gifDataURLSnippet, ""); - fileExtension = ".gif"; - } else if (dataURL.contains(pngDataURLSnippet)) { - base64EncodedString = dataURL.replaceAll(pngDataURLSnippet, ""); - fileExtension = ".png"; - } else if (dataURL.contains(jpegDataURLSnippet)) { - base64EncodedString = dataURL.replaceAll(jpegDataURLSnippet, ""); - fileExtension = ".jpeg"; + if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.GIF.getValue())) { + base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.GIF.getValue(), ""); + fileExtension = IMAGE_FILE_EXTENSION.GIF.getValue(); + } else if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.PNG.getValue())) { + base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.PNG.getValue(), ""); + fileExtension = IMAGE_FILE_EXTENSION.PNG.getValue(); + } else if (StringUtils.startsWith(dataURL, IMAGE_DATA_URI_SUFFIX.JPEG.getValue())) { + base64EncodedString = dataURL.replaceAll(IMAGE_DATA_URI_SUFFIX.JPEG.getValue(), ""); + fileExtension = IMAGE_FILE_EXTENSION.JPEG.getValue(); } else { throw new ImageDataURLNotValidException(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeDeletedException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeDeletedException.java index 1b8b8ed5a..2e829a4fc 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeDeletedException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeDeletedException.java @@ -11,14 +11,14 @@ public class LoanChargeCannotBeDeletedException extends AbstractPlatformDomainRu /*** enum of reasons of why Loan Charge cannot be waived **/ public static enum LOAN_CHARGE_CANNOT_BE_DELETED_REASON { - ALREADY_PAID, ALREADY_WAIVED, LOAN_APPROVED; + ALREADY_PAID, ALREADY_WAIVED, LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE; public String errorMessage() { if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { return "This loan charge has been partially/completely paid"; } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { return "This loan charge has already been waived"; - } else if (name().toString().equalsIgnoreCase("LOAN_APPROVED")) { return "This charge cannot be deleted as the loan it is associated with has already been approved"; } + } else if (name().toString().equalsIgnoreCase("LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE")) { return "This charge cannot be deleted as the loan it is associated with is not in submitted and pending approval stage"; } return name().toString(); } @@ -27,7 +27,7 @@ public class LoanChargeCannotBeDeletedException extends AbstractPlatformDomainRu return "error.msg.loan.charge.already.paid"; } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { return "error.msg.loan.charge.already.waived"; - } else if (name().toString().equalsIgnoreCase("LOAN_APPROVED")) { return "error.msg.loan.charge.associated.loan.already.approved"; } + } else if (name().toString().equalsIgnoreCase("LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE")) { return "error.msg.loan.charge.associated.loan.not.in.submitted.and.pending.approval.stage"; } return name().toString(); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeUpdatedException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeUpdatedException.java new file mode 100755 index 000000000..36f1e9671 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeUpdatedException.java @@ -0,0 +1,38 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.charge.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class LoanChargeCannotBeUpdatedException extends AbstractPlatformDomainRuleException { + + /*** enum of reasons of why Loan Charge cannot be waived **/ + public static enum LOAN_CHARGE_CANNOT_BE_UPDATED_REASON { + ALREADY_PAID, ALREADY_WAIVED, LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE; + + public String errorMessage() { + if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + return "This loan charge has been partially/completely paid"; + } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + return "This loan charge has already been waived"; + } else if (name().toString().equalsIgnoreCase("LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE")) { return "This charge cannot be updated as the loan it is associated with is not in submitted and pending approval stage"; } + return name().toString(); + } + + public String errorCode() { + if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + return "error.msg.loan.charge.already.paid"; + } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + return "error.msg.loan.charge.already.waived"; + } else if (name().toString().equalsIgnoreCase("LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE")) { return "error.msg.loan.charge.associated.loan.not.in.submitted.and.pending.approval.stage"; } + return name().toString(); + } + } + + public LoanChargeCannotBeUpdatedException(final LOAN_CHARGE_CANNOT_BE_UPDATED_REASON reason, final Long loanChargeId) { + super(reason.errorCode(), reason.errorMessage(), loanChargeId); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java index cdda2bff7..8c7ea0736 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.client.api; +import java.io.File; import java.io.InputStream; import javax.ws.rs.Consumes; @@ -17,12 +18,18 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.api.ApiConstants; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.mifosplatform.infrastructure.core.service.FileUtils; +import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_DATA_URI_SUFFIX; +import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_FILE_EXTENSION; +import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_MIME_TYPE; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.portfolio.client.data.ClientData; import org.mifosplatform.portfolio.client.exception.ImageNotFoundException; @@ -42,128 +49,146 @@ import com.sun.jersey.multipart.FormDataParam; @Scope("singleton") public class ClientImagesApiResource { - private final PlatformSecurityContext context; - private final ClientReadPlatformService clientReadPlatformService; - private final ClientWritePlatformService clientWritePlatformService; - private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PlatformSecurityContext context; + private final ClientReadPlatformService clientReadPlatformService; + private final ClientWritePlatformService clientWritePlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; - @Autowired - public ClientImagesApiResource(final PlatformSecurityContext context, - final ClientReadPlatformService readPlatformService, - final ClientWritePlatformService clientWritePlatformService, - final DefaultToApiJsonSerializer toApiJsonSerializer) { - this.context = context; - this.clientReadPlatformService = readPlatformService; - this.clientWritePlatformService = clientWritePlatformService; - this.toApiJsonSerializer = toApiJsonSerializer; - } + @Autowired + public ClientImagesApiResource(final PlatformSecurityContext context, final ClientReadPlatformService readPlatformService, + final ClientWritePlatformService clientWritePlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer) { + this.context = context; + this.clientReadPlatformService = readPlatformService; + this.clientWritePlatformService = clientWritePlatformService; + this.toApiJsonSerializer = toApiJsonSerializer; + } - /** - * Upload images through multi-part form upload - */ - @POST - @Consumes({ MediaType.MULTIPART_FORM_DATA }) - @Produces({ MediaType.APPLICATION_JSON }) - public String addNewClientImage( - @PathParam("clientId") final Long clientId, - @HeaderParam("Content-Length") final Long fileSize, - @FormDataParam("file") final InputStream inputStream, - @FormDataParam("file") final FormDataContentDisposition fileDetails, - @FormDataParam("file") final FormDataBodyPart bodyPart) { + /** + * Upload images through multi-part form upload + */ + @POST + @Consumes({ MediaType.MULTIPART_FORM_DATA }) + @Produces({ MediaType.APPLICATION_JSON }) + public String addNewClientImage(@PathParam("clientId") final Long clientId, @HeaderParam("Content-Length") final Long fileSize, + @FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails, + @FormDataParam("file") final 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(), - ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); + // 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(), ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); - final CommandProcessingResult result = this.clientWritePlatformService - .saveOrUpdateClientImage(clientId, fileDetails.getFileName(), - inputStream); + final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(), + inputStream); - return this.toApiJsonSerializer.serialize(result); - } + return this.toApiJsonSerializer.serialize(result); + } - /** - * Upload image as a Data URL (essentially a base64 encoded stream) - */ - @POST - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String addNewClientImage(@PathParam("clientId") final Long clientId, - final String jsonRequestBody) { + /** + * Upload image as a Data URL (essentially a base64 encoded stream) + */ + @POST + @Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String addNewClientImage(@PathParam("clientId") final Long clientId, final String jsonRequestBody) { - final Base64EncodedImage base64EncodedImage = FileUtils - .extractImageFromDataURL(jsonRequestBody); + final Base64EncodedImage base64EncodedImage = FileUtils.extractImageFromDataURL(jsonRequestBody); - final CommandProcessingResult result = this.clientWritePlatformService - .saveOrUpdateClientImage(clientId, base64EncodedImage); + final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage); - return this.toApiJsonSerializer.serialize(result); - } + return this.toApiJsonSerializer.serialize(result); + } - /** - * Returns a base 64 encoded client image - */ - @GET - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveClientImage(@PathParam("clientId") final Long clientId) { + /** + * Returns a base 64 encoded client image Data URI + */ + @GET + @Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces({ MediaType.TEXT_PLAIN }) + public String retrieveClientImage(@PathParam("clientId") final Long clientId) { - context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE"); + context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE"); - final ClientData clientData = this.clientReadPlatformService - .retrieveIndividualClient(clientId); + final ClientData clientData = this.clientReadPlatformService.retrieveIndividualClient(clientId); - if (clientData.imageKeyDoesNotExist()) { - throw new ImageNotFoundException("clients", clientId); - } + if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } - return Base64.encodeFromFile(clientData.imageKey()); - } + // TODO: Need a better way of determining image type + String imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.JPEG.getValue(); + if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.GIF.getValue())) { + imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.GIF.getValue(); + } else if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.PNG.getValue())) { + imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.PNG.getValue(); + } - /** - * 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 - */ - @PUT - @Consumes({ MediaType.MULTIPART_FORM_DATA }) - @Produces({ MediaType.APPLICATION_JSON }) - public String updateClientImage( - @PathParam("clientId") final Long clientId, - @HeaderParam("Content-Length") final Long fileSize, - @FormDataParam("file") final InputStream inputStream, - @FormDataParam("file") final FormDataContentDisposition fileDetails, - @FormDataParam("file") final FormDataBodyPart bodyPart) { + String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeFromFile(clientData.imageKey()); - return addNewClientImage(clientId, fileSize, inputStream, fileDetails, - bodyPart); - } + return clientImageAsBase64Text; + } - @DELETE - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String deleteClientImage(@PathParam("clientId") final Long clientId) { - this.clientWritePlatformService.deleteClientImage(clientId); + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_OCTET_STREAM }) + public Response downloadClientImage(@PathParam("clientId") final Long clientId) { - return this.toApiJsonSerializer.serialize(new CommandProcessingResult( - clientId)); - } + context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE"); + final ClientData clientData = this.clientReadPlatformService.retrieveIndividualClient(clientId); + + if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } + + File image = new File(clientData.imageKey()); + String imageName = image.getName(); + ResponseBuilder response = Response.ok(image); + response.header("Content-Disposition", "attachment; filename=\"" + imageName + "\""); + + // TODO: Need a better way of determining image type + + // determine image type + String contentType = IMAGE_MIME_TYPE.JPEG.getValue(); + if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.GIF.getValue())) { + contentType = IMAGE_MIME_TYPE.GIF.getValue(); + } else if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.PNG.getValue())) { + contentType = IMAGE_MIME_TYPE.PNG.getValue(); + } + + response.header("Content-Type", contentType); + return response.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 + */ + @PUT + @Consumes({ MediaType.MULTIPART_FORM_DATA }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateClientImage(@PathParam("clientId") final Long clientId, @HeaderParam("Content-Length") final Long fileSize, + @FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails, + @FormDataParam("file") final FormDataBodyPart bodyPart) { + return addNewClientImage(clientId, fileSize, inputStream, fileDetails, bodyPart); + } + + /** + * 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 + * + * Upload image as a Data URL (essentially a base64 encoded stream) + */ + @PUT + @Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateClientImage(@PathParam("clientId") final Long clientId, final String jsonRequestBody) { + return addNewClientImage(clientId, jsonRequestBody); + } + + @DELETE + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String deleteClientImage(@PathParam("clientId") final Long clientId) { + this.clientWritePlatformService.deleteClientImage(clientId); + return this.toApiJsonSerializer.serialize(new CommandProcessingResult(clientId)); + } - /** - * 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 - * - * Upload image as a Data URL (essentially a base64 encoded stream) - */ - @PUT - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String updateClientImage(@PathParam("clientId") final Long clientId, - final String jsonRequestBody) { - return addNewClientImage(clientId, jsonRequestBody); - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ImageNotFoundException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ImageNotFoundException.java index f6198003d..c96d4fd74 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ImageNotFoundException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ImageNotFoundException.java @@ -5,9 +5,9 @@ */ package org.mifosplatform.portfolio.client.exception; -import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; -public class ImageNotFoundException extends AbstractPlatformDomainRuleException { +public class ImageNotFoundException extends AbstractPlatformResourceNotFoundException { public ImageNotFoundException(final String resource, final Long resourceId) { super("error.msg.entity.image.invalid", "Image for resource " + resource + " with Identifier " + resourceId + " does not exist", diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java index f100719c1..1dcb5842a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java @@ -23,21 +23,21 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi switch (loanEvent) { case LOAN_CREATED: if (from == null) { - newState = stateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL, allowedLoanStatuses); + newState = stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, allowedLoanStatuses); } break; case LOAN_REJECTED: - if (from.hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL)) { + if (from.hasStateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) { newState = stateOf(LoanStatus.REJECTED, allowedLoanStatuses); } break; case LOAN_APPROVED: - if (from.hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL)) { + if (from.hasStateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) { newState = stateOf(LoanStatus.APPROVED, allowedLoanStatuses); } break; case LOAN_WITHDRAWN: - if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.SUBMITED_AND_PENDING_APPROVAL, LoanStatus.APPROVED)) { + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.APPROVED)) { newState = stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, allowedLoanStatuses); } break; @@ -48,7 +48,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi break; case LOAN_APPROVAL_UNDO: if (from.hasStateOf(LoanStatus.APPROVED)) { - newState = stateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL, allowedLoanStatuses); + newState = stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, allowedLoanStatuses); } break; case LOAN_DISBURSAL_UNDO: diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java index 08011ceb4..abf9b99a9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java @@ -11,7 +11,7 @@ package org.mifosplatform.portfolio.loanaccount.domain; public enum LoanStatus { INVALID(0, "loanStatusType.invalid"), // - SUBMITED_AND_PENDING_APPROVAL(100, "loanStatusType.submitted.and.pending.approval"), // + SUBMITTED_AND_PENDING_APPROVAL(100, "loanStatusType.submitted.and.pending.approval"), // APPROVED(200, "loanStatusType.approved"), // ACTIVE(300, "loanStatusType.active"), // WITHDRAWN_BY_CLIENT(400, "loanStatusType.withdrawn.by.client"), // @@ -29,7 +29,7 @@ public enum LoanStatus { LoanStatus enumeration = LoanStatus.INVALID; switch (statusValue) { case 100: - enumeration = LoanStatus.SUBMITED_AND_PENDING_APPROVAL; + enumeration = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL; break; case 200: enumeration = LoanStatus.APPROVED; @@ -77,7 +77,7 @@ public enum LoanStatus { } public boolean isSubmittedAndPendingApproval() { - return this.value.equals(LoanStatus.SUBMITED_AND_PENDING_APPROVAL.getValue()); + return this.value.equals(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue()); } public boolean isApproved() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 724ec2e77..cece3ba8f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -26,6 +26,8 @@ import org.mifosplatform.portfolio.charge.domain.Charge; import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeDeletedException; import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeDeletedException.LOAN_CHARGE_CANNOT_BE_DELETED_REASON; +import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeUpdatedException; +import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeUpdatedException.LOAN_CHARGE_CANNOT_BE_UPDATED_REASON; import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeWaivedException; import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeWaivedException.LOAN_CHARGE_CANNOT_BE_WAIVED_REASON; import org.mifosplatform.portfolio.charge.exception.LoanChargeNotFoundException; @@ -529,6 +531,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = retrieveLoanBy(loanId); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + // Charges may be edited only when the loan associated with them are + // yet to be approved (are in submitted and pending status) + if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeUpdatedException( + LOAN_CHARGE_CANNOT_BE_UPDATED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); } + final Map changes = loan.updateLoanCharge(loanCharge, command); this.loanRepository.save(loan); @@ -597,11 +604,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = retrieveLoanBy(loanId); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - // validate loan charge is not partly paid or waived - if (loanCharge.isWaived()) { - throw new LoanChargeCannotBeDeletedException(LOAN_CHARGE_CANNOT_BE_DELETED_REASON.ALREADY_WAIVED, loanCharge.getId()); - } else if (loanCharge.isPaidOrPartiallyPaid(loan.getCurrency())) { throw new LoanChargeCannotBeDeletedException( - LOAN_CHARGE_CANNOT_BE_DELETED_REASON.ALREADY_PAID, loanCharge.getId()); } + // Charges may be deleted only when the loan associated with them are + // yet to be approved (are in submitted and pending status) + if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeDeletedException( + LOAN_CHARGE_CANNOT_BE_DELETED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); } loan.removeLoanCharge(loanCharge); this.loanRepository.save(loan); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java index 93df32e6a..2215708c5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java @@ -263,9 +263,9 @@ public class LoanEnumerations { case INVALID: optionData = new LoanStatusEnumData(LoanStatus.INVALID.getValue().longValue(), LoanStatus.INVALID.getCode(), "Invalid"); break; - case SUBMITED_AND_PENDING_APPROVAL: - optionData = new LoanStatusEnumData(LoanStatus.SUBMITED_AND_PENDING_APPROVAL.getValue().longValue(), - LoanStatus.SUBMITED_AND_PENDING_APPROVAL.getCode(), "Submitted and pending approval"); + case SUBMITTED_AND_PENDING_APPROVAL: + optionData = new LoanStatusEnumData(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue().longValue(), + LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getCode(), "Submitted and pending approval"); break; case APPROVED: optionData = new LoanStatusEnumData(LoanStatus.APPROVED.getValue().longValue(), LoanStatus.APPROVED.getCode(), "Approved");