FINERACT-1932: Fineract modularization - fineract-document

This commit is contained in:
Jose Alberto Hernandez
2024-04-19 15:32:04 +02:00
committed by Adam Saghy
parent ce2f05ee78
commit 2b9e114c05
41 changed files with 251 additions and 1 deletions
@@ -0,0 +1,61 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.api;
import com.google.common.io.ByteSource;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import java.io.IOException;
import java.io.InputStream;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utilities common to file upload/download resources.
*
* @author Michael Vorburger.ch
*/
final class ContentResources {
private static final Logger LOG = LoggerFactory.getLogger(ContentResources.class);
private ContentResources() {}
static Response fileDataToResponse(FileData fileData, String fileName, String dispositionType) {
ResponseBuilder response;
try {
ByteSource byteSource = fileData.getByteSource();
// TODO Where is this InputStream closed?! It needs to be AFTER it's read by JAX-RS.. how to do that?
InputStream is = byteSource.openBufferedStream();
response = Response.ok(is);
response.header("Content-Disposition", dispositionType + "; filename=\"" + fileName + "\"");
response.header("Content-Length", byteSource.sizeIfKnown().or(-1L));
response.header("Content-Type", fileData.contentType());
} catch (IOException e) {
LOG.error("resizedImage.getByteSource().openBufferedStream() failed", e);
response = Response.serverError();
}
return response.build();
}
static Response fileDataToResponse(FileData fileData, String dispositionType) {
return fileDataToResponse(fileData, fileData.name(), dispositionType);
}
}
@@ -0,0 +1,233 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.stereotype.Component;
@Component
@Path("/v1/{entityType}/{entityId}/documents")
@Tag(name = "Documents", description = "Multiple Documents (a combination of a name, description and a file) may be attached to different Entities like Clients, Groups, Staff, Loans, Savings and Client Identifiers in the system\n"
+ "\n" + "Note: The currently allowed Entities are\n" + "\n" + "Clients: URL Pattern as clients\n" + "Staff: URL Pattern as staff\n"
+ "Loans: URL Pattern as loans\n" + "Savings: URL Pattern as savings\n" + "Client Identifiers: URL Pattern as client_identifiers\n"
+ "Groups: URL Pattern as groups")
@RequiredArgsConstructor
public class DocumentManagementApiResource {
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList("id", "parentEntityType", "parentEntityId", "name", "fileName", "size", "type", "description"));
private static final String SYSTEM_ENTITY_TYPE = "DOCUMENT";
private final PlatformSecurityContext context;
private final DocumentReadPlatformService documentReadPlatformService;
private final DocumentWritePlatformService documentWritePlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final ToApiJsonSerializer<DocumentData> toApiJsonSerializer;
private final FileUploadValidator fileUploadValidator;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List documents", description = "Example Requests:\n" + "\n" + "clients/1/documents\n" + "\n"
+ "client_identifiers/1/documents\n" + "\n" + "loans/1/documents?fields=name,description")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.GetEntityTypeEntityIdDocumentsResponse.class)))) })
public String retrieveAllDocuments(@Context final UriInfo uriInfo,
@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId) {
this.context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
final Collection<DocumentData> documentDatas = this.documentReadPlatformService.retrieveAllDocuments(entityType, entityId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, documentDatas, RESPONSE_DATA_PARAMETERS);
}
@POST
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
@RequestBody(description = "Create document", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = DocumentManagementApiResourceSwagger.DocumentUploadRequest.class)) })
@Operation(summary = "Create a Document", description = "Note: A document is created using a Multi-part form upload \n" + "\n"
+ "Body Parts\n" + "\n" + "name : \n" + "Name or summary of the document\n" + "\n" + "description : \n"
+ "Description of the document\n" + "\n" + "file : \n" + "The file to be uploaded\n" + "\n" + "Mandatory Fields : \n"
+ "file and description")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.PostEntityTypeEntityIdDocumentsResponse.class))) })
public String createDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@HeaderParam("Content-Length") @Parameter(description = "Content-Length") final Long fileSize,
@FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails,
@FormDataParam("file") final FormDataBodyPart bodyPart, @FormDataParam("name") final String name,
@FormDataParam("description") final String description) {
// TODO: stop reading from stream after max size is reached to protect against malicious clients
// TODO: need to extract the actual file type and determine if they are permissible
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
final DocumentCommand documentCommand = new DocumentCommand(null, null, entityType, entityId, name, fileDetails.getFileName(),
fileSize, bodyPart.getMediaType().toString(), description, null);
final Long documentId = this.documentWritePlatformService.createDocument(documentCommand, inputStream);
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(documentId));
}
@PUT
@Path("{documentId}")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
@RequestBody(description = "Update document", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = DocumentManagementApiResourceSwagger.DocumentUploadRequest.class)) })
@Operation(summary = "Update a Document", description = "Note: A document is updated using a Multi-part form upload \n" + "Body Parts\n"
+ "name\n" + "Name or summary of the document\n" + "description\n" + "Description of the document\n" + "file\n"
+ "The file to be uploaded")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.PutEntityTypeEntityIdDocumentsResponse.class))) })
public String updateDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId,
@HeaderParam("Content-Length") @Parameter(description = "Content-Length") final Long fileSize,
@FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails,
@FormDataParam("file") final FormDataBodyPart bodyPart, @FormDataParam("name") final String name,
@FormDataParam("description") final String description) {
final Set<String> modifiedParams = new HashSet<>();
modifiedParams.add("name");
modifiedParams.add("description");
/***
* Populate Document command based on whether a file has also been passed in as a part of the update
***/
DocumentCommand documentCommand = null;
if (inputStream != null && fileDetails.getFileName() != null) {
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
modifiedParams.add("fileName");
modifiedParams.add("size");
modifiedParams.add("type");
modifiedParams.add("location");
documentCommand = new DocumentCommand(modifiedParams, documentId, entityType, entityId, name, fileDetails.getFileName(),
fileSize, bodyPart.getMediaType().toString(), description, null);
} else {
documentCommand = new DocumentCommand(modifiedParams, documentId, entityType, entityId, name, null, null, null, description,
null);
}
/***
* TODO: does not return list of changes, should be done for consistency with rest of API
**/
final CommandProcessingResult identifier = this.documentWritePlatformService.updateDocument(documentCommand, inputStream);
return this.toApiJsonSerializer.serialize(identifier);
}
@GET
@Path("{documentId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Document", description = "Example Requests:\n" + "\n" + "clients/1/documents/1\n" + "\n" + "\n"
+ "loans/1/documents/1\n" + "\n" + "\n" + "client_identifiers/1/documents/1?fields=name,description")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.GetEntityTypeEntityIdDocumentsResponse.class))) })
public String getDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, documentData, RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("{documentId}/attachment")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
@Operation(summary = "Retrieve Binary File associated with Document", description = "Request used to download the file associated with the document\n"
+ "\n" + "Example Requests:\n" + "\n" + "clients/1/documents/1/attachment\n" + "\n" + "\n" + "loans/1/documents/1/attachment")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "Not Shown: The corresponding Binary file") })
public Response downloadFile(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
final FileData fileData = this.documentReadPlatformService.retrieveFileData(entityType, entityId, documentId);
return ContentResources.fileDataToResponse(fileData, "attachment");
}
@DELETE
@Path("{documentId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Remove a Document", description = "")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.DeleteEntityTypeEntityIdDocumentsResponse.class))) })
public String deleteDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
final DocumentCommand documentCommand = new DocumentCommand(null, documentId, entityType, entityId, null, null, null, null, null,
null);
final CommandProcessingResult documentIdentifier = this.documentWritePlatformService.deleteDocument(documentCommand);
return this.toApiJsonSerializer.serialize(documentIdentifier);
}
}
@@ -0,0 +1,149 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.api;
import io.swagger.v3.oas.annotations.media.Schema;
import org.apache.fineract.infrastructure.core.data.UploadRequest;
import org.glassfish.jersey.media.multipart.FormDataParam;
/**
* Created by sanyam on 7/8/17.
*/
final class DocumentManagementApiResourceSwagger {
private DocumentManagementApiResourceSwagger() {
}
@Schema(description = "GetEntityTypeEntityIdDocumentsResponse")
public static final class GetEntityTypeEntityIdDocumentsResponse {
private GetEntityTypeEntityIdDocumentsResponse() {
}
@Schema(example = "1")
public Long id;
@Schema(example = "clients")
public String parentEntityType;
@Schema(example = "1")
public Long parentEntityId;
@Schema(example = "Client Details Form")
public String name;
@Schema(example = "CGAP.pdf")
public String fileName;
@Schema(example = "5246719")
public Long size;
@Schema(example = "application/pdf")
public String type;
@Schema(example = "A signed form signed by new member")
public String description;
@Schema(example = "")
public String location;
@Schema(example = "")
public Integer storageType;
}
@Schema(description = "PostEntityTypeEntityIdDocumentsResponse")
public static final class PostEntityTypeEntityIdDocumentsResponse {
private PostEntityTypeEntityIdDocumentsResponse() {
}
@Schema(example = "3")
public Long resourceId;
@Schema(example = "3")
public String resourceIdentifier;
}
@Schema(description = "PutEntityTypeEntityIdDocumentsResponse")
public static final class PutEntityTypeEntityIdDocumentsResponse {
private PutEntityTypeEntityIdDocumentsResponse() {
}
public static final class PutEntityTypeEntityIdDocumentsResponseChangesSwagger {
private PutEntityTypeEntityIdDocumentsResponseChangesSwagger() {
}
}
@Schema(example = "3")
public Long resourceId;
public PutEntityTypeEntityIdDocumentsResponse.PutEntityTypeEntityIdDocumentsResponseChangesSwagger changes;
@Schema(example = "3")
public String resourceIdentifier;
}
@Schema(description = "DeleteEntityTypeEntityIdDocumentsResponse")
public static final class DeleteEntityTypeEntityIdDocumentsResponse {
private DeleteEntityTypeEntityIdDocumentsResponse() {
}
public static final class PutEntityTypeEntityIdDocumentsResponseChangesSwagger {
private PutEntityTypeEntityIdDocumentsResponseChangesSwagger() {
}
}
@Schema(example = "3")
public Long resourceId;
public DeleteEntityTypeEntityIdDocumentsResponse.PutEntityTypeEntityIdDocumentsResponseChangesSwagger changes;
@Schema(example = "3")
public String resourceIdentifier;
}
@Schema(description = "Document upload request")
public static final class DocumentUploadRequest extends UploadRequest {
@Schema(name = "name", type = "string", accessMode = Schema.AccessMode.READ_WRITE)
@FormDataParam("name")
private String name;
@Schema(name = "description", type = "string", accessMode = Schema.AccessMode.READ_WRITE)
@FormDataParam("description")
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.api;
import java.io.InputStream;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.springframework.stereotype.Component;
/**
* Validator for uploaded files.
*
* @author Michael Vorburger.ch
*/
@Component
public class FileUploadValidator {
public void validate(Long contentLength, InputStream inputStream, FormDataContentDisposition fileDetails, FormDataBodyPart bodyPart) {
new DataValidatorBuilder().resource("fileUpload").reset().parameter("Content-Length").value(contentLength).notBlank()
.integerGreaterThanNumber(0).reset().parameter("InputStream").value(inputStream).notNull().reset()
.parameter("FormDataContentDisposition").value(fileDetails).notNull().reset().parameter("FormDataBodyPart").value(bodyPart)
.notNull().throwValidationErrors();
}
}
@@ -0,0 +1,232 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.api;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.UploadRequest;
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils.ImageFileExtension;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
import org.apache.fineract.infrastructure.documentmanagement.data.ImageResizer;
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
import org.apache.fineract.infrastructure.documentmanagement.exception.InvalidEntityTypeForImageManagementException;
import org.apache.fineract.infrastructure.documentmanagement.service.ImageReadPlatformService;
import org.apache.fineract.infrastructure.documentmanagement.service.ImageWritePlatformService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.data.ClientData;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
@Path("/v1/{entity}/{entityId}/images")
public class ImagesApiResource {
private final PlatformSecurityContext context;
private final ImageReadPlatformService imageReadPlatformService;
private final ImageWritePlatformService imageWritePlatformService;
private final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer;
private final FileUploadValidator fileUploadValidator;
private final ImageResizer imageResizer;
/**
* Upload images through multi-part form upload
*/
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
@RequestBody(description = "Upload new client image", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
public String addNewClientImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId,
@HeaderParam("Content-Length") final Long fileSize, @FormDataParam("file") final InputStream inputStream,
@FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart) {
validateEntityTypeforImage(entityName);
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
// TODO: vishwas might need more advances validation (like reading magic
// number) for handling malicious clients
// and clients not setting mime type
ContentRepositoryUtils.validateClientImageNotEmpty(fileDetails.getFileName());
ContentRepositoryUtils.validateImageMimeType(bodyPart.getMediaType().toString());
final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateImage(entityName, entityId,
fileDetails.getFileName(), inputStream, fileSize);
return this.toApiJsonSerializer.serialize(result);
}
/**
* 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("entity") final String entityName, @PathParam("entityId") final Long entityId,
final String jsonRequestBody) {
validateEntityTypeforImage(entityName);
final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody);
final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateImage(entityName, entityId, base64EncodedImage);
return this.toApiJsonSerializer.serialize(result);
}
/**
* Returns a images, either as Base64 encoded text/plain or as inline or attachment with image MIME type as
* Content-Type.
*/
@GET
@Consumes(MediaType.APPLICATION_JSON)
// FINERACT-1265: Do NOT specify @Produces(TEXT_PLAIN) here - it may actually not (if it calls the next methods it's
// octet-stream)
public Response retrieveImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId,
@QueryParam("maxWidth") final Integer maxWidth, @QueryParam("maxHeight") final Integer maxHeight,
@QueryParam("output") final String output, @HeaderParam("Accept") String acceptHeader) {
validateEntityTypeforImage(entityName);
if (EntityTypeForImages.CLIENTS.toString().equalsIgnoreCase(entityName)) {
this.context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE");
} else if (EntityTypeForImages.STAFF.toString().equalsIgnoreCase(entityName)) {
this.context.authenticatedUser().validateHasReadPermission("STAFFIMAGE");
}
final FileData imageData = this.imageReadPlatformService.retrieveImage(entityName, entityId);
final FileData resizedImage = imageResizer.resize(imageData, maxWidth, maxHeight);
// If client wants (Accept header) octet-stream, or output="octet" or "inline_octet", then send that instead of
// text
if ("application/octet-stream".equalsIgnoreCase(acceptHeader)
|| (output != null && (output.equals("octet") || output.equals("inline_octet")))) {
return ContentResources.fileDataToResponse(resizedImage, resizedImage.name() + ImageFileExtension.JPEG,
"inline_octet".equals(output) ? "inline" : "attachment");
}
// Else return response with Base64 encoded
// TODO: Need a better way of determining image type
String imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.JPEG.getValue();
if (StringUtils.endsWith(imageData.name(), ContentRepositoryUtils.ImageFileExtension.GIF.getValue())) {
imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.GIF.getValue();
} else if (StringUtils.endsWith(imageData.name(), ContentRepositoryUtils.ImageFileExtension.PNG.getValue())) {
imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.PNG.getValue();
}
try {
byte[] resizedImageBytes = resizedImage.getByteSource().read();
if (resizedImageBytes != null) {
final String clientImageAsBase64Text = imageDataURISuffix + Base64.getMimeEncoder().encodeToString(resizedImageBytes);
return Response.ok(clientImageAsBase64Text, MediaType.TEXT_PLAIN_TYPE).build();
} else {
log.error("resizedImageBytes is null for entityName={}, entityId={}, maxWidth={}, maxHeight={}", entityName, entityId,
maxWidth, maxHeight);
return Response.serverError().build();
}
} catch (IOException e) {
throw new ContentManagementException(imageData.name(), e.getMessage(), e);
}
}
/**
* 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)
@RequestBody(description = "Update client image", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
public String updateClientImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId,
@HeaderParam("Content-Length") final Long fileSize, @FormDataParam("file") final InputStream inputStream,
@FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart) {
return addNewClientImage(entityName, entityId, 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("entity") final String entityName, @PathParam("entityId") final Long entityId,
final String jsonRequestBody) {
return addNewClientImage(entityName, entityId, jsonRequestBody);
}
@DELETE
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String deleteClientImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId) {
validateEntityTypeforImage(entityName);
this.imageWritePlatformService.deleteImage(entityName, entityId);
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(entityId));
}
/*** Entities for document Management **/
public enum EntityTypeForImages {
STAFF, CLIENTS;
@Override
public String toString() {
return name().toString().toLowerCase();
}
}
private void validateEntityTypeforImage(final String entityName) {
if (!checkValidEntityType(entityName)) {
throw new InvalidEntityTypeForImageManagementException(entityName);
}
}
private static boolean checkValidEntityType(final String entityType) {
for (final EntityTypeForImages entities : EntityTypeForImages.values()) {
if (entities.name().equalsIgnoreCase(entityType)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.api;
/**
* Created by sanyam on 10/8/17.
*/
final class ImagesApiResourceSwagger {
private ImagesApiResourceSwagger() {
}
}
@@ -0,0 +1,144 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.command;
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 Integer storageType;
private final Set<String> modifiedParameters;
public DocumentCommand(final Set<String> modifiedParameters, 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.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 this.id;
}
public String getParentEntityType() {
return this.parentEntityType;
}
public Long getParentEntityId() {
return this.parentEntityId;
}
public String getName() {
return this.name;
}
public String getFileName() {
return this.fileName;
}
public Long getSize() {
return this.size;
}
public String getType() {
return this.type;
}
public String getDescription() {
return this.description;
}
public String getLocation() {
return this.location;
}
public Set<String> getModifiedParameters() {
return this.modifiedParameters;
}
public void setFileName(final String fileName) {
this.fileName = fileName;
}
public void setSize(final Long size) {
this.size = size;
}
public void setType(final String type) {
this.type = type;
}
public void setLocation(final String location) {
this.location = location;
}
public Integer getStorageType() {
return this.storageType;
}
public void setStorageType(final Integer storageType) {
this.storageType = storageType;
}
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");
}
}
@@ -0,0 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.command;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
public class DocumentCommandValidator {
private final DocumentCommand command;
public DocumentCommandValidator(final DocumentCommand command) {
this.command = command;
}
public void validateForUpdate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("document");
baseDataValidator.reset().parameter("name").value(this.command.getName()).ignoreIfNull().notBlank();
baseDataValidator.reset().parameter("size").value(this.command.getSize()).ignoreIfNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("fileName").value(this.command.getFileName()).ignoreIfNull().notBlank()
.notExceedingLengthOf(250);
baseDataValidator.reset().parameter("location").value(this.command.getLocation()).ignoreIfNull().notBlank();
baseDataValidator.reset().parameter("description").value(this.command.getName()).ignoreIfNull().notExceedingLengthOf(250);
baseDataValidator.reset().anyOfNotNull(this.command.getName(), this.command.getFileName(), this.command.getDescription(),
this.command.getLocation(), this.command.getSize());
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
public void validateForCreate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("document");
baseDataValidator.reset().parameter("parentEntityType").value(this.command.getParentEntityType()).notBlank()
.notExceedingLengthOf(50);
baseDataValidator.reset().parameter("parentEntityId").value(this.command.getParentEntityId()).integerGreaterThanZero();
baseDataValidator.reset().parameter("name").value(this.command.getName()).notBlank().notExceedingLengthOf(250);
baseDataValidator.reset().parameter("size").value(this.command.getSize()).integerGreaterThanZero();
baseDataValidator.reset().parameter("fileName").value(this.command.getFileName()).notBlank().notExceedingLengthOf(250);
baseDataValidator.reset().parameter("description").value(this.command.getName()).notExceedingLengthOf(250);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
import java.io.BufferedInputStream;
public interface ContentPathSanitizer {
String sanitize(String path);
String sanitize(String path, BufferedInputStream is);
}
@@ -0,0 +1,56 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
import java.io.InputStream;
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
import org.apache.fineract.infrastructure.documentmanagement.data.ImageData;
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
/**
* Repository which stores Files (AKA Documents) and Images.
*/
public interface ContentRepository {
// TODO:Vishwas Need to move these settings to the Database
Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5;
// TODO:Vishwas Need to move these settings to the Database
Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1;
String saveFile(InputStream uploadedInputStream, DocumentCommand documentCommand);
void deleteFile(String documentPath);
FileData fetchFile(DocumentData documentData);
String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize);
String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName);
void deleteImage(String location);
FileData fetchImage(ImageData imageData);
StorageType getStorageType();
}
@@ -0,0 +1,53 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ContentRepositoryFactory {
private final FineractProperties fineractProperties;
private final List<ContentRepository> contentRepositories;
public ContentRepository getRepository() {
if (fineractProperties.getContent() != null) {
if (fineractProperties.getContent().getFilesystem() != null
&& Boolean.TRUE.equals(fineractProperties.getContent().getFilesystem().getEnabled())) {
return getRepository(StorageType.FILE_SYSTEM);
} else if (fineractProperties.getContent().getS3() != null
&& Boolean.TRUE.equals(fineractProperties.getContent().getS3().getEnabled())) {
return getRepository(StorageType.S3);
}
}
throw new RuntimeException(
"No content repository enabled. Please set either 'fineract.content.filesystem.enabled=true' or 'fineract.content.s3.enabled=true' in your application.properties.");
}
public ContentRepository getRepository(StorageType storageType) {
return contentRepositories.stream().filter(cr -> cr.getStorageType().equals(storageType)).findFirst().orElseThrow(
() -> new RuntimeException(String.format("No content repository implementation found for storage type: %s", storageType)));
}
}
@@ -0,0 +1,204 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
import org.apache.fineract.infrastructure.core.exception.ImageDataURLNotValidException;
import org.apache.fineract.infrastructure.core.exception.ImageUploadException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
public final class ContentRepositoryUtils {
private static final SecureRandom random = new SecureRandom();
private ContentRepositoryUtils() {}
public enum ImageMIMEtype {
GIF("image/gif"), JPEG("image/jpeg"), PNG("image/png");
private final String value;
ImageMIMEtype(final String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
@SuppressWarnings("UnnecessaryDefaultInEnumSwitch")
public static ImageMIMEtype fromFileExtension(ImageFileExtension fileExtension) {
switch (fileExtension) {
case GIF:
return ImageMIMEtype.GIF;
case JPG:
case JPEG:
return ImageMIMEtype.JPEG;
case PNG:
return ImageMIMEtype.PNG;
default:
throw new IllegalArgumentException();
}
}
}
public enum ImageFileExtension {
GIF(".gif"), JPEG(".jpeg"), JPG(".jpg"), PNG(".png");
private final String value;
ImageFileExtension(final String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public String getValueWithoutDot() {
return this.value.substring(1);
}
public ImageFileExtension getFileExtension() {
switch (this) {
case GIF:
return ImageFileExtension.GIF;
case JPEG:
return ImageFileExtension.JPEG;
case PNG:
return ImageFileExtension.PNG;
default:
throw new IllegalArgumentException();
}
}
}
public enum ImageDataURIsuffix {
GIF("data:" + ImageMIMEtype.GIF.getValue() + ";base64,"), JPEG("data:" + ImageMIMEtype.JPEG.getValue() + ";base64,"), PNG(
"data:" + ImageMIMEtype.PNG.getValue() + ";base64,");
private final String value;
ImageDataURIsuffix(final String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
public static ImageFileExtension imageExtensionFromFileName(String fileName) {
if (StringUtils.endsWith(fileName.toLowerCase(), ContentRepositoryUtils.ImageFileExtension.GIF.getValue())) {
return ContentRepositoryUtils.ImageFileExtension.GIF;
} else if (StringUtils.endsWith(fileName, ContentRepositoryUtils.ImageFileExtension.PNG.getValue())) {
return ContentRepositoryUtils.ImageFileExtension.PNG;
} else {
return ContentRepositoryUtils.ImageFileExtension.JPEG;
}
}
/**
* Validates that passed in Mime type maps to known image mime types
*
* @param mimeType
*/
public static void validateImageMimeType(final String mimeType) {
if ((!mimeType.equalsIgnoreCase(ImageMIMEtype.GIF.getValue()) && !mimeType.equalsIgnoreCase(ImageMIMEtype.JPEG.getValue())
&& !mimeType.equalsIgnoreCase(ImageMIMEtype.PNG.getValue()))) {
throw new ImageUploadException(mimeType);
}
}
/**
* Extracts Image from a Data URL
*
* @param dataURL
* mimeType
*/
public static Base64EncodedImage extractImageFromDataURL(final String dataURL) {
String fileExtension = "";
String base64EncodedString = null;
if (StringUtils.startsWith(dataURL, ImageDataURIsuffix.GIF.getValue())) {
base64EncodedString = dataURL.replaceAll(ImageDataURIsuffix.GIF.getValue(), "");
fileExtension = ImageFileExtension.GIF.getValue();
} else if (StringUtils.startsWith(dataURL, ImageDataURIsuffix.PNG.getValue())) {
base64EncodedString = dataURL.replaceAll(ImageDataURIsuffix.PNG.getValue(), "");
fileExtension = ImageFileExtension.PNG.getValue();
} else if (StringUtils.startsWith(dataURL, ImageDataURIsuffix.JPEG.getValue())) {
base64EncodedString = dataURL.replaceAll(ImageDataURIsuffix.JPEG.getValue(), "");
fileExtension = ImageFileExtension.JPEG.getValue();
} else {
throw new ImageDataURLNotValidException();
}
return new Base64EncodedImage(base64EncodedString, fileExtension);
}
public static void validateFileSizeWithinPermissibleRange(final Long fileSize, final 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)) > ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB)) {
throw new ContentManagementException(name, fileSize, ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB);
}
}
public static void validateClientImageNotEmpty(final String imageFileName) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
if (imageFileName == null) {
final StringBuilder validationErrorCode = new StringBuilder("validation.msg.clientImage.cannot.be.blank");
final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter image cannot be blank.");
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
defaultEnglishMessage.toString(), "image");
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
/**
* Generate a random String.
*/
@SuppressFBWarnings(value = {
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
public static String generateRandomString() {
final String characters = "abcdefghijklmnopqrstuvwxyz123456789";
// length is a random number between 5 to 16
final int length = random.nextInt(11) + 5;
final char[] text = new char[length];
for (int i = 0; i < length; i++) {
text[i] = characters.charAt(random.nextInt(characters.length()));
}
return new String(text);
}
}
@@ -0,0 +1,138 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
import jakarta.annotation.PostConstruct;
import java.io.BufferedInputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
import org.apache.tika.Tika;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.sax.BodyContentHandler;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class FileSystemContentPathSanitizer implements ContentPathSanitizer {
private final FineractProperties fineractProperties;
private List<Pattern> regexWhitelist;
private static Pattern OVERWRITE_SIBLING_IMAGE = Pattern.compile(".*\\.\\./+[0-9]+/+.*");
@PostConstruct
public void init() {
regexWhitelist = fineractProperties.getContent().getRegexWhitelist().stream().map(Pattern::compile).toList();
}
@Override
public String sanitize(String path) {
return sanitize(path, null);
}
@Override
public String sanitize(String path, BufferedInputStream is) {
try {
if (OVERWRITE_SIBLING_IMAGE.matcher(path).matches()) {
throw new RuntimeException(String.format("Trying to overwrite another resource's image: %s", path));
}
String sanitizedPath = Path.of(path).normalize().toString();
String fileName = FilenameUtils.getName(sanitizedPath).toLowerCase();
if (log.isDebugEnabled()) {
log.debug("Path: {} -> {} ({})", path, sanitizedPath, fileName);
}
if (fineractProperties.getContent().isRegexWhitelistEnabled()) {
boolean matches = regexWhitelist.stream().anyMatch(p -> p.matcher(fileName).matches());
if (!matches) {
throw new RuntimeException(String.format("File name not allowed: %s", fileName));
}
}
if (is != null && fineractProperties.getContent().isMimeWhitelistEnabled()) {
Tika tika = new Tika();
String extensionMimeType = tika.detect(fileName);
if (StringUtils.isEmpty(extensionMimeType)) {
throw new RuntimeException(String.format("Could not detect mime type for filename %s!", fileName));
}
if (!fineractProperties.getContent().getMimeWhitelist().contains(extensionMimeType)) {
throw new RuntimeException(
String.format("Detected mime type %s for filename %s not allowed!", extensionMimeType, fileName));
}
String contentMimeType = detectContentMimeType(is);
if (StringUtils.isEmpty(contentMimeType)) {
throw new RuntimeException(String.format("Could not detect content mime type for %s!", fileName));
}
if (!fineractProperties.getContent().getMimeWhitelist().contains(contentMimeType)) {
throw new RuntimeException(
String.format("Detected content mime type %s for %s not allowed!", contentMimeType, fileName));
}
if (!contentMimeType.equalsIgnoreCase(extensionMimeType)) {
throw new RuntimeException(String.format("Detected filename (%s) and content (%s) mime type do not match!",
extensionMimeType, contentMimeType));
}
}
Path target = Path.of(sanitizedPath);
Path rootFolder = Path.of(fineractProperties.getContent().getFilesystem().getRootFolder(),
ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim());
if (!target.startsWith(rootFolder)) {
throw new RuntimeException(String.format("Path traversal attempt: %s (%s)", target, rootFolder));
}
return sanitizedPath;
} catch (Exception e) {
throw new ContentManagementException(path, e.getMessage(), e);
}
}
private String detectContentMimeType(BufferedInputStream bis) throws Exception {
TikaInputStream tis = TikaInputStream.get(bis);
AutoDetectParser parser = new AutoDetectParser();
// NOTE: turn off write limit with "-1"
BodyContentHandler handler = new BodyContentHandler(-1);
Metadata metadata = new Metadata();
parser.parse(tis, handler, metadata);
return metadata.get("Content-Type");
}
}
@@ -0,0 +1,164 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
import com.google.common.io.Files;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
import org.apache.fineract.infrastructure.documentmanagement.data.ImageData;
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
@ConditionalOnProperty("fineract.content.filesystem.enabled")
public class FileSystemContentRepository implements ContentRepository {
private final FileSystemContentPathSanitizer pathSanitizer;
private final FineractProperties fineractProperties;
@Override
public String saveFile(final InputStream uploadedInputStream, final DocumentCommand documentCommand) {
final String fileName = documentCommand.getFileName();
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName);
final String fileLocation = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId())
+ File.separator + fileName;
return writeFileToFileSystem(fileName, uploadedInputStream, fileLocation);
}
@Override
public String saveImage(final InputStream uploadedInputStream, final Long resourceId, final String imageName, final Long fileSize) {
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName);
final String fileLocation = generateClientImageParentDirectory(resourceId) + File.separator + imageName;
return writeFileToFileSystem(imageName, uploadedInputStream, fileLocation);
}
@Override
public String saveImage(final Base64EncodedImage base64EncodedImage, final Long resourceId, final String imageName) {
final String fileLocation = generateClientImageParentDirectory(resourceId) + File.separator + imageName
+ base64EncodedImage.getFileExtension();
String base64EncodedImageString = base64EncodedImage.getBase64EncodedString();
try {
final InputStream toUploadInputStream = new ByteArrayInputStream(Base64.getMimeDecoder().decode(base64EncodedImageString));
return writeFileToFileSystem(imageName, toUploadInputStream, fileLocation);
} catch (IllegalArgumentException iae) {
log.error("IllegalArgumentException due to invalid Base64 encoding: {}", base64EncodedImageString, iae);
throw iae;
}
}
@Override
public void deleteImage(final String location) {
deleteFileInternal(location);
}
@Override
public void deleteFile(final String documentPath) {
deleteFileInternal(documentPath);
}
private void deleteFileInternal(final String documentPath) {
String sanitizedPath = pathSanitizer.sanitize(documentPath);
final File fileToBeDeleted = new File(sanitizedPath);
final boolean fileDeleted = fileToBeDeleted.delete();
if (!fileDeleted) {
// no need to throw an Error, what's a caller going to do about it, so simply log a warning
log.warn("Unable to delete file {}", documentPath);
}
}
@Override
public FileData fetchFile(final DocumentData documentData) {
String sanitizedPath = pathSanitizer.sanitize(documentData.fileLocation());
final File file = new File(sanitizedPath);
return new FileData(Files.asByteSource(file), documentData.fileName(), documentData.contentType());
}
@Override
public FileData fetchImage(final ImageData imageData) {
String sanitizedPath = pathSanitizer.sanitize(imageData.location());
final File file = new File(sanitizedPath);
return new FileData(Files.asByteSource(file), imageData.getEntityDisplayName(), imageData.contentType().getValue());
}
@Override
public StorageType getStorageType() {
return StorageType.FILE_SYSTEM;
}
/**
* Generate the directory path for storing the new document
*/
private String generateFileParentDirectory(final String entityType, final Long entityId) {
return fineractProperties.getContent().getFilesystem().getRootFolder() + File.separator
+ ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "documents" + File.separator
+ entityType + File.separator + entityId + File.separator + ContentRepositoryUtils.generateRandomString();
}
/**
* Generate ContentRepositoryUtilsfineractProperties.getContentgetWhitelist()getBlacklist() path for storing new
* Image
*/
private String generateClientImageParentDirectory(final Long resourceId) {
return fineractProperties.getContent().getFilesystem().getRootFolder() + File.separator
+ ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "images" + File.separator
+ "clients" + File.separator + resourceId;
}
/**
* Recursively create the directory if it does not exist.
*/
private void makeDirectories(final String uploadDocumentLocation) throws IOException {
String sanitizedPath = pathSanitizer.sanitize(uploadDocumentLocation);
Files.createParentDirs(new File(sanitizedPath));
}
private String writeFileToFileSystem(final String fileName, final InputStream uploadedInputStream, final String fileLocation) {
try (BufferedInputStream bis = new BufferedInputStream(uploadedInputStream)) {
String sanitizedPath = pathSanitizer.sanitize(fileLocation, bis);
makeDirectories(sanitizedPath);
FileUtils.copyInputStreamToFile(bis, new File(sanitizedPath)); // NOSONAR
return sanitizedPath;
} catch (final IOException ioException) {
log.warn("Failed to write file!", ioException);
throw new ContentManagementException(fileName, ioException.getMessage(), ioException);
}
}
}
@@ -0,0 +1,177 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
import com.google.common.io.ByteSource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
import org.apache.fineract.infrastructure.documentmanagement.data.ImageData;
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
import org.apache.fineract.infrastructure.security.utils.LogParameterEscapeUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
@Slf4j
@RequiredArgsConstructor
@Component
@ConditionalOnProperty("fineract.content.s3.enabled")
public class S3ContentRepository implements ContentRepository {
private final S3Client s3Client;
private final FineractProperties fineractProperties;
@Override
public String saveFile(final InputStream toUpload, final DocumentCommand documentCommand) {
final String fileName = documentCommand.getFileName();
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName);
final String uploadDocFolder = generateFileParentDirectory(documentCommand.getParentEntityType(),
documentCommand.getParentEntityId());
final String uploadDocFullPath = uploadDocFolder + File.separator + fileName;
putObject(fileName, toUpload, uploadDocFullPath);
return uploadDocFullPath;
}
@Override
public void deleteFile(final String documentPath) {
deleteObject(documentPath);
}
@Override
public String saveImage(final InputStream toUploadInputStream, final Long resourceId, final String imageName, final Long fileSize) {
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName);
final String uploadImageLocation = generateClientImageParentDirectory(resourceId);
final String fileLocation = uploadImageLocation + File.separator + imageName;
putObject(imageName, toUploadInputStream, fileLocation);
return fileLocation;
}
@Override
public String saveImage(final Base64EncodedImage base64EncodedImage, final Long resourceId, final String imageName) {
final String uploadImageLocation = generateClientImageParentDirectory(resourceId);
final String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension();
final InputStream toUploadInputStream = new ByteArrayInputStream(
Base64.getMimeDecoder().decode(base64EncodedImage.getBase64EncodedString()));
putObject(imageName, toUploadInputStream, fileLocation);
return fileLocation;
}
@Override
public void deleteImage(final String location) {
deleteObject(location);
}
@Override
public FileData fetchFile(final DocumentData documentData) throws DocumentNotFoundException {
return new FileData(new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return s3Client.getObject(GetObjectRequest.builder().bucket(fineractProperties.getContent().getS3().getBucketName())
.key(documentData.fileLocation()).build(), ResponseTransformer.toBytes()).asInputStream();
}
}, documentData.fileName(), documentData.contentType());
}
@Override
public FileData fetchImage(final ImageData imageData) {
return new FileData(new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return s3Client.getObject(GetObjectRequest.builder().bucket(fineractProperties.getContent().getS3().getBucketName())
.key(imageData.location()).build(), ResponseTransformer.toBytes()).asInputStream();
}
}, imageData.getEntityDisplayName(), imageData.contentType().getValue());
}
@Override
public StorageType getStorageType() {
return StorageType.S3;
}
private String generateFileParentDirectory(final String entityType, final Long entityId) {
return "documents" + File.separator + entityType + File.separator + entityId + File.separator
+ ContentRepositoryUtils.generateRandomString();
}
private String generateClientImageParentDirectory(final Long resourceId) {
return "images" + File.separator + "clients" + File.separator + resourceId;
}
private void deleteObject(final String location) {
try {
this.s3Client.deleteObject(builder -> builder.bucket(fineractProperties.getContent().getS3().getBucketName()).key(location));
} catch (final AwsServiceException ase) {
throw new ContentManagementException(location,
"message=" + ase.getMessage() + ", Error Code=" + ase.awsErrorDetails().errorCode(), ase);
} catch (final SdkException ace) {
throw new ContentManagementException(location, ace.getMessage(), ace);
}
}
public void putObject(final String filename, final InputStream inputStream, final String s3UploadLocation)
throws ContentManagementException {
try {
if (log.isDebugEnabled()) {
log.debug("Uploading a new object to S3 {}", LogParameterEscapeUtil.escapeLogParameter(s3UploadLocation));
}
this.s3Client.putObject(
builder -> builder.bucket(fineractProperties.getContent().getS3().getBucketName()).key(s3UploadLocation),
RequestBody.fromBytes(IOUtils.toByteArray(inputStream)));
} catch (AwsServiceException | IOException ase) {
throw new ContentManagementException(filename, ase.getMessage(), ase);
}
}
public ResponseBytes<GetObjectResponse> getObject(String key) {
try {
log.debug("Downloading an object from Amazon S3 Bucket: {}, location: {}",
fineractProperties.getContent().getS3().getBucketName(), key);
return this.s3Client.getObject(builder -> builder.bucket(fineractProperties.getContent().getS3().getBucketName()).key(key),
ResponseTransformer.toBytes());
} catch (SdkException ase) {
throw new ContentManagementException(key, ase.getMessage(), ase);
}
}
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.data;
import com.google.common.io.ByteSource;
public class FileData {
private final String fileName;
private final String contentType;
private final ByteSource byteSource;
public FileData(final ByteSource byteSource, final String fileName, final String contentType) {
this.fileName = fileName;
this.contentType = contentType;
this.byteSource = byteSource;
}
public String contentType() {
return this.contentType;
}
public String name() {
return this.fileName;
}
public ByteSource getByteSource() {
return this.byteSource;
}
}
@@ -0,0 +1,55 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.data;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils.ImageMIMEtype;
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
public class ImageData {
private final String location;
private final StorageType storageType;
private final String entityDisplayName;
private final ContentRepositoryUtils.ImageMIMEtype contentType;
public ImageData(final String location, final StorageType storageType, final String entityDisplayName) {
this.location = location;
this.storageType = storageType;
this.entityDisplayName = entityDisplayName;
this.contentType = ContentRepositoryUtils.ImageMIMEtype
.fromFileExtension(ContentRepositoryUtils.imageExtensionFromFileName(location));
}
public ImageMIMEtype contentType() {
return this.contentType;
}
public StorageType storageType() {
return this.storageType;
}
public String location() {
return this.location;
}
public String getEntityDisplayName() {
return this.entityDisplayName;
}
}
@@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.data;
import com.google.common.io.ByteSource;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import javax.imageio.ImageIO;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils.ImageFileExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class ImageResizer {
private static final Logger LOG = LoggerFactory.getLogger(ImageResizer.class);
public FileData resize(FileData fileData, Integer maxWidth, Integer maxHeight) {
if (maxWidth == null && maxHeight != null) {
return fileData;
}
try (InputStream is = fileData.getByteSource().openBufferedStream()) {
Optional<InputStream> optResizedIS = resizeImage(ContentRepositoryUtils.imageExtensionFromFileName(fileData.name()), is,
maxWidth != null ? maxWidth : Integer.MAX_VALUE, maxHeight != null ? maxHeight : Integer.MAX_VALUE);
if (optResizedIS.isPresent()) {
FileData resizedImage = new FileData(new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return optResizedIS.get(); // NOSONAR
}
}, fileData.name(), fileData.contentType());
return resizedImage;
}
return fileData;
} catch (IOException e) {
LOG.warn("resize() failed, returning original image: {}", e.getMessage(), e);
return fileData;
}
}
private Optional<InputStream> resizeImage(ImageFileExtension fileExtension, InputStream in, int maxWidth, int maxHeight)
throws IOException {
BufferedImage src = ImageIO.read(in);
if (src.getWidth() <= maxWidth && src.getHeight() <= maxHeight) {
return Optional.empty();
}
float widthRatio = (float) src.getWidth() / maxWidth;
float heightRatio = (float) src.getHeight() / maxHeight;
float scaleRatio = widthRatio > heightRatio ? widthRatio : heightRatio;
// TODO(lindahl): Improve compressed image quality (perhaps quality ratio)
int newWidth = (int) (src.getWidth() / scaleRatio);
int newHeight = (int) (src.getHeight() / scaleRatio);
int colorModel = fileExtension == ContentRepositoryUtils.ImageFileExtension.JPEG ? BufferedImage.TYPE_INT_RGB
: BufferedImage.TYPE_INT_ARGB;
BufferedImage target = new BufferedImage(newWidth, newHeight, colorModel);
Graphics2D g = target.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(src, 0, 0, newWidth, newHeight, Color.BLACK, null);
g.dispose();
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(target, fileExtension != null ? fileExtension.getValueWithoutDot() : "jpeg", os);
return Optional.of(new ByteArrayInputStream(os.toByteArray()));
}
}
@@ -0,0 +1,167 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
@Entity
@Table(name = "m_document")
public class Document extends AbstractPersistableCustom {
@Column(name = "parent_entity_type", length = 50)
private String parentEntityType;
@Column(name = "parent_entity_id", length = 1000)
private Long parentEntityId;
@Column(name = "name", length = 250)
private String name;
@Column(name = "file_name", length = 250)
private String fileName;
@Column(name = "size")
private Long size;
@Column(name = "type", length = 50)
private String type;
@Column(name = "description", length = 1000)
private String description;
@Column(name = "location", length = 500)
private String location;
@Column(name = "storage_type_enum")
private Integer storageType;
public Document() {}
public static Document createNew(final String parentEntityType, final Long parentEntityId, final String name, final String fileName,
final Long size, final String type, final String description, final String location, final StorageType storageType) {
return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location, storageType);
}
private Document(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size,
final String type, final String description, final String location, final StorageType storageType) {
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);
this.storageType = storageType.getValue();
}
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();
}
}
public String getParentEntityType() {
return this.parentEntityType;
}
public void setParentEntityType(final String parentEntityType) {
this.parentEntityType = parentEntityType;
}
public Long getParentEntityId() {
return this.parentEntityId;
}
public void setParentEntityId(final Long parentEntityId) {
this.parentEntityId = parentEntityId;
}
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
public String getFileName() {
return this.fileName;
}
public void setFileName(final String fileName) {
this.fileName = fileName;
}
public Long getSize() {
return this.size;
}
public void setSize(final Long size) {
this.size = size;
}
public String getType() {
return this.type;
}
public void setType(final String type) {
this.type = type;
}
public String getDescription() {
return this.description;
}
public void setDescription(final String description) {
this.description = description;
}
public String getLocation() {
return this.location;
}
public void setLocation(final String location) {
this.location = location;
}
public StorageType storageType() {
return StorageType.fromInt(this.storageType);
}
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface DocumentRepository extends JpaRepository<Document, Long>, JpaSpecificationExecutor<Document> {
// no added behaviour
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.domain;
import org.apache.fineract.portfolio.client.domain.Client;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ImageRepository extends JpaRepository<Image, Long>, JpaSpecificationExecutor<Client> {}
@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ContentManagementException extends AbstractPlatformDomainRuleException {
public ContentManagementException(final String filename, final String message) {
super("error.msg.document.save", "Error while manipulating file " + filename + " due to a ContentRepository issue " + message,
filename, message);
}
public ContentManagementException(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 " + maxFileSize + " MB", name, fileSize);
}
public ContentManagementException(String filename, String message, Exception exception) {
super("error.msg.document.save", "Error while manipulating file " + filename + " due to a ContentRepository issue " + message,
filename, message, exception);
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import software.amazon.awssdk.core.exception.SdkException;
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);
}
public DocumentNotFoundException(String entityType, Long entityId, Long id, EmptyResultDataAccessException e) {
super("error.msg.document.id.invalid",
"Document with identifier " + id + " does not exist for the " + entityType + " with Identifier " + entityId, id, e);
}
public DocumentNotFoundException(String entityType, Long entityId, Long id, SdkException ace) {
super("error.msg.document.id.invalid",
"Document with identifier " + id + " does not exist for the " + entityType + " with Identifier " + entityId, id, ace);
}
}
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
/**
* 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);
}
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
/**
* Runtime exception for invalid image types
*/
public class InvalidEntityTypeForImageManagementException extends AbstractPlatformResourceNotFoundException {
public InvalidEntityTypeForImageManagementException(String imageType) {
super("error.imagemanagement.entitytype.invalid", "Image Management is not support for the Entity Type: " + imageType);
}
}
@@ -0,0 +1,33 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
import java.util.Collection;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
public interface DocumentReadPlatformService {
Collection<DocumentData> retrieveAllDocuments(String entityType, Long entityId);
FileData retrieveFileData(String entityType, Long entityId, Long documentId);
DocumentData retrieveDocument(String entityType, Long entityId, Long documentId);
}
@@ -0,0 +1,125 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepository;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
private final ContentRepositoryFactory contentRepositoryFactory;
@Override
public Collection<DocumentData> retrieveAllDocuments(final String entityType, final Long entityId) {
this.context.authenticatedUser();
// TODO verify if the entities are valid and a user
// has data
// scope for the particular entities
final DocumentMapper mapper = new DocumentMapper(true, true);
final String sql = "select " + mapper.schema() + " order by d.id";
return this.jdbcTemplate.query(sql, mapper, new Object[] { entityType, entityId }); // NOSONAR
}
@Override
public FileData retrieveFileData(final String entityType, final Long entityId, final Long documentId) {
try {
final DocumentMapper mapper = new DocumentMapper(false, false);
final DocumentData documentData = fetchDocumentDetails(entityType, entityId, documentId, mapper);
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(documentData.storageType());
return contentRepository.fetchFile(documentData);
} catch (final EmptyResultDataAccessException e) {
throw new DocumentNotFoundException(entityType, entityId, documentId, e);
}
}
@Override
public DocumentData retrieveDocument(final String entityType, final Long entityId, final Long documentId) {
try {
final DocumentMapper mapper = new DocumentMapper(true, true);
return fetchDocumentDetails(entityType, entityId, documentId, mapper);
} catch (final EmptyResultDataAccessException e) {
throw new DocumentNotFoundException(entityType, entityId, documentId, e);
}
}
private DocumentData fetchDocumentDetails(final String entityType, final Long entityId, final Long documentId,
final DocumentMapper mapper) {
final String sql = "select " + mapper.schema() + " and d.id=? ";
return this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { entityType, entityId, documentId }); // NOSONAR
}
private static final class DocumentMapper implements RowMapper<DocumentData> {
private final boolean hideLocation;
private final boolean hideStorageType;
DocumentMapper(final boolean hideLocation, final boolean hideStorageType) {
this.hideLocation = hideLocation;
this.hideStorageType = hideStorageType;
}
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," + " d.storage_type_enum as storageType"
+ " from m_document d where d.parent_entity_type=? and d.parent_entity_id=?";
}
@Override
public DocumentData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = JdbcSupport.getLong(rs, "id");
final Long parentEntityId = JdbcSupport.getLong(rs, "parentEntityId");
final Long fileSize = JdbcSupport.getLong(rs, "fileSize");
final String parentEntityType = rs.getString("parentEntityType");
final String name = rs.getString("name");
final String fileName = rs.getString("fileName");
final String fileType = rs.getString("fileType");
final String description = rs.getString("description");
String location = null;
Integer storageType = null;
if (!this.hideLocation) {
location = rs.getString("location");
}
if (!this.hideStorageType) {
storageType = rs.getInt("storageType");
}
return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location,
storageType);
}
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
import java.io.InputStream;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.springframework.security.access.prepost.PreAuthorize;
public interface DocumentWritePlatformService {
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_DOCUMENT')")
Long createDocument(DocumentCommand documentCommand, InputStream inputStream);
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'UPDATE_DOCUMENT')")
CommandProcessingResult updateDocument(DocumentCommand documentCommand, InputStream inputStream);
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'DELETE_DOCUMENT')")
CommandProcessingResult deleteDocument(DocumentCommand documentCommand);
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_DOCUMENT')")
Long createInternalDocument(String entityType, Long entityId, Long fileSize, InputStream inputStream, String mimeType, String name,
String description, String fileName);
}
@@ -0,0 +1,189 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
import java.io.InputStream;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommandValidator;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepository;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory;
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
import org.apache.fineract.infrastructure.documentmanagement.exception.InvalidEntityTypeForDocumentManagementException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(DocumentWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final DocumentRepository documentRepository;
private final ContentRepositoryFactory contentRepositoryFactory;
@Autowired
public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DocumentRepository documentRepository,
final ContentRepositoryFactory documentStoreFactory) {
this.context = context;
this.documentRepository = documentRepository;
this.contentRepositoryFactory = documentStoreFactory;
}
@Transactional
@Override
public Long createDocument(final DocumentCommand documentCommand, final InputStream inputStream) {
try {
this.context.authenticatedUser();
final DocumentCommandValidator validator = new DocumentCommandValidator(documentCommand);
validateParentEntityType(documentCommand);
validator.validateForCreate();
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository();
final String fileLocation = contentRepository.saveFile(inputStream, documentCommand);
final Document document = Document.createNew(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(),
documentCommand.getName(), documentCommand.getFileName(), documentCommand.getSize(), documentCommand.getType(),
documentCommand.getDescription(), fileLocation, contentRepository.getStorageType());
this.documentRepository.saveAndFlush(document);
return document.getId();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
LOG.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.document.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.");
}
}
@Transactional
@Override
public Long createInternalDocument(final String entityType, final Long entityId, final Long fileSize, final InputStream inputStream,
final String mimeType, final String name, final String description, final String fileName) {
final DocumentCommand documentCommand = new DocumentCommand(null, null, entityType, entityId, name, fileName, fileSize, mimeType,
description, null);
final Long documentId = createDocument(documentCommand, inputStream);
return documentId;
}
@Transactional
@Override
public CommandProcessingResult updateDocument(final DocumentCommand documentCommand, final InputStream inputStream) {
try {
this.context.authenticatedUser();
String oldLocation = null;
final DocumentCommandValidator validator = new DocumentCommandValidator(documentCommand);
validator.validateForUpdate();
// TODO check if entity id is valid and within data scope for the
// user
final Document documentForUpdate = this.documentRepository.findById(documentCommand.getId())
.orElseThrow(() -> new DocumentNotFoundException(documentCommand.getParentEntityType(),
documentCommand.getParentEntityId(), documentCommand.getId()));
final StorageType documentStoreType = documentForUpdate.storageType();
oldLocation = documentForUpdate.getLocation();
if (inputStream != null && documentCommand.isFileNameChanged()) {
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository();
documentCommand.setLocation(contentRepository.saveFile(inputStream, documentCommand));
documentCommand.setStorageType(contentRepository.getStorageType().getValue());
}
documentForUpdate.update(documentCommand);
if (inputStream != null && documentCommand.isFileNameChanged()) {
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(documentStoreType);
contentRepository.deleteFile(oldLocation);
}
this.documentRepository.saveAndFlush(documentForUpdate);
return CommandProcessingResult.resourceResult(documentForUpdate.getId());
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
LOG.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.document.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.");
} catch (final ContentManagementException cme) {
LOG.error("Error occured.", cme);
throw new ContentManagementException(documentCommand.getName(), cme.getMessage(), cme);
}
}
@Transactional
@Override
public CommandProcessingResult deleteDocument(final DocumentCommand documentCommand) {
this.context.authenticatedUser();
validateParentEntityType(documentCommand);
// TODO: Check document is present under this entity Id
final Document document = this.documentRepository.findById(documentCommand.getId())
.orElseThrow(() -> new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(),
documentCommand.getId()));
this.documentRepository.delete(document);
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(document.storageType());
contentRepository.deleteFile(document.getLocation());
return CommandProcessingResult.resourceResult(document.getId());
}
private void validateParentEntityType(final DocumentCommand documentCommand) {
if (!checkValidEntityType(documentCommand.getParentEntityType())) {
throw new InvalidEntityTypeForDocumentManagementException(documentCommand.getParentEntityType());
}
}
private static boolean checkValidEntityType(final String entityType) {
for (final DocumentManagementEntity entities : DocumentManagementEntity.values()) {
if (entities.name().equalsIgnoreCase(entityType)) {
return true;
}
}
return false;
}
/*** Entities for document Management **/
public enum DocumentManagementEntity {
CLIENTS, CLIENT_IDENTIFIERS, STAFF, LOANS, SAVINGS, GROUPS, IMPORT;
@Override
public String toString() {
return name().toString().toLowerCase();
}
}
}
@@ -0,0 +1,27 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
public interface ImageReadPlatformService {
FileData retrieveImage(String entityType, Long entityId);
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
import java.io.InputStream;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
import org.springframework.security.access.prepost.PreAuthorize;
public interface ImageWritePlatformService {
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE','CREATE_STAFFIMAGE')")
CommandProcessingResult saveOrUpdateImage(String entityName, Long entityId, String imageName, InputStream inputStream, Long fileSize);
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE','CREATE_STAFFIMAGE')")
CommandProcessingResult saveOrUpdateImage(String entityName, Long entityId, Base64EncodedImage encodedImage);
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'DELETE_CLIENTIMAGE','DELETE_STAFFIMAGE')")
CommandProcessingResult deleteImage(String entityName, Long entityId);
}
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<!-- This file is only used for static weaving, nothing more. -->
<!-- You can find the runtime configuration in the JPAConfig class -->
<persistence-unit name="jpa-pu" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<!-- Fineract core module -->
<class>org.apache.fineract.accounting.glaccount.domain.GLAccount</class>
<class>org.apache.fineract.accounting.journalentry.domain.JournalEntry</class>
<class>org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom</class>
<class>org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom</class>
<class>org.apache.fineract.infrastructure.codes.domain.Code</class>
<class>org.apache.fineract.infrastructure.codes.domain.CodeValue</class>
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Image</class>
<class>org.apache.fineract.organisation.staff.domain.Staff</class>
<class>org.apache.fineract.organisation.office.domain.Office</class>
<class>org.apache.fineract.organisation.office.domain.OrganisationCurrency</class>
<class>org.apache.fineract.organisation.monetary.domain.ApplicationCurrency</class>
<class>org.apache.fineract.organisation.holiday.domain.Holiday</class>
<class>org.apache.fineract.organisation.workingdays.domain.WorkingDays</class>
<class>org.apache.fineract.portfolio.group.domain.Group</class>
<class>org.apache.fineract.portfolio.group.domain.GroupLevel</class>
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
<class>org.apache.fineract.portfolio.client.domain.Client</class>
<class>org.apache.fineract.portfolio.fund.domain.Fund</class>
<class>org.apache.fineract.portfolio.charge.domain.Charge</class>
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
<class>org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail</class>
<class>org.apache.fineract.portfolio.tax.domain.TaxGroup</class>
<class>org.apache.fineract.portfolio.tax.domain.TaxGroupMappings</class>
<class>org.apache.fineract.portfolio.tax.domain.TaxComponent</class>
<class>org.apache.fineract.portfolio.tax.domain.TaxComponentHistory</class>
<class>org.apache.fineract.portfolio.calendar.domain.Calendar</class>
<class>org.apache.fineract.portfolio.calendar.domain.CalendarHistory</class>
<class>org.apache.fineract.portfolio.calendar.domain.CalendarInstance</class>
<class>org.apache.fineract.useradministration.domain.AppUser</class>
<class>org.apache.fineract.useradministration.domain.Role</class>
<class>org.apache.fineract.useradministration.domain.Permission</class>
<class>org.apache.fineract.useradministration.domain.AppUserClientMapping</class>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="eclipselink.weaving" value="static" />
</properties>
</persistence-unit>
</persistence>