FINERACT-1932: Fineract modularization - fineract-document
This commit is contained in:
committed by
Adam Saghy
parent
ce2f05ee78
commit
2b9e114c05
+61
@@ -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);
|
||||
}
|
||||
}
|
||||
+233
@@ -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);
|
||||
}
|
||||
}
|
||||
+149
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -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();
|
||||
}
|
||||
}
|
||||
+232
@@ -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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+144
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
+56
@@ -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();
|
||||
|
||||
}
|
||||
+53
@@ -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)));
|
||||
}
|
||||
}
|
||||
+204
@@ -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);
|
||||
}
|
||||
}
|
||||
+138
@@ -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");
|
||||
}
|
||||
}
|
||||
+164
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+177
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+55
@@ -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;
|
||||
}
|
||||
}
|
||||
+93
@@ -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()));
|
||||
}
|
||||
}
|
||||
+167
@@ -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);
|
||||
}
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
+25
@@ -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> {}
|
||||
+39
@@ -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);
|
||||
}
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
|
||||
}
|
||||
+125
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -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);
|
||||
|
||||
}
|
||||
+189
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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);
|
||||
|
||||
}
|
||||
+36
@@ -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>
|
||||
Reference in New Issue
Block a user