diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
index cab710d68..271bf5d8a 100644
--- a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
+++ b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
@@ -114,6 +114,23 @@ dependencyManagement {
exclude 'org.apache.commons:commons-compress'
exclude 'xml-apis:xml-apis'
}
+ dependency ('org.apache.tika:tika-parser-image-module:3.2.3') {
+ exclude 'org.bouncycastle:bcprov-jdk15on'
+ exclude 'org.bouncycastle:bcjmail-jdk15on'
+ exclude 'org.bouncycastle:bcprov-jdk18on'
+ exclude 'org.bouncycastle:bcjmail-jdk18on'
+ exclude 'commons-logging:commons-logging'
+ exclude 'org.apache.logging.log4j:log4j-api'
+ exclude 'org.slf4j:slf4j-api'
+ exclude 'commons-io:commons-io'
+ exclude 'commons-codec:commons-codec'
+ exclude 'org.apache.commons:commons-lang3'
+ exclude 'org.apache.poi:poi'
+ exclude 'org.apache.poi:poi-scratchpad'
+ exclude 'org.glassfish.jaxb:jaxb-runtime'
+ exclude 'org.apache.commons:commons-compress'
+ exclude 'xml-apis:xml-apis'
+ }
dependency ('org.apache.httpcomponents:httpclient:4.5.14') {
exclude 'commons-logging:commons-logging:'
}
@@ -265,6 +282,12 @@ dependencyManagement {
dependency 'com.lmax:disruptor:3.4.4'
+ dependency ('com.github.romankh3:image-comparison:4.4.0') {
+ exclude 'org.junit.jupiter:junit-jupiter-api'
+ exclude 'org.junit.jupiter:junit-jupiter-engine'
+ exclude 'org.mockito:mockito-core'
+ }
+
dependency 'com.ibm.icu:icu4j:76.1'
dependency 'org.yakworks:spring-icu4j:0.4.2'
dependency 'org.apache.commons:commons-lang3:3.18.0'
diff --git a/config/docker/compose/fineract.yml b/config/docker/compose/fineract.yml
index 801b73fc6..4d92fdf49 100644
--- a/config/docker/compose/fineract.yml
+++ b/config/docker/compose/fineract.yml
@@ -18,12 +18,13 @@
services:
fineract:
- # user: "${FINERACT_USER}:${FINERACT_GROUP}"
image: fineract:latest
+ user: "${FINERACT_USER}:${FINERACT_GROUP}"
volumes:
- ${PWD}/config/docker/logback/logback-override.xml:/app/logback-override.xml
- ${PWD}/config/docker/aws/etc/credentials:/etc/aws/credentials:ro
- ${PWD}/build/fineract/logs:/var/logs/fineract:rw
+ # - ${PWD}/build/fineract/tmp:/tmp:rw
healthcheck:
test: ["CMD", 'sh', '-c', 'echo -e "Checking for the availability of Fineract server deployment"; while ! nc -z "localhost" 8443; do sleep 1; printf "-"; done; echo -e " >> Fineract server has started";' ]
timeout: 1s
diff --git a/config/docker/env/fineract-common.env b/config/docker/env/fineract-common.env
index 4b0dc2465..78ac9951f 100644
--- a/config/docker/env/fineract-common.env
+++ b/config/docker/env/fineract-common.env
@@ -54,6 +54,7 @@ FINERACT_MANAGEMENT_ENDPOINT_WEB_EXPOSURE_INCLUDE=health,info,prometheus
FINERACT_MANAGEMENT_METRICS_TAGS_APPLICATION=fineract
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED=true
FINERACT_INSECURE_HTTP_CLIENT=true
+FINERACT_CONTENT_FILESYSTEM_ROOT_FOLDER=/tmp
SPRING_PROFILES_ACTIVE=test,diagnostics
OTEL_SERVICE_NAME=fineract
-JAVA_TOOL_OPTIONS=" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5000 -Xmx1G -XX:MinRAMPercentage=25 -XX:MaxRAMPercentage=80 -XX:TieredStopAtLevel=1 -XX:+UseContainerSupport -XX:+UseStringDeduplication --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED"
+JAVA_TOOL_OPTIONS="-Xmx1G -XX:MinRAMPercentage=25 -XX:MaxRAMPercentage=80 -XX:TieredStopAtLevel=1 -XX:+UseContainerSupport -XX:+UseStringDeduplication -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5000 --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED"
diff --git a/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml b/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml
index 60e55ff84..81504a3f7 100644
--- a/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml
+++ b/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml b/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml
index 5e972e532..9d976c68d 100644
--- a/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml
+++ b/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml b/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml
index 695790147..fb0b9cf94 100644
--- a/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml
+++ b/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/DocumentsApiFixed.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/DocumentsApiFixed.java
index 39fd22156..1d9aad236 100644
--- a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/DocumentsApiFixed.java
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/DocumentsApiFixed.java
@@ -22,10 +22,10 @@ import feign.Param;
import feign.RequestLine;
import feign.Response;
import java.util.List;
-import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.client.models.DocumentCreateResponse;
import org.apache.fineract.client.models.DocumentData;
-import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
-import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.client.models.DocumentDeleteResponse;
+import org.apache.fineract.client.models.DocumentUpdateResponse;
/**
* This class was originally generated by OpenAPI Generator, but then had
@@ -44,7 +44,7 @@ public interface DocumentsApiFixed {
* @return PostEntityTypeEntityIdDocumentsResponse
*/
@RequestLine("POST /v1/{entityType}/{entityId}/documents")
- PostEntityTypeEntityIdDocumentsResponse createDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
+ DocumentCreateResponse createDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
/**
@@ -59,7 +59,7 @@ public interface DocumentsApiFixed {
* @return DeleteEntityTypeEntityIdDocumentsResponse
*/
@RequestLine("DELETE /v1/{entityType}/{entityId}/documents/{documentId}")
- DeleteEntityTypeEntityIdDocumentsResponse deleteDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
+ DocumentDeleteResponse deleteDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
@Param("documentId") Long documentId);
/**
@@ -114,6 +114,6 @@ public interface DocumentsApiFixed {
* @return PutEntityTypeEntityIdDocumentsResponse
*/
@RequestLine("PUT /v1/{entityType}/{entityId}/documents/{documentId}")
- PutEntityTypeEntityIdDocumentsResponse updateDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
+ DocumentUpdateResponse updateDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
@Param("documentId") Long documentId, org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/services/DocumentsApiFixedIntegrationTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/DocumentsApiFixedIntegrationTest.java
index d19703607..b59be072c 100644
--- a/fineract-client-feign/src/test/java/org/apache/fineract/client/services/DocumentsApiFixedIntegrationTest.java
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/DocumentsApiFixedIntegrationTest.java
@@ -35,7 +35,6 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.fineract.client.feign.FineractFeignClientConfig;
import org.apache.fineract.client.feign.services.DocumentsApiFixed;
-import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
import org.apache.fineract.client.models.DocumentData;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -149,7 +148,7 @@ class DocumentsApiFixedIntegrationTest {
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
- DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("clients", 123L, 456L);
+ var response = api.deleteDocument("clients", 123L, 456L);
assertThat(response).isNotNull();
assertThat(response.getResourceId()).isEqualTo(456L);
@@ -162,7 +161,7 @@ class DocumentsApiFixedIntegrationTest {
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
- DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("loans", 789L, 999L);
+ var response = api.deleteDocument("loans", 789L, 999L);
assertThat(response).isNotNull();
assertThat(response.getResourceId()).isEqualTo(999L);
diff --git a/fineract-client/build.gradle b/fineract-client/build.gradle
index 3017b6dfd..4acfe358a 100644
--- a/fineract-client/build.gradle
+++ b/fineract-client/build.gradle
@@ -17,6 +17,7 @@
* under the License.
*/
apply plugin: 'org.openapi.generator'
+apply plugin: 'idea'
description = 'Fineract Client'
apply from: 'dependencies.gradle'
@@ -35,6 +36,19 @@ openApiValidate {
recommend = true
}
+openApiGenerate {
+ outputDir = "$buildDir/generated/temp-java".toString()
+}
+
+idea {
+ module {
+ // Marks the generated directory as a Source Root
+ sourceDirs += file("$buildDir/generated/temp-java/src/main/java")
+ // Or use generatedSourceDirs for explicit "Generated Source" marking
+ generatedSourceDirs += file("$buildDir/generated/temp-java/src/main/java")
+ }
+}
+
task buildJavaSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
generatorName = 'java'
verbose = false
diff --git a/fineract-client/src/main/java/org/apache/fineract/client/services/DocumentsApiFixed.java b/fineract-client/src/main/java/org/apache/fineract/client/services/DocumentsApiFixed.java
index 8d56912fb..647054177 100644
--- a/fineract-client/src/main/java/org/apache/fineract/client/services/DocumentsApiFixed.java
+++ b/fineract-client/src/main/java/org/apache/fineract/client/services/DocumentsApiFixed.java
@@ -20,10 +20,10 @@ package org.apache.fineract.client.services;
import java.util.List;
import okhttp3.ResponseBody;
-import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.client.models.DocumentCreateResponse;
import org.apache.fineract.client.models.DocumentData;
-import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
-import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.client.models.DocumentDeleteResponse;
+import org.apache.fineract.client.models.DocumentUpdateResponse;
import retrofit2.Call;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
@@ -54,7 +54,7 @@ public interface DocumentsApiFixed {
*/
@retrofit2.http.Multipart
@POST("v1/{entityType}/{entityId}/documents")
- Call createDocument(@retrofit2.http.Path("entityType") String entityType,
+ Call createDocument(@retrofit2.http.Path("entityType") String entityType,
@retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Part okhttp3.MultipartBody.Part file,
@retrofit2.http.Part("name") String name, @retrofit2.http.Part("description") String description);
@@ -70,7 +70,7 @@ public interface DocumentsApiFixed {
* @return Call<DeleteEntityTypeEntityIdDocumentsResponse>
*/
@DELETE("v1/{entityType}/{entityId}/documents/{documentId}")
- Call deleteDocument(@retrofit2.http.Path("entityType") String entityType,
+ Call deleteDocument(@retrofit2.http.Path("entityType") String entityType,
@retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Path("documentId") Long documentId);
/**
@@ -139,7 +139,7 @@ public interface DocumentsApiFixed {
*/
@retrofit2.http.Multipart
@PUT("v1/{entityType}/{entityId}/documents/{documentId}")
- Call updateDocument(@retrofit2.http.Path("entityType") String entityType,
+ Call updateDocument(@retrofit2.http.Path("entityType") String entityType,
@retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Path("documentId") Long documentId,
@retrofit2.http.Part okhttp3.MultipartBody.Part file, @retrofit2.http.Part("name") String name,
@retrofit2.http.Part("description") String description);
diff --git a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
index 78d58dca7..509a60dd9 100644
--- a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
+++ b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
@@ -33,7 +33,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandAuditor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandAuditor.java
index 410560f9a..6946b8cd2 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandAuditor.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandAuditor.java
@@ -114,6 +114,7 @@ public class DefaultCommandAuditor implements CommandAuditor {
final var now = Instant.now();
final var commandEntity = mapper.map(command);
+ commandEntity.setExecutedAt(now);
commandEntity.setUpdatedAt(now);
commandEntity.setState(ERROR);
@@ -138,7 +139,17 @@ public class DefaultCommandAuditor implements CommandAuditor {
@Retry(name = "commandAuditFallback", fallbackMethod = "fatal")
@SuppressWarnings("UnusedMethod")
- private void fallback(Command> command, Throwable t) throws Exception {
+ void fallback(Command> command, Throwable t) throws Exception {
+ if (Boolean.TRUE.equals(properties.getFileDeadLetterQueueEnabled())) {
+ fallback(command, null, t);
+ } else {
+ fatal(command, t);
+ }
+ }
+
+ @Retry(name = "commandAuditFallback", fallbackMethod = "fatal")
+ @SuppressWarnings("UnusedMethod")
+ void fallback(Command> command, Object response, Throwable t) throws Exception {
if (Boolean.TRUE.equals(properties.getFileDeadLetterQueueEnabled())) {
setError(command, t);
write(command);
@@ -148,7 +159,12 @@ public class DefaultCommandAuditor implements CommandAuditor {
}
@SuppressWarnings("UnusedMethod")
- private void fatal(Command> command, Throwable t) {
+ void fatal(Command> command, Throwable t) {
+ fatal(command, null, t);
+ }
+
+ @SuppressWarnings("UnusedMethod")
+ void fatal(Command> command, Object response, Throwable t) {
// note: last line of defense; if this fails then all is lost
setError(command, t);
log.error("Command audit error: {}", command, t);
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
index 43d3a73b0..110908f75 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
@@ -28,7 +28,7 @@ import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
@Configuration
-@EnableJdbcRepositories(basePackages = { "org.apache.fineract.command.persistence" })
+@EnableJdbcRepositories(basePackages = { "org.apache.fineract.**.domain", "org.apache.fineract.**.persistence" })
@ComponentScan("org.apache.fineract.command.persistence")
class CommandPersistenceConfiguration {
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/ImportData.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/ImportData.java
index 445f3667f..2b8dc3a68 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/ImportData.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/ImportData.java
@@ -19,7 +19,13 @@
package org.apache.fineract.infrastructure.bulkimport.data;
import java.time.LocalDate;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
public final class ImportData {
@SuppressWarnings("unused")
@@ -27,6 +33,8 @@ public final class ImportData {
@SuppressWarnings("unused")
private Long documentId;
@SuppressWarnings("unused")
+ private Integer entityType;
+ @SuppressWarnings("unused")
private String name;
@SuppressWarnings("unused")
private LocalDate importTime;
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookService.java
index 30dd64e46..68889f9dd 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookService.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookService.java
@@ -18,12 +18,10 @@
*/
package org.apache.fineract.infrastructure.bulkimport.service;
-import jakarta.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Collection;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
import org.apache.fineract.infrastructure.bulkimport.data.ImportData;
-import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
public interface BulkImportWorkbookService {
@@ -33,8 +31,5 @@ public interface BulkImportWorkbookService {
Collection getImports(GlobalEntityType type);
- DocumentData getOutputTemplateLocation(String importDocumentId);
-
- Response getOutputTemplate(String importDocumentId);
-
+ ImportData getImport(Long id);
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
index 9730467be..26100f390 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
@@ -364,6 +364,7 @@ public class FineractProperties {
private List regexWhitelist;
private boolean mimeWhitelistEnabled;
private List mimeWhitelist;
+ private Integer defaultBufferSize;
private FineractContentFilesystemProperties filesystem;
private FineractContentS3Properties s3;
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntityImageIdAdapter.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntityImageIdAdapter.java
new file mode 100644
index 000000000..f763f7268
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntityImageIdAdapter.java
@@ -0,0 +1,51 @@
+/**
+ * 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.adapter;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Optional;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+// NOTE: this is a trick to decouple the entity image IDs from the image service
+@Deprecated
+public interface EntityImageIdAdapter {
+
+ boolean accept(String entityType);
+
+ Optional get(Long entityId);
+
+ Optional set(Long entityId, Long imageId);
+
+ @Builder
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ class ImageIdResult implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Long id;
+ private String displayName;
+ }
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java
index 312344862..7fc73a2fc 100644
--- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java
+++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java
@@ -40,7 +40,6 @@ public class CurrencyUpdateCommandHandler implements CommandHandler command) {
- // NOTE: fallback method needs to be in the same class
return writePlatformService.updateAllowedCurrencies(command.getPayload());
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java b/fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java
index 7830c40f6..477c3df13 100644
--- a/fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java
+++ b/fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java
@@ -22,7 +22,6 @@ import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
-import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.LocalDate;
@@ -33,7 +32,6 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
import org.apache.fineract.organisation.office.domain.Office;
@Getter
@@ -81,9 +79,8 @@ public class Staff extends AbstractPersistableCustom {
@JoinColumn(name = "organisational_role_parent_staff_id")
private Staff organisationalRoleParentStaff;
- @OneToOne(optional = true)
- @JoinColumn(name = "image_id")
- private Image image;
+ @Column(name = "image_id")
+ private Long imageId;
public static Staff fromJson(final Office staffOffice, final JsonCommand command) {
@@ -262,8 +259,4 @@ public class Staff extends AbstractPersistableCustom {
return this.office;
}
- public void setImage(Image image) {
- this.image = image;
- }
-
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientImageIdAdapter.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientImageIdAdapter.java
new file mode 100644
index 000000000..b46c79cc5
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientImageIdAdapter.java
@@ -0,0 +1,66 @@
+/**
+ * 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.portfolio.client.adapter;
+
+import static java.util.Objects.nonNull;
+
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
+import org.apache.fineract.portfolio.client.domain.ClientRepository;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+@Deprecated
+class ClientImageIdAdapter implements EntityImageIdAdapter {
+
+ private static final String ENTITY_TYPE = "clients";
+
+ private final ClientRepository repository;
+
+ @Override
+ public boolean accept(String entityType) {
+ return ENTITY_TYPE.equalsIgnoreCase(entityType);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Optional get(Long entityId) {
+ return repository.findById(entityId).filter(client -> nonNull(client.getImageId()))
+ .map(client -> ImageIdResult.builder().id(client.getImageId()).displayName(client.getDisplayName()).build());
+ }
+
+ @Override
+ @Transactional
+ public Optional set(Long entityId, Long imageId) {
+ final var result = get(entityId);
+
+ if (imageId == null) {
+ repository.removeImageId(entityId);
+ } else {
+ repository.updateByIdAndImageId(entityId, imageId);
+ }
+
+ return result;
+ }
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java
index 60fafa98a..951128679 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java
@@ -27,7 +27,6 @@ import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
-import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.LocalDate;
@@ -46,7 +45,6 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff;
@@ -72,13 +70,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom {
@JoinColumn(name = "transfer_to_office_id")
private Office transferToOffice;
- @OneToOne(optional = true)
- @JoinColumn(name = "image_id")
- private Image image;
+ @Column(name = "image_id")
+ private Long imageId;
- /**
- * A value from {@link ClientStatus}.
- */
@Column(name = "status_enum", nullable = false)
private Integer status;
@@ -494,10 +488,6 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom {
return this.office.getId();
}
- public void setImage(final Image image) {
- this.image = image;
- }
-
public String mobileNo() {
return this.mobileNo;
}
@@ -510,10 +500,6 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom {
this.mobileNo = mobileNo;
}
- public boolean isNotStaff() {
- return !isStaff();
- }
-
public boolean isStaff() {
return this.isStaff;
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java
index f3a9ff112..055789653 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java
@@ -22,6 +22,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.client.domain.search.SearchingClientRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -46,4 +47,11 @@ public interface ClientRepository extends JpaRepository, JpaSpecif
@Query("SELECT c.id FROM Client c WHERE c.externalId = :externalId")
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+ @Modifying
+ @Query("UPDATE Client client SET client.imageId = :imageId WHERE client.id = :clientId")
+ void updateByIdAndImageId(@Param("clientId") Long staffId, @Param("imageId") Long imageId);
+
+ @Modifying
+ @Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId")
+ void removeImageId(@Param("clientId") Long clientId);
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/util/StreamResponseUtil.java b/fineract-core/src/main/java/org/apache/fineract/util/StreamResponseUtil.java
new file mode 100644
index 000000000..9beb490df
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/util/StreamResponseUtil.java
@@ -0,0 +1,89 @@
+/**
+ * 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.util;
+
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.StreamingOutput;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+@Slf4j
+public final class StreamResponseUtil {
+
+ public static String DISPOSITION_TYPE_ATTACHMENT = "attachment";
+ public static String DISPOSITION_TYPE_INLINE = "inline";
+
+ private static final ExecutorService executor = Executors.newCachedThreadPool();
+
+ private StreamResponseUtil() {}
+
+ public static Response ok(final StreamResponseData content) {
+ final var stream = new StreamingOutput() {
+
+ @Override
+ public void write(OutputStream out) throws IOException {
+ IOUtils.copy(content.getStream(), out);
+ }
+ };
+
+ if (StringUtils.isEmpty(content.getDispositionType())) {
+ return Response.ok(stream, content.getType()).build();
+ } else {
+ return Response.ok(stream, content.getType()).header(HttpHeaders.CONTENT_DISPOSITION,
+ String.format("%s; filename=\"%s\"", content.getDispositionType(), content.getFileName())).build();
+ }
+ }
+
+ public static Future> ok(final AsyncResponse asyncResponse, final StreamResponseData content) {
+ return executor.submit(() -> {
+ if (StringUtils.isEmpty(content.getDispositionType())) {
+ asyncResponse.resume(Response.ok(content.getStream(), content.getType()).build());
+ } else {
+ asyncResponse.resume(Response.ok(content.getStream(), content.getType()).header(HttpHeaders.CONTENT_DISPOSITION,
+ String.format("%s; filename=\"%s\"", content.getDispositionType(), content.getFileName())).build());
+ }
+ });
+ }
+
+ @Builder
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static final class StreamResponseData {
+
+ private InputStream stream;
+ private String type;
+ private String fileName;
+ private String dispositionType;
+ private Long size;
+ }
+}
diff --git a/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml b/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml
index aeb8e6522..551f6cbee 100644
--- a/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml
+++ b/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-document/dependencies.gradle b/fineract-document/dependencies.gradle
index 0f4c8facd..e1cc0e429 100644
--- a/fineract-document/dependencies.gradle
+++ b/fineract-document/dependencies.gradle
@@ -24,27 +24,25 @@ dependencies {
// implementation dependencies are directly used (compiled against) in src/main (and src/test)
//
- implementation(project(path: ':fineract-core'))
+ implementation(
+ project(path: ':fineract-core'),
+ project(path: ':fineract-command'),
+ )
implementation(
'org.springframework.boot:spring-boot-starter-web',
+ 'org.springframework.boot:spring-boot-starter-validation',
'org.springframework.boot:spring-boot-starter-security',
+ 'org.springframework.boot:spring-boot-starter-data-jdbc',
'jakarta.ws.rs:jakarta.ws.rs-api',
'org.glassfish.jersey.media:jersey-media-multipart',
-
- 'com.google.guava:guava',
- 'com.google.code.gson:gson',
+ 'com.fasterxml.jackson.core:jackson-databind',
'org.apache.commons:commons-lang3',
- 'com.jayway.jsonpath:json-path',
-
'com.github.spotbugs:spotbugs-annotations',
'io.swagger.core.v3:swagger-annotations-jakarta',
- 'com.squareup.retrofit2:converter-gson',
-
- 'org.springdoc:springdoc-openapi-starter-webmvc-ui',
'org.mapstruct:mapstruct',
'io.github.resilience4j:resilience4j-spring-boot3',
@@ -53,6 +51,7 @@ dependencies {
'org.apache.tika:tika-core',
'org.apache.tika:tika-parser-microsoft-module',
'org.apache.tika:tika-parser-miscoffice-module',
+ 'org.apache.tika:tika-parser-image-module',
)
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
@@ -62,12 +61,6 @@ dependencies {
}
implementation ('software.amazon.awssdk:auth') {
}
- implementation ('org.springframework.boot:spring-boot-starter-data-jpa') {
- exclude group: 'org.hibernate'
- }
- implementation('org.eclipse.persistence:org.eclipse.persistence.jpa') {
- exclude group: 'org.eclipse.persistence', module: 'jakarta.persistence'
- }
// testCompile dependencies are ONLY used in src/test, not src/main.
// Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly!
//
@@ -80,4 +73,5 @@ dependencies {
exclude group: 'org.skyscreamer'
}
testImplementation ('org.mockito:mockito-inline')
+ testImplementation 'com.github.romankh3:image-comparison'
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/config/ContentStoreConfig.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/config/ContentStoreConfig.java
new file mode 100644
index 000000000..2af614d85
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/config/ContentStoreConfig.java
@@ -0,0 +1,38 @@
+/**
+ * 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.contentstore.config;
+
+import static org.apache.fineract.infrastructure.contentstore.processor.ContentProcessor.BEAN_NAME_EXECUTOR;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@RequiredArgsConstructor
+class ContentStoreConfig {
+
+ @Bean(BEAN_NAME_EXECUTOR)
+ ExecutorService contentProcessorExecutor() {
+ // TODO: make this more configurable?
+ return Executors.newCachedThreadPool();
+ }
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/StorageType.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/data/ContentStoreType.java
similarity index 63%
rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/StorageType.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/data/ContentStoreType.java
index 2bd697eb1..306952d60 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/StorageType.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/data/ContentStoreType.java
@@ -16,36 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.infrastructure.documentmanagement.domain;
+package org.apache.fineract.infrastructure.contentstore.data;
-import java.util.HashMap;
-import java.util.Map;
-
-public enum StorageType {
+public enum ContentStoreType {
FILE_SYSTEM(1), //
S3(2); //
private final Integer value;
- StorageType(final Integer value) {
+ ContentStoreType(final Integer value) {
this.value = value;
}
public Integer getValue() {
return this.value;
}
-
- private static final Map intToEnumMap = new HashMap<>();
-
- static {
- for (final StorageType type : StorageType.values()) {
- intToEnumMap.put(type.value, type);
- }
- }
-
- public static StorageType fromInt(final int i) {
- final StorageType type = intToEnumMap.get(Integer.valueOf(i));
- return type;
- }
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResourceSwagger.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetector.java
similarity index 80%
rename from fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResourceSwagger.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetector.java
index 1535c0b54..323b3c798 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResourceSwagger.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetector.java
@@ -16,16 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.infrastructure.documentmanagement.api;
+package org.apache.fineract.infrastructure.contentstore.detector;
-/**
- * Created by sanyam on 10/8/17.
- */
-
-final class ImagesApiResourceSwagger {
-
- private ImagesApiResourceSwagger() {
-
- }
+public interface ContentDetector {
+ ContentDetectorContext detect(ContentDetectorContext ctx);
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetectorContext.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetectorContext.java
new file mode 100644
index 000000000..fffbe0b4c
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetectorContext.java
@@ -0,0 +1,48 @@
+/**
+ * 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.contentstore.detector;
+
+import java.io.InputStream;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public final class ContentDetectorContext {
+
+ private InputStream inputStream;
+ @Builder.Default
+ private boolean inputStreamEnabled = false;
+ private String fileName;
+ private String mimeType;
+ private String extension;
+ private String format;
+
+ public ContentDetectorContext clone(String mimeType, String extension, String format) {
+ return new ContentDetectorContext(this.inputStream, this.inputStreamEnabled, this.fileName, mimeType, extension, format);
+ }
+
+ public ContentDetectorContext clone(InputStream inputStream, String mimeType, String extension, String format) {
+ return new ContentDetectorContext(inputStream, this.inputStreamEnabled, this.fileName, mimeType, extension, format);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetectorManager.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetectorManager.java
new file mode 100644
index 000000000..c7d5c0398
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetectorManager.java
@@ -0,0 +1,24 @@
+/**
+ * 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.contentstore.detector;
+
+public interface ContentDetectorManager {
+
+ ContentDetectorContext detect(ContentDetectorContext ctx);
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/DefaultContentDetectorManager.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/DefaultContentDetectorManager.java
new file mode 100644
index 000000000..d89bcb938
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/DefaultContentDetectorManager.java
@@ -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.contentstore.detector;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public final class DefaultContentDetectorManager implements ContentDetectorManager {
+
+ private final TikaContentDetector tikaContentDetector;
+
+ @Override
+ public ContentDetectorContext detect(ContentDetectorContext ctx) {
+ return tikaContentDetector.detect(ctx);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/FileContentDetector.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/FileContentDetector.java
new file mode 100644
index 000000000..048bc1a32
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/FileContentDetector.java
@@ -0,0 +1,45 @@
+/**
+ * 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.contentstore.detector;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentDetectorException;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public final class FileContentDetector implements ContentDetector {
+
+ @Override
+ public ContentDetectorContext detect(ContentDetectorContext ctx) {
+ try {
+ final var mimeType = Files.probeContentType(Paths.get(ctx.getFileName()));
+ final var format = FilenameUtils.getExtension(ctx.getFileName());
+
+ return ctx.clone(mimeType, "." + format, format);
+ } catch (Exception e) {
+ throw new ContentDetectorException(e);
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetector.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetector.java
new file mode 100644
index 000000000..bdfbf7eef
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetector.java
@@ -0,0 +1,88 @@
+/**
+ * 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.contentstore.detector;
+
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentDetectorException;
+import org.apache.tika.Tika;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.mime.MimeTypeException;
+import org.apache.tika.mime.MimeTypes;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public final class TikaContentDetector implements ContentDetector {
+
+ private final Tika tika = new Tika();
+
+ @Override
+ public ContentDetectorContext detect(ContentDetectorContext ctx) {
+ if (ctx.getInputStream() != null && !ctx.isInputStreamEnabled()) {
+ log.warn(
+ "Input stream provided, but not explicitly enabled for detection. This operation is potentially making the input stream unusable, especially with HTTP multipart streams. Input stream will be ignored!");
+ }
+
+ try {
+ if (ctx.getInputStream() != null && ctx.isInputStreamEnabled()) {
+ final var stream = TikaInputStream.get(ctx.getInputStream());
+ final var metadata = new Metadata();
+ metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, ctx.getFileName());
+
+ final var mimeType = tika.detect(stream, metadata);
+ final var extension = getExtension(mimeType);
+
+ stream.reset();
+
+ // note: original stream should be still usable if "mark" is supported
+ return ctx.clone(mimeType, extension, extension.substring(1));
+ } else if (StringUtils.isNotEmpty(ctx.getFileName())) {
+ final var mimeType = tika.detect(ctx.getFileName());
+ final var extension = getExtension(mimeType);
+
+ return ctx.clone(mimeType, extension, extension.substring(1));
+ }
+ } catch (Exception e) {
+ throw new ContentDetectorException(e);
+ }
+
+ throw new ContentDetectorException(
+ new IllegalArgumentException("Could not run detection, because required arguments were missing."));
+ }
+
+ private String getExtension(String mimeType) {
+ try {
+ return Optional.ofNullable(MimeTypes.getDefaultMimeTypes().forName(mimeType)).orElseGet(() -> {
+ try {
+ return MimeTypes.getDefaultMimeTypes().forName(MimeTypes.OCTET_STREAM);
+ } catch (MimeTypeException e) {
+ throw new ContentDetectorException(e);
+ }
+ }).getExtension();
+ } catch (Exception e) {
+ throw new ContentDetectorException(e);
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/InvalidEntityTypeForDocumentManagementException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentDetectorException.java
similarity index 59%
rename from fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/InvalidEntityTypeForDocumentManagementException.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentDetectorException.java
index 655f77106..d2fba0c53 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/InvalidEntityTypeForDocumentManagementException.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentDetectorException.java
@@ -16,17 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.infrastructure.documentmanagement.exception;
+package org.apache.fineract.infrastructure.contentstore.exception;
-import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformException;
-/**
- * A {@link RuntimeException} thrown when document management functionality is invoked for invalid Entity Types
- */
-public class InvalidEntityTypeForDocumentManagementException extends AbstractPlatformResourceNotFoundException {
+public class ContentDetectorException extends AbstractPlatformException {
- public InvalidEntityTypeForDocumentManagementException(final String entityType) {
- super("error.documentmanagement.entitytype.invalid", "Document Management is not support for the Entity Type: " + entityType,
- entityType);
+ public ContentDetectorException(Exception e) {
+ super("error.msg.content.detector", e.getMessage(), e);
}
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentPathSanitizerException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentPathSanitizerException.java
new file mode 100644
index 000000000..56f149349
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentPathSanitizerException.java
@@ -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.contentstore.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformException;
+
+public class ContentPathSanitizerException extends AbstractPlatformException {
+
+ public ContentPathSanitizerException(String message) {
+ super("error.msg.content.sanitizer", message);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentPolicyException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentPolicyException.java
new file mode 100644
index 000000000..3ba139b11
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentPolicyException.java
@@ -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.contentstore.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformException;
+
+public class ContentPolicyException extends AbstractPlatformException {
+
+ public ContentPolicyException(String message) {
+ super("error.msg.content.policy", message);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentProcessorException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentProcessorException.java
new file mode 100644
index 000000000..1fe80600b
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentProcessorException.java
@@ -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.contentstore.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformException;
+
+public class ContentProcessorException extends AbstractPlatformException {
+
+ public ContentProcessorException(String message) {
+ super("error.msg.content.processor", message);
+ }
+
+ public ContentProcessorException(Exception e) {
+ super("error.msg.content.processor", e.getMessage(), e);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentStoreException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentStoreException.java
new file mode 100644
index 000000000..9dc81396b
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentStoreException.java
@@ -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.contentstore.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformException;
+
+public class ContentStoreException extends AbstractPlatformException {
+
+ public ContentStoreException(Exception e) {
+ super("error.msg.content.store", e.getMessage(), e);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/ContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/ContentPolicy.java
new file mode 100644
index 000000000..db6c7666a
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/ContentPolicy.java
@@ -0,0 +1,24 @@
+/**
+ * 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.contentstore.policy;
+
+public interface ContentPolicy {
+
+ void check(ContentPolicyContext ctx);
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/ContentPolicyContext.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/ContentPolicyContext.java
new file mode 100644
index 000000000..9b27922ac
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/ContentPolicyContext.java
@@ -0,0 +1,38 @@
+/**
+ * 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.contentstore.policy;
+
+import java.io.InputStream;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public final class ContentPolicyContext {
+
+ private String path;
+ private Long size;
+ private InputStream inputStream;
+ private String mimeType;
+ private String extension;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultDeleteContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultDeleteContentPolicy.java
new file mode 100644
index 000000000..9fb410118
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultDeleteContentPolicy.java
@@ -0,0 +1,38 @@
+/**
+ * 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.contentstore.policy;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DefaultDeleteContentPolicy implements ContentPolicy {
+
+ private final TraversalContentPolicy traversalContentPolicy;
+ private final FineractProperties properties;
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ traversalContentPolicy.check(ctx);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultDownloadContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultDownloadContentPolicy.java
new file mode 100644
index 000000000..2a0249911
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultDownloadContentPolicy.java
@@ -0,0 +1,38 @@
+/**
+ * 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.contentstore.policy;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DefaultDownloadContentPolicy implements ContentPolicy {
+
+ private final TraversalContentPolicy traversalContentPolicy;
+ private final FineractProperties properties;
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ traversalContentPolicy.check(ctx);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultPostUploadContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultPostUploadContentPolicy.java
new file mode 100644
index 000000000..c071545ed
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultPostUploadContentPolicy.java
@@ -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.contentstore.policy;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DefaultPostUploadContentPolicy implements ContentPolicy {
+
+ private final MimeContentPolicy mimeContentPolicy;
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ mimeContentPolicy.check(ctx);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultPreUploadContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultPreUploadContentPolicy.java
new file mode 100644
index 000000000..26f8beaf0
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/DefaultPreUploadContentPolicy.java
@@ -0,0 +1,38 @@
+/**
+ * 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.contentstore.policy;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DefaultPreUploadContentPolicy implements ContentPolicy {
+
+ private final WhitelistContentPolicy whitelistContentPolicy;
+ private final TraversalContentPolicy traversalContentPolicy;
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ traversalContentPolicy.check(ctx);
+ whitelistContentPolicy.check(ctx);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/MimeContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/MimeContentPolicy.java
new file mode 100644
index 000000000..e1218d564
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/MimeContentPolicy.java
@@ -0,0 +1,48 @@
+/**
+ * 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.contentstore.policy;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.Strings;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class MimeContentPolicy implements ContentPolicy {
+
+ private final ContentDetectorManager contentDetectorManager;
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ if (ctx.getInputStream() != null) {
+ var result = contentDetectorManager
+ .detect(ContentDetectorContext.builder().inputStream(ctx.getInputStream()).inputStreamEnabled(true).build());
+
+ if (!Strings.CI.equals(result.getMimeType(), ctx.getMimeType())) {
+ throw new ContentPolicyException(String.format("Detected file type (%s), but mime type (%s) was provided. Mismatch!",
+ result.getMimeType(), ctx.getMimeType()));
+ }
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/TraversalContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/TraversalContentPolicy.java
new file mode 100644
index 000000000..7ca247d95
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/TraversalContentPolicy.java
@@ -0,0 +1,43 @@
+/**
+ * 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.contentstore.policy;
+
+import java.util.regex.Pattern;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class TraversalContentPolicy implements ContentPolicy {
+
+ private final Pattern overwriteSiblingsPattern = Pattern.compile(".*((\\.)?\\./+)+(/+)?.*");
+
+ private final FineractProperties properties;
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ if (overwriteSiblingsPattern.matcher(ctx.getPath()).matches()) {
+ throw new ContentPolicyException(String.format("Trying to overwrite a sibling file: %s", ctx.getPath()));
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/WhitelistContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/WhitelistContentPolicy.java
new file mode 100644
index 000000000..f12e37c09
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/WhitelistContentPolicy.java
@@ -0,0 +1,72 @@
+/**
+ * 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.contentstore.policy;
+
+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.contentstore.exception.ContentPolicyException;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.boot.context.event.ApplicationStartedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class WhitelistContentPolicy implements ContentPolicy {
+
+ private final FineractProperties properties;
+
+ private List regexWhitelist;
+
+ @EventListener(ApplicationStartedEvent.class)
+ void onStartup() {
+ regexWhitelist = properties.getContent().getRegexWhitelist().stream().map(Pattern::compile).toList();
+ }
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ if (properties.getContent().isRegexWhitelistEnabled()) {
+ final var fileName = FilenameUtils.getName(ctx.getPath());
+
+ boolean matches = regexWhitelist.stream().anyMatch(p -> p.matcher(fileName).matches());
+
+ if (!matches) {
+ throw new ContentPolicyException(String.format("File name not allowed: %s", fileName));
+ }
+ }
+
+ if (properties.getContent().isMimeWhitelistEnabled()) {
+ final var fileName = FilenameUtils.getName(ctx.getPath());
+
+ if (StringUtils.isEmpty(ctx.getMimeType())) {
+ throw new ContentPolicyException(String.format("Could not detect mime type for filename %s!", fileName));
+ }
+
+ if (!properties.getContent().getMimeWhitelist().contains(ctx.getMimeType())) {
+ throw new ContentPolicyException(
+ String.format("Detected mime type %s for filename %s not allowed!", ctx.getMimeType(), fileName));
+ }
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/Base64DecoderContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/Base64DecoderContentProcessor.java
new file mode 100644
index 000000000..8a9245868
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/Base64DecoderContentProcessor.java
@@ -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.contentstore.processor;
+
+import static java.util.Objects.requireNonNullElse;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64InputStream;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class Base64DecoderContentProcessor implements ContentProcessor {
+
+ private static final String BASE64_DECODE_PREFIX = "base64.decode.";
+
+ public static final String BASE64_DECODE_PARAM_BUFFER_SIZE = BASE64_DECODE_PREFIX + "buffer-size";
+
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public ContentProcessorContext process(final ContentProcessorContext ctx) {
+ final Integer bufferSize = ctx.getParameter(BASE64_DECODE_PARAM_BUFFER_SIZE,
+ requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+
+ final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
+ pipe.write(new Base64InputStream(in), out, new byte[bufferSize]);
+ });
+
+ return ctx.clone(pipedInputStream);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/Base64EncoderContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/Base64EncoderContentProcessor.java
new file mode 100644
index 000000000..b870a94cb
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/Base64EncoderContentProcessor.java
@@ -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.contentstore.processor;
+
+import static java.util.Objects.requireNonNullElse;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64OutputStream;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class Base64EncoderContentProcessor implements ContentProcessor {
+
+ private static final String BASE64_ENCODE_PREFIX = "base64.encode.";
+
+ public static final String BASE64_ENCODE_PARAM_BUFFER_SIZE = BASE64_ENCODE_PREFIX + "buffer-size";
+
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public ContentProcessorContext process(final ContentProcessorContext ctx) {
+ final Integer bufferSize = ctx.getParameter(BASE64_ENCODE_PARAM_BUFFER_SIZE,
+ requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+
+ final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
+ pipe.write(in, new Base64OutputStream(out), new byte[bufferSize]);
+ });
+
+ return ctx.clone(pipedInputStream);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessor.java
new file mode 100644
index 000000000..6a882f415
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessor.java
@@ -0,0 +1,59 @@
+/**
+ * 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.contentstore.processor;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+@FunctionalInterface
+public interface ContentProcessor {
+
+ String BEAN_NAME_EXECUTOR = "contentProcessorExecutor";
+
+ ContentProcessorContext process(ContentProcessorContext context);
+
+ default void apply() {
+ // do nothing by default
+ }
+
+ default ContentProcessorContext process(InputStream is) {
+ return process(new ContentProcessorContext(is));
+ }
+
+ default ContentProcessorContext process(InputStream is, Map parameters) {
+ return process(new ContentProcessorContext(is, parameters));
+ }
+
+ default ContentProcessor then(ContentProcessor next) {
+ return (context) -> next.process(this.process(context));
+ }
+
+ @FunctionalInterface
+ interface InputOutputStreamConsumer {
+
+ void accept(InputStream input, OutputStream output) throws Exception;
+ }
+
+ @FunctionalInterface
+ interface OutputStreamConsumer {
+
+ void accept(OutputStream output) throws Exception;
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorContext.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorContext.java
new file mode 100644
index 000000000..71cdc82a7
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorContext.java
@@ -0,0 +1,76 @@
+/**
+ * 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.contentstore.processor;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Getter
+public final class ContentProcessorContext {
+
+ private final InputStream inputStream;
+ private final Map parameters;
+ private final Map results;
+
+ public ContentProcessorContext(InputStream inputStream) {
+ this(inputStream, Map.of(), new HashMap<>());
+ }
+
+ public ContentProcessorContext(InputStream inputStream, Map parameters) {
+ this(inputStream, parameters, new HashMap<>());
+ }
+
+ ContentProcessorContext(InputStream inputStream, Map parameters, Map results) {
+ this.inputStream = inputStream;
+ this.parameters = parameters;
+ this.results = results;
+ }
+
+ public R getParameter(String key) {
+ return getParameter(key, null);
+ }
+
+ public R getParameter(String key, R defaultValue) {
+ final var val = parameters.get(key);
+
+ return val != null ? (R) val : defaultValue;
+ }
+
+ public void setResult(String key, Object value) {
+ results.put(key, value);
+ }
+
+ public R getResult(String key) {
+ return getResult(key, null);
+ }
+
+ public R getResult(String key, R defaultValue) {
+ final var val = results.get(key);
+
+ return val != null ? (R) val : defaultValue;
+ }
+
+ public ContentProcessorContext clone(InputStream inputStream) {
+ return new ContentProcessorContext(inputStream, this.parameters, this.results);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/DataUrlDecoderContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/DataUrlDecoderContentProcessor.java
new file mode 100644
index 000000000..608efe4d8
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/DataUrlDecoderContentProcessor.java
@@ -0,0 +1,153 @@
+/**
+ * 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.contentstore.processor;
+
+import static java.util.Objects.requireNonNullElse;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DataUrlDecoderContentProcessor implements ContentProcessor {
+
+ private static final String DATA_URL_PREFIX = "dataurl.decode.";
+
+ public static final String DATA_URL_DECODE_RESULT_CONTENT_TYPE = DATA_URL_PREFIX + "result.content-type";
+ public static final String DATA_URL_DECODE_RESULT_ENCODING = DATA_URL_PREFIX + "result.encoding";
+ public static final String DATA_URL_DECODE_PARAM_BUFFER_SIZE = DATA_URL_PREFIX + "param.buffer-size";
+
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public ContentProcessorContext process(final ContentProcessorContext ctx) {
+ final Integer bufferSize = ctx.getParameter(DATA_URL_DECODE_PARAM_BUFFER_SIZE,
+ requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+
+ final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
+ var dataUrlIn = new DataUrlInputStream(in);
+
+ ctx.setResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE, dataUrlIn.getContentType());
+ ctx.setResult(DATA_URL_DECODE_RESULT_ENCODING, dataUrlIn.getEncoding());
+
+ pipe.write(dataUrlIn, out, new byte[bufferSize]);
+ });
+
+ return ctx.clone(pipedInputStream);
+ }
+
+ private static class DataUrlInputStream extends FilterInputStream {
+
+ private static final String DEFAULT_MEDIA_TYPE = "text/plain;charset=US-ASCII";
+ private static final String ENCODING_BASE64 = "base64";
+ private static final String ENCODING_URL = "urlencoded";
+ private static final String META_PART = "data:";
+ private static final char COMMA_CHAR = ',';
+ private static final char SEMICOLON_CHAR = ';';
+
+ private volatile String contentType;
+ private volatile String encoding;
+
+ DataUrlInputStream(InputStream in) {
+ super(in);
+ }
+
+ private void readHeaderProcessed() throws IOException {
+ if (StringUtils.isNotEmpty(contentType)) {
+ return;
+ }
+
+ final var headerBuffer = new ByteArrayOutputStream(128);
+
+ int b;
+ while ((b = in.read()) != -1) {
+ if (b == COMMA_CHAR) {
+ // stop reading header
+ break;
+ }
+ headerBuffer.write(b);
+ }
+
+ var header = headerBuffer.toString(StandardCharsets.UTF_8);
+
+ parseHeader(header);
+ }
+
+ private void parseHeader(String header) {
+ if (!header.startsWith(META_PART)) {
+ this.contentType = DEFAULT_MEDIA_TYPE;
+ this.encoding = ENCODING_URL;
+ return;
+ }
+
+ String metaPart = header.substring(5); // Remove "data:"
+
+ if (metaPart.endsWith(SEMICOLON_CHAR + ENCODING_BASE64)) {
+ this.encoding = ENCODING_BASE64;
+ // remove ";base64"
+ metaPart = metaPart.substring(0, metaPart.length() - ENCODING_BASE64.length() - 1);
+ } else {
+ this.encoding = ENCODING_URL;
+ }
+
+ if (metaPart.isEmpty()) {
+ this.contentType = DEFAULT_MEDIA_TYPE;
+ } else {
+ this.contentType = metaPart;
+ }
+ }
+
+ String getContentType() throws IOException {
+ readHeaderProcessed();
+
+ return contentType;
+ }
+
+ String getEncoding() throws IOException {
+ readHeaderProcessed();
+
+ return encoding;
+ }
+
+ @Override
+ public int read() throws IOException {
+ readHeaderProcessed();
+
+ return in.read();
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ readHeaderProcessed();
+
+ return in.read(b, off, len);
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/DataUrlEncoderContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/DataUrlEncoderContentProcessor.java
new file mode 100644
index 000000000..6163cea17
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/DataUrlEncoderContentProcessor.java
@@ -0,0 +1,154 @@
+/**
+ * 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.contentstore.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNullElse;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DataUrlEncoderContentProcessor implements ContentProcessor {
+
+ private static final String DATA_URL_ENCODE_PREFIX = "dataurl.encode.";
+
+ public static final String DATA_URL_ENCODE_PARAM_BUFFER_SIZE = DATA_URL_ENCODE_PREFIX + "param.buffer-size";
+ public static final String DATA_URL_ENCODE_PARAM_CONTENT_TYPE = DATA_URL_ENCODE_PREFIX + "param.content-type";
+ public static final String DATA_URL_ENCODE_PARAM_ENCODING = DATA_URL_ENCODE_PREFIX + "param.encoding";
+
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public ContentProcessorContext process(final ContentProcessorContext ctx) {
+ final Integer bufferSize = ctx.getParameter(DATA_URL_ENCODE_PARAM_BUFFER_SIZE,
+ requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+ final String contentType = ctx.getParameter(DATA_URL_ENCODE_PARAM_CONTENT_TYPE, "text/plain");
+ final String encoding = ctx.getParameter(DATA_URL_ENCODE_PARAM_ENCODING, "charset=US-ASCII");
+
+ final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
+ pipe.write(new DataUrlEncoderInputStream(in, contentType, encoding), out, new byte[bufferSize]);
+ });
+
+ return ctx.clone(pipedInputStream);
+ }
+
+ private static class DataUrlEncoderInputStream extends FilterInputStream {
+
+ private static final String META_PART = "data:";
+ private static final char COMMA_CHAR = ',';
+ private static final char SEMICOLON_CHAR = ';';
+
+ private final byte[] prefix;
+ private int prefixIndex = 0;
+
+ DataUrlEncoderInputStream(InputStream in, String contentType, String encoding) {
+ super(in);
+ this.prefix = (META_PART + contentType + SEMICOLON_CHAR + encoding + COMMA_CHAR).getBytes(UTF_8);
+ }
+
+ private boolean isPrefixConsumed() {
+ return prefixIndex >= prefix.length;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (!isPrefixConsumed()) {
+ // return the next byte of the prefix
+ // we need to cast to int and mask with 0xFF to ensure positive value (0-255)
+ return prefix[prefixIndex++] & 0xFF;
+ }
+
+ // served the prefix, now the real stream starts
+ return in.read();
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ // optimization: do not call super.read(b, off, len) as that might call
+ // the single-byte read() repeatedly, which is slow.
+
+ if (isPrefixConsumed()) {
+ return in.read(b, off, len);
+ }
+
+ // still have prefix data to write
+ int availableInPrefix = prefix.length - prefixIndex;
+
+ // fill the buffer with remaining prefix bytes (or as much as requested)
+ int bytesToCopyFromPrefix = Math.min(len, availableInPrefix);
+
+ System.arraycopy(prefix, prefixIndex, b, off, bytesToCopyFromPrefix);
+ prefixIndex += bytesToCopyFromPrefix;
+
+ // if we filled the request entirely with the prefix, return immediately
+ if (bytesToCopyFromPrefix == len) {
+ return len;
+ }
+
+ // if we still have space in the buffer, fill the rest from the underlying stream
+ int bytesToReadFromStream = len - bytesToCopyFromPrefix;
+ int bytesReadFromStream = in.read(b, off + bytesToCopyFromPrefix, bytesToReadFromStream);
+
+ if (bytesReadFromStream == -1) {
+ // underlying stream is empty, but we did write the prefix
+ return bytesToCopyFromPrefix;
+ }
+
+ return bytesToCopyFromPrefix + bytesReadFromStream;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (isPrefixConsumed()) {
+ return in.skip(n);
+ }
+
+ long availableInPrefix = Integer.valueOf(prefix.length - prefixIndex).longValue();
+ long skippableInPrefix = Math.min(n, availableInPrefix);
+
+ prefixIndex += Long.valueOf(skippableInPrefix).intValue();
+ long remainingToSkip = n - skippableInPrefix;
+
+ if (remainingToSkip > 0) {
+ return skippableInPrefix + in.skip(remainingToSkip);
+ }
+
+ return skippableInPrefix;
+ }
+
+ @Override
+ public int available() throws IOException {
+ if (isPrefixConsumed()) {
+ return in.available();
+ }
+ // total available is prefix remaining + stream available
+ return (prefix.length - prefixIndex) + in.available();
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/GzipDecoderContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/GzipDecoderContentProcessor.java
new file mode 100644
index 000000000..aa0e7eecf
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/GzipDecoderContentProcessor.java
@@ -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.contentstore.processor;
+
+import static java.util.Objects.requireNonNullElse;
+
+import java.util.zip.GZIPInputStream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class GzipDecoderContentProcessor implements ContentProcessor {
+
+ private static final String GZIP_DECODE_PREFIX = "gzip.decode.";
+
+ public static final String GZIP_DECODE_PARAM_BUFFER_SIZE = GZIP_DECODE_PREFIX + "buffer-size";
+
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public ContentProcessorContext process(final ContentProcessorContext ctx) {
+ final Integer bufferSize = ctx.getParameter(GZIP_DECODE_PARAM_BUFFER_SIZE,
+ requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+
+ final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
+ pipe.write(new GZIPInputStream(in), out, new byte[bufferSize]);
+ });
+
+ return ctx.clone(pipedInputStream);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/GzipEncoderContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/GzipEncoderContentProcessor.java
new file mode 100644
index 000000000..a3a0d7bb2
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/GzipEncoderContentProcessor.java
@@ -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.contentstore.processor;
+
+import static java.util.Objects.requireNonNullElse;
+
+import java.util.zip.GZIPOutputStream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class GzipEncoderContentProcessor implements ContentProcessor {
+
+ private static final String GZIP_ENCODE_PREFIX = "gzip.encode.";
+
+ public static final String GZIP_ENCODE_PARAM_BUFFER_SIZE = GZIP_ENCODE_PREFIX + "buffer-size";
+
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public ContentProcessorContext process(final ContentProcessorContext ctx) {
+ final Integer bufferSize = ctx.getParameter(GZIP_ENCODE_PARAM_BUFFER_SIZE,
+ requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+
+ final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
+ pipe.write(in, new GZIPOutputStream(out), new byte[bufferSize]);
+ });
+
+ return ctx.clone(pipedInputStream);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ImageResizeContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ImageResizeContentProcessor.java
new file mode 100644
index 000000000..13caf17fa
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/ImageResizeContentProcessor.java
@@ -0,0 +1,110 @@
+/**
+ * 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.contentstore.processor;
+
+import static java.util.Objects.requireNonNull;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.regex.Pattern;
+import javax.imageio.ImageIO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentProcessorException;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StreamUtils;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class ImageResizeContentProcessor implements ContentProcessor {
+
+ private static final String IMAGE_RESIZE_PREFIX = "image.resize.";
+
+ public static final String IMAGE_RESIZE_PARAM_MAX_WIDTH = IMAGE_RESIZE_PREFIX + "max-width";
+ public static final String IMAGE_RESIZE_PARAM_MAX_HEIGHT = IMAGE_RESIZE_PREFIX + "max-height";
+ public static final String IMAGE_RESIZE_PARAM_FORMAT = IMAGE_RESIZE_PREFIX + "format";
+
+ private final ContentPipe pipe;
+
+ private static final Pattern VALID_IMAGE_FORMATS = Pattern.compile("^(gif|jpg|jpeg|png)$", Pattern.CASE_INSENSITIVE);
+
+ @Override
+ public ContentProcessorContext process(final ContentProcessorContext ctx) {
+ final Integer maxWidth = ctx.getParameter(IMAGE_RESIZE_PARAM_MAX_WIDTH);
+ final Integer maxHeight = ctx.getParameter(IMAGE_RESIZE_PARAM_MAX_HEIGHT);
+ final String format = ctx.getParameter(IMAGE_RESIZE_PARAM_FORMAT, "jpg");
+
+ requireNonNull(maxWidth, "Missing max width parameter");
+ requireNonNull(maxHeight, "Missing max height parameter");
+
+ if (!VALID_IMAGE_FORMATS.matcher(format).matches()) {
+ throw new ContentProcessorException(String.format("Wrong image format parameter: %s", format));
+ }
+
+ final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
+ final var image = resize(in, maxWidth, maxHeight);
+
+ // compresses the image into the specified format (e.g., "gif", "jpeg", "png")
+ ImageIO.write(image, format, out);
+ });
+
+ return ctx.clone(pipedInputStream);
+ }
+
+ private BufferedImage resize(InputStream is, int targetWidth, int targetHeight) throws IOException {
+ final var imageIs = ImageIO.createImageInputStream(StreamUtils.nonClosing(is));
+
+ final var readers = ImageIO.getImageReaders(imageIs);
+
+ if (!readers.hasNext()) {
+ throw new ContentProcessorException("No image reader found for format");
+ }
+
+ final var reader = readers.next();
+
+ try {
+ reader.setInput(imageIs, true, true);
+
+ // get original dimensions to calculate subsampling ratio
+ int originalWidth = reader.getWidth(0);
+ int originalHeight = reader.getHeight(0);
+
+ // calculate subsampling ratio
+ // subsampling is an integer ratio of source size to output size
+ int subsampleX = Math.max(1, originalWidth / targetWidth);
+ int subsampleY = Math.max(1, originalHeight / targetHeight);
+ int subsampling = Math.max(subsampleX, subsampleY);
+
+ final var param = reader.getDefaultReadParam();
+ param.setSourceSubsampling(subsampling, subsampling, 0, 0);
+
+ // image will be close to the target size, but not exact,
+ // as subsampling only works with integer ratios. Further quality scaling
+ // can be done on the smaller BufferedImage if exact dimensions are needed.
+
+ // read the image using the parameters
+ return reader.read(0, param);
+ } finally {
+ reader.dispose();
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/SizeContentProcessor.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/SizeContentProcessor.java
new file mode 100644
index 000000000..3c7b9575f
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/processor/SizeContentProcessor.java
@@ -0,0 +1,80 @@
+/**
+ * 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.contentstore.processor;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class SizeContentProcessor implements ContentProcessor {
+
+ private static final String SIZE_PREFIX = "size.";
+
+ public static final String SIZE_RESULT_VALUE = SIZE_PREFIX + "result.value";
+
+ @Override
+ public ContentProcessorContext process(ContentProcessorContext ctx) {
+ return ctx.clone(new ByteCountingInputStream(ctx));
+ }
+
+ private static final class ByteCountingInputStream extends FilterInputStream {
+
+ private final ContentProcessorContext ctx;
+ private long byteCount = 0;
+
+ ByteCountingInputStream(ContentProcessorContext ctx) {
+ super(ctx.getInputStream());
+ this.ctx = ctx;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int data = in.read();
+
+ if (data != -1) {
+ byteCount++;
+ }
+
+ return data;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int count = in.read(b, off, len);
+
+ if (count != -1) {
+ byteCount += count;
+ }
+
+ return count;
+ }
+
+ @Override
+ public void close() throws IOException {
+ ctx.setResult(SIZE_RESULT_VALUE, byteCount);
+
+ super.close();
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/ContentStoreService.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/ContentStoreService.java
new file mode 100644
index 000000000..2da1fce35
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/ContentStoreService.java
@@ -0,0 +1,40 @@
+/**
+ * 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.contentstore.service;
+
+import java.io.InputStream;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
+
+public interface ContentStoreService {
+
+ String EXTENSION_REGEX = "^.+\\..+$";
+ String DELIMITER = "/";
+
+ InputStream download(String path);
+
+ String upload(String path, InputStream is, String mimeType);
+
+ void delete(String path);
+
+ ContentStoreType getType();
+
+ default String getDelimiter() {
+ return DELIMITER;
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/FileContentStoreService.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/FileContentStoreService.java
new file mode 100644
index 000000000..1f293764b
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/FileContentStoreService.java
@@ -0,0 +1,198 @@
+/**
+ * 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.contentstore.service;
+
+import static java.util.Objects.requireNonNull;
+import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.stream.Stream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentStoreException;
+import org.apache.fineract.infrastructure.contentstore.policy.ContentPolicyContext;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultDeleteContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultDownloadContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultPostUploadContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultPreUploadContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathRandomizer;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathSanitizer;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+@ConditionalOnProperty(name = "fineract.content.filesystem.enabled", havingValue = "true")
+public class FileContentStoreService implements ContentStoreService {
+
+ private final ContentPathSanitizer pathSanitizer;
+ private final ContentPathRandomizer pathRandomizer;
+ private final DefaultDownloadContentPolicy downloadContentPolicy;
+ private final DefaultPreUploadContentPolicy preUploadContentPolicy;
+ private final DefaultPostUploadContentPolicy postUploadContentPolicy;
+ private final DefaultDeleteContentPolicy deleteContentPolicy;
+ private final ContentDetectorManager contentDetectorManager;
+ private final FineractProperties properties;
+
+ @Override
+ public InputStream download(String path) {
+ downloadContentPolicy.check(ContentPolicyContext.builder().path(path).build());
+
+ final var safePath = pathSanitizer.sanitize(path);
+
+ try {
+ final var target = getPath(safePath, false);
+
+ return Files.newInputStream(target);
+ } catch (IOException e) {
+ throw new ContentStoreException(e);
+ }
+ }
+
+ @Override
+ public String upload(String path, InputStream is, String mimeType) {
+ requireNonNull(path, "Path missing");
+ requireNonNull(is, "Data missing");
+
+ if (StringUtils.isEmpty(mimeType)) {
+ mimeType = Optional.ofNullable(contentDetectorManager.detect(ContentDetectorContext.builder().fileName(path).build()))
+ .map(ContentDetectorContext::getMimeType).orElse(APPLICATION_OCTET_STREAM_VALUE);
+ }
+
+ preUploadContentPolicy.check(ContentPolicyContext.builder().path(path).mimeType(mimeType).build());
+
+ var safePath = pathSanitizer.sanitize(path);
+
+ final var target = Files.exists(getPath(safePath, false)) ? getPath(safePath, false) : getPath(safePath, true);
+
+ try (var in = is) {
+ IOUtils.copy(in, new FileOutputStream(target.toString()));
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+
+ final var relativePath = getRelativePath(target);
+
+ try {
+ postUploadContentPolicy
+ .check(ContentPolicyContext.builder().path(path).inputStream(getInputStream(target)).mimeType(mimeType).build());
+ } catch (ContentPolicyException e) {
+ log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", target, e.getMessage());
+
+ delete(relativePath.toString());
+
+ throw e;
+ }
+
+ return relativePath.toString();
+ }
+
+ @Override
+ public void delete(String path) {
+ deleteContentPolicy.check(ContentPolicyContext.builder().path(path).build());
+
+ final var safePath = pathSanitizer.sanitize(path);
+
+ try {
+ final var target = getPath(safePath, false);
+
+ final boolean deleted = Files.deleteIfExists(target);
+
+ if (!hasFiles(target.getParent())) {
+ Files.deleteIfExists(target.getParent());
+ }
+
+ if (!deleted) {
+ // 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 {}", safePath);
+ }
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+ }
+
+ @Override
+ public ContentStoreType getType() {
+ return ContentStoreType.FILE_SYSTEM;
+ }
+
+ private Path getPath(String path, boolean randomize) {
+ Path p;
+
+ if (randomize) {
+ var tail = Path.of(path);
+ var file = tail.getFileName();
+
+ p = getRootPath().resolve(tail.getParent()).resolve(pathRandomizer.randomize()).resolve(file);
+
+ try {
+ if (Files.notExists(p.getParent())) {
+ Files.createDirectories(p.getParent());
+ }
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+ } else {
+ p = getRootPath().resolve(path);
+ }
+
+ return p;
+ }
+
+ private Path getRelativePath(Path fullPath) {
+ return getRootPath().relativize(fullPath);
+ }
+
+ private Path getRootPath() {
+ return Path.of(properties.getContent().getFilesystem().getRootFolder(),
+ ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim());
+ }
+
+ private InputStream getInputStream(Path path) {
+ try {
+ return Files.newInputStream(path);
+ } catch (IOException ioe) {
+ throw new ContentStoreException(ioe);
+ }
+ }
+
+ boolean hasFiles(Path folder) throws IOException {
+ if (Files.exists(folder) && Files.isDirectory(folder)) {
+ try (Stream entries = Files.list(folder)) {
+ return entries.anyMatch(Files::isRegularFile);
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/S3ContentStoreService.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/S3ContentStoreService.java
new file mode 100644
index 000000000..8800f35fc
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/S3ContentStoreService.java
@@ -0,0 +1,126 @@
+/**
+ * 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.contentstore.service;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.InputStream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentStoreException;
+import org.apache.fineract.infrastructure.contentstore.policy.ContentPolicyContext;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultDeleteContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultDownloadContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultPostUploadContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.DefaultPreUploadContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathSanitizer;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.core.sync.ResponseTransformer;
+import software.amazon.awssdk.http.ContentStreamProvider;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+@ConditionalOnProperty(name = "fineract.content.s3.enabled", havingValue = "true")
+public class S3ContentStoreService implements ContentStoreService {
+
+ private final S3Client s3Client;
+ private final ContentPathSanitizer pathSanitizer;
+ private final DefaultDownloadContentPolicy downloadContentPolicy;
+ private final DefaultPreUploadContentPolicy preUploadContentPolicy;
+ private final DefaultPostUploadContentPolicy postUploadContentPolicy;
+ private final DefaultDeleteContentPolicy deleteContentPolicy;
+ private final ContentDetectorManager contentDetectorManager;
+ private final FineractProperties properties;
+
+ @Override
+ public InputStream download(String path) {
+ downloadContentPolicy.check(ContentPolicyContext.builder().path(path).build());
+
+ final var safePath = pathSanitizer.sanitize(path);
+
+ try {
+ return s3Client
+ .getObject(GetObjectRequest.builder().bucket(properties.getContent().getS3().getBucketName()).key(safePath).build(),
+ ResponseTransformer.toBytes())
+ .asInputStream();
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+ }
+
+ @Override
+ public String upload(String path, InputStream is, String mimeType) {
+ requireNonNull(path, "Path missing");
+ requireNonNull(is, "Data missing");
+
+ preUploadContentPolicy.check(ContentPolicyContext.builder().path(path).mimeType(mimeType).build());
+
+ var safePath = pathSanitizer.sanitize(path);
+
+ try {
+ final var p = safePath;
+
+ s3Client.putObject(builder -> builder.bucket(properties.getContent().getS3().getBucketName()).key(p),
+ RequestBody.fromContentProvider(ContentStreamProvider.fromInputStream(is), mimeType));
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+
+ try {
+ // TODO: verify if this is working; maybe there is a better way (e.g. temporary storage on file system)
+ postUploadContentPolicy
+ .check(ContentPolicyContext.builder().path(path).inputStream(download(safePath)).mimeType(mimeType).build());
+ } catch (ContentPolicyException e) {
+ log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", safePath, e.getMessage());
+
+ delete(safePath);
+
+ throw e;
+ }
+
+ return safePath;
+ }
+
+ @Override
+ public void delete(String path) {
+ deleteContentPolicy.check(ContentPolicyContext.builder().path(path).build());
+
+ final var safePath = pathSanitizer.sanitize(path);
+
+ try {
+ s3Client.deleteObject(builder -> builder.bucket(properties.getContent().getS3().getBucketName()).key(safePath));
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+ }
+
+ @Override
+ public ContentStoreType getType() {
+ return ContentStoreType.S3;
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPathRandomizer.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPathRandomizer.java
new file mode 100644
index 000000000..6ac73bd70
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPathRandomizer.java
@@ -0,0 +1,24 @@
+/**
+ * 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.contentstore.util;
+
+public interface ContentPathRandomizer {
+
+ String randomize();
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentPathSanitizer.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPathSanitizer.java
similarity index 83%
rename from fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentPathSanitizer.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPathSanitizer.java
index 415532fcd..ef07ee769 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentPathSanitizer.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPathSanitizer.java
@@ -16,13 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
-
-import java.io.BufferedInputStream;
+package org.apache.fineract.infrastructure.contentstore.util;
public interface ContentPathSanitizer {
String sanitize(String path);
-
- String sanitize(String path, BufferedInputStream is);
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPipe.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPipe.java
new file mode 100644
index 000000000..131c50e40
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/ContentPipe.java
@@ -0,0 +1,169 @@
+/**
+ * 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.contentstore.util;
+
+import static java.util.Objects.requireNonNullElse;
+import static org.apache.fineract.infrastructure.contentstore.processor.ContentProcessor.BEAN_NAME_EXECUTOR;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentProcessorException;
+import org.apache.fineract.infrastructure.contentstore.processor.ContentProcessor;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public final class ContentPipe {
+
+ private final ExecutorService executor;
+ private final FineractProperties properties;
+
+ public ContentPipe(@Qualifier(BEAN_NAME_EXECUTOR) ExecutorService executor, FineractProperties properties) {
+ this.executor = executor;
+ this.properties = properties;
+ }
+
+ public InputStream pipe(ContentProcessor.OutputStreamConsumer consumer) {
+ var pis = new PipedInputStream(requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+ var pos = newPipedOutputStream(pis);
+
+ var future = executor.submit(() -> {
+ try (var os = pos) {
+ consumer.accept(os);
+ } catch (Throwable e) {
+ // if an error occurs, the pipe will close
+ throw new ContentProcessorException(new IOException(e));
+ }
+ });
+
+ return new FutureInputStream(pis, future);
+ }
+
+ public InputStream pipe(InputStream inputStream, ContentProcessor.InputOutputStreamConsumer consumer) {
+ // Java default buffer is 1024 bytes
+ var pis = new PipedInputStream(requireNonNullElse(properties.getContent().getDefaultBufferSize(), 8192));
+ var pos = newPipedOutputStream(pis);
+
+ // submit the work to the executor
+ var future = executor.submit(() -> {
+ try (var os = pos; var is = inputStream) {
+ // run the user logic (e.g., zipping, resizing)
+ consumer.accept(is, os);
+ // flushing is handled by try-with-resources close()
+ } catch (Throwable e) {
+ // if an error occurs, the pipe will close
+ throw new ContentProcessorException(new IOException(e));
+ }
+ });
+
+ // tracks errors
+ return new FutureInputStream(pis, future);
+ }
+
+ public void write(InputStream is, OutputStream os, byte[] buffer) throws IOException {
+ int bytesRead;
+
+ while ((bytesRead = is.read(buffer)) != -1) {
+ os.write(buffer, 0, bytesRead);
+ }
+ }
+
+ private PipedOutputStream newPipedOutputStream(PipedInputStream pis) {
+ try {
+ return new PipedOutputStream(pis);
+ } catch (IOException e) {
+ throw new ContentProcessorException(e);
+ }
+ }
+
+ private static class FutureInputStream extends InputStream {
+
+ private final PipedInputStream delegate;
+ private final Future> future;
+
+ FutureInputStream(PipedInputStream delegate, Future> future) {
+ this.delegate = delegate;
+ this.future = future;
+ }
+
+ @SuppressWarnings("AvoidHidingCauseException")
+ private void checkException() throws IOException {
+ // if the task is done, check for exceptions
+ if (future.isDone()) {
+ try {
+ // throws "ExecutionException" if task failed
+ future.get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+
+ throw new IOException("Processing interrupted", e);
+ } catch (ExecutionException e) {
+ // unwrap cause
+ Throwable cause = e.getCause();
+
+ if (cause instanceof IOException ioe) {
+ throw ioe;
+ }
+
+ throw new IOException("Processing failed in worker thread", cause);
+ }
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ checkException();
+
+ int data = delegate.read();
+
+ // check EOF because of error
+ checkException();
+
+ return data;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ checkException();
+
+ int count = delegate.read(b, off, len);
+
+ checkException();
+
+ return count;
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+
+ // good practice
+ future.cancel(true);
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/DefaultContentPathRandomizer.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/DefaultContentPathRandomizer.java
new file mode 100644
index 000000000..90da0da83
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/DefaultContentPathRandomizer.java
@@ -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.contentstore.util;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DefaultContentPathRandomizer implements ContentPathRandomizer {
+
+ private final FineractProperties properties;
+
+ @Override
+ public String randomize() {
+ // TODO: make length configurable
+ return RandomStringUtils.secureStrong().randomAlphabetic(16);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/DefaultContentPathSanitizer.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/DefaultContentPathSanitizer.java
new file mode 100644
index 000000000..2827480d6
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/util/DefaultContentPathSanitizer.java
@@ -0,0 +1,47 @@
+/**
+ * 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.contentstore.util;
+
+import java.nio.file.Path;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DefaultContentPathSanitizer implements ContentPathSanitizer {
+
+ private final FineractProperties properties;
+
+ @Override
+ public String sanitize(String path) {
+ final var sanitizedPath = Path.of(path).normalize().toString();
+
+ if (log.isDebugEnabled()) {
+ final var fileName = FilenameUtils.getName(sanitizedPath).toLowerCase();
+
+ log.debug("Path: {} -> {} ({})", path, sanitizedPath, fileName);
+ }
+
+ return sanitizedPath;
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ContentResources.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ContentResources.java
deleted file mode 100644
index 905d2a614..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ContentResources.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * 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);
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentApiConstants.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentApiConstants.java
new file mode 100644
index 000000000..59fe93273
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentApiConstants.java
@@ -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;
+
+public final class DocumentApiConstants {
+
+ private DocumentApiConstants() {}
+
+ public static final String DOCUMENT_API_PARAM_ENTITY_TYPE = "entityType";
+ public static final String DOCUMENT_API_PARAM_ENTITY_ID = "entityId";
+ public static final String DOCUMENT_API_PARAM_DOCUMENT_ID = "documentId";
+ public static final String DOCUMENT_API_PARAM_FILE = "file";
+ public static final String DOCUMENT_API_PARAM_NAME = "name";
+ public static final String DOCUMENT_API_PARAM_DESCRIPTION = "description";
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentApiResource.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentApiResource.java
new file mode 100644
index 000000000..1922b2bb0
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentApiResource.java
@@ -0,0 +1,253 @@
+/**
+ * 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 static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_DESCRIPTION;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_DOCUMENT_ID;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_ID;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_TYPE;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_FILE;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_NAME;
+import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_ATTACHMENT;
+import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
+import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+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.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.CommandPipeline;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCreateCommand;
+import org.apache.fineract.infrastructure.documentmanagement.command.DocumentDeleteCommand;
+import org.apache.fineract.infrastructure.documentmanagement.command.DocumentUpdateCommand;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
+import org.apache.fineract.util.StreamResponseUtil;
+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
+@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
+
+ Note: the currently allowed entities are
+
+ - clients: URL pattern as clients
+ - staff: URL pattern as staff
+ - loans: URL pattern as loans
+ - savings: URL pattern as savings
+ - client identifiers: URL pattern as client_identifiers
+ - groups: URL pattern as groups
+ """)
+@RequiredArgsConstructor
+public class DocumentApiResource {
+
+ private final DocumentReadPlatformService documentReadPlatformService;
+ private final FileUploadValidator fileUploadValidator;
+ private final ContentDetectorManager contentDetectorManager;
+ private final CommandPipeline commandPipeline;
+
+ @GET
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "List documents", description = """
+ Example Requests:
+
+ - clients/1/documents
+ - client_identifiers/1/documents
+ - loans/1/documents?fields=name,description
+ """)
+ public List retrieveAllDocuments(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId) {
+
+ return documentReadPlatformService.retrieveAllDocuments(entityType, entityId);
+ }
+
+ @GET
+ @Path("{documentId}")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "Retrieve a Document", description = """
+ Example Requests:
+
+ - clients/1/documents/1
+ - loans/1/documents/1
+ - client_identifiers/1/documents/1?fields=name,description
+ """)
+ public DocumentData getDocument(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId,
+ @PathParam(DOCUMENT_API_PARAM_DOCUMENT_ID) final Long documentId) {
+
+ return documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
+ }
+
+ @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
+
+ Example Requests:
+
+ - clients/1/documents/1/attachment
+ - loans/1/documents/1/inline
+ """)
+ @ApiResponse(responseCode = "200", description = "Not Shown: The corresponding Binary file")
+ public Response downloadFile(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId,
+ @PathParam(DOCUMENT_API_PARAM_DOCUMENT_ID) final Long documentId) {
+
+ final var content = documentReadPlatformService.retrieveDocumentContent(entityType, entityId, documentId);
+
+ return StreamResponseUtil
+ .ok(StreamResponseUtil.StreamResponseData.builder().type(content.getContentType()).fileName(content.getFileName())
+ .dispositionType(DISPOSITION_TYPE_ATTACHMENT).stream(content.getStream()).size(content.getSize()).build());
+ }
+
+ @POST
+ @Consumes({ MediaType.MULTIPART_FORM_DATA })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "Create a Document", description = """
+ Note: A document is created using a Multi-part form upload
+
+ Body parts
+
+ - name : name or summary of the document
+ - description : description of the document
+ - file : the file to be uploaded
+
+ Mandatory fields :
+
+ - file
+ - description
+ """)
+ @ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)")
+ public DocumentCreateResponse createDocument(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @HeaderParam(CONTENT_LENGTH) final Long fileSize,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream is,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart filePart,
+ @FormDataParam(DOCUMENT_API_PARAM_NAME) final String name,
+ @FormDataParam(DOCUMENT_API_PARAM_DESCRIPTION) final String description) {
+
+ fileUploadValidator.validate(fileSize, is, fileDetails, filePart);
+
+ final var command = new DocumentCreateCommand();
+
+ var type = Optional.ofNullable(filePart.getMediaType()).map(MediaType::toString)
+ .or(() -> Optional.of(contentDetectorManager
+ .detect(ContentDetectorContext.builder().fileName(fileDetails.getFileName()).build()).getMimeType()))
+ .orElse(APPLICATION_OCTET_STREAM_VALUE);
+
+ command.setPayload(DocumentCreateRequest.builder().entityId(entityId).entityType(entityType).name(name).description(description)
+ .fileName(fileDetails.getFileName()).size(fileSize).type(type).stream(is).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ return response.get();
+ }
+
+ @PUT
+ @Path("{documentId}")
+ @Consumes({ MediaType.MULTIPART_FORM_DATA })
+ @Produces({ MediaType.APPLICATION_JSON })
+ // @RequestBody(description = "Update document", content = { @Content(mediaType = MediaType.MULTIPART_FORM_DATA) })
+ @Operation(summary = "Update a Document", description = """
+ Note: A document is updated using a Multi-part form upload
+
+ Body Parts
+
+ - name: name or summary of the document
+ - description: description of the document
+ - file: the file to be uploaded
+ """)
+ @ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)")
+ public DocumentUpdateResponse updateDocument(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @PathParam(DOCUMENT_API_PARAM_DOCUMENT_ID) final Long documentId,
+ @HeaderParam(CONTENT_LENGTH) final Long fileSize, @FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream is,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart filePart,
+ @FormDataParam(DOCUMENT_API_PARAM_NAME) final String name,
+ @FormDataParam(DOCUMENT_API_PARAM_DESCRIPTION) final String description) {
+
+ final var command = new DocumentUpdateCommand();
+
+ final var request = DocumentUpdateRequest.builder().id(documentId).entityId(entityId).entityType(entityType).name(name)
+ .description(description).stream(is);
+
+ if (fileDetails != null) {
+ request.fileName(fileDetails.getFileName()).type(fileDetails.getType()).size(fileSize);
+ }
+
+ command.setPayload(DocumentUpdateRequest.builder().id(documentId).entityId(entityId).entityType(entityType).name(name)
+ .description(description).stream(is).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ // TODO: does not return list of changes, should be done for consistency with rest of API
+ return response.get();
+ }
+
+ @DELETE
+ @Path("{documentId}")
+ @Operation(summary = "Remove a Document", description = """
+ """)
+ @ApiResponse(responseCode = "200", description = "OK")
+ public DocumentDeleteResponse deleteDocument(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId,
+ @PathParam(DOCUMENT_API_PARAM_DOCUMENT_ID) final Long documentId) {
+
+ final var command = new DocumentDeleteCommand();
+
+ command.setPayload(DocumentDeleteRequest.builder().id(documentId).entityId(entityId).entityType(entityType).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ return response.get();
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java
deleted file mode 100644
index c44d4fcaa..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java
+++ /dev/null
@@ -1,249 +0,0 @@
-/**
- * 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.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.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.MediaType;
-import jakarta.ws.rs.core.Response;
-import java.io.InputStream;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import lombok.RequiredArgsConstructor;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-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
-
- Note: The currently allowed Entities are
-
- Clients: URL Pattern as clients
- Staff: URL Pattern as staff
- Loans: URL Pattern as loans
- Savings: URL Pattern as savings
- Client Identifiers: URL Pattern as client_identifiers
- Groups: URL Pattern as groups""")
-@RequiredArgsConstructor
-public class DocumentManagementApiResource {
-
- private static final String SYSTEM_ENTITY_TYPE = "DOCUMENT";
-
- private final PlatformSecurityContext context;
- private final DocumentReadPlatformService documentReadPlatformService;
- private final DocumentWritePlatformService documentWritePlatformService;
- private final FileUploadValidator fileUploadValidator;
-
- @GET
- @Consumes({ MediaType.APPLICATION_JSON })
- @Produces({ MediaType.APPLICATION_JSON })
- @Operation(summary = "List documents", description = """
- Example Requests:
-
- clients/1/documents
-
- client_identifiers/1/documents
-
- loans/1/documents?fields=name,description""")
- public List retrieveAllDocuments(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
- @PathParam("entityId") @Parameter(description = "entityId") final Long entityId) {
-
- context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
- return documentReadPlatformService.retrieveAllDocuments(entityType, entityId);
- }
-
- @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\s
-
- Body Parts
-
- name :\s
- Name or summary of the document
-
- description :\s
- Description of the document
-
- file :\s
- The file to be uploaded
-
- Mandatory Fields :\s
- file and description""")
-
- @ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.PostEntityTypeEntityIdDocumentsResponse.class)))
- public CommandProcessingResult 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 = documentWritePlatformService.createDocument(documentCommand, inputStream);
- return 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\s
- Body Parts
- name
- Name or summary of the document
- description
- Description of the document
- file
- The file to be uploaded""")
-
- @ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.PutEntityTypeEntityIdDocumentsResponse.class)))
- public CommandProcessingResult 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 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;
- 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
- **/
- return documentWritePlatformService.updateDocument(documentCommand, inputStream);
- }
-
- @GET
- @Path("{documentId}")
- @Consumes({ MediaType.APPLICATION_JSON })
- @Produces({ MediaType.APPLICATION_JSON })
- @Operation(summary = "Retrieve a Document", description = """
- Example Requests:
-
- clients/1/documents/1
-
-
- loans/1/documents/1
-
-
- client_identifiers/1/documents/1?fields=name,description""")
- public DocumentData 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.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
- return documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
- }
-
- @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
-
- Example Requests:
-
- clients/1/documents/1/attachment
-
-
- loans/1/documents/1/attachment""")
- @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) {
-
- context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
- final FileData fileData = 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 = "")
-
- @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.DeleteEntityTypeEntityIdDocumentsResponse.class)))
- public CommandProcessingResult 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);
-
- return documentWritePlatformService.deleteDocument(documentCommand);
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResourceSwagger.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResourceSwagger.java
deleted file mode 100644
index 91ad2f2c9..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResourceSwagger.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * 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 = "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() {
-
- }
-
- @Schema(example = "3")
- public Long resourceId;
- @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;
- }
- }
-
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImageApiConstants.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImageApiConstants.java
new file mode 100644
index 000000000..54c8d473e
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImageApiConstants.java
@@ -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.api;
+
+public final class ImageApiConstants {
+
+ private ImageApiConstants() {}
+
+ public static final String IMAGE_API_PARAM_MAX_WIDTH = "maxWidth";
+ public static final String IMAGE_API_PARAM_MAX_HEIGHT = "maxHeight";
+ public static final String IMAGE_API_PARAM_OUTPUT = "output";
+ public static final String IMAGE_API_VALUE_OUTPUT_INLINE_OCTET = "inline_octet";
+ public static final String IMAGE_API_VALUE_IMAGE_TYPE_JPG = "jpg";
+ public static final String IMAGE_API_VALUE_ENCODING_BASE64 = "base64";
+ // public static final String IMAGE_API_VALUE_IMAGE_TYPE_GIF = "gif";
+ // public static final String IMAGE_API_VALUE_IMAGE_TYPE_PNG = "png";
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java
index 2c3f257c2..b29b99346 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java
@@ -18,9 +18,30 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.api;
+import static java.util.Objects.requireNonNull;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_CONTENT_TYPE;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_ENCODING;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_FORMAT;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_HEIGHT;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_WIDTH;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_ID;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_TYPE;
+import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_FILE;
+import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_PARAM_MAX_HEIGHT;
+import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_PARAM_MAX_WIDTH;
+import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_PARAM_OUTPUT;
+import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_VALUE_ENCODING_BASE64;
+import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_VALUE_OUTPUT_INLINE_OCTET;
+import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_ATTACHMENT;
+import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_INLINE;
+import static org.springframework.http.HttpHeaders.ACCEPT;
+import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
+import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
+import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
+
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 jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
@@ -33,196 +54,175 @@ 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 java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
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.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.command.core.CommandPipeline;
+import org.apache.fineract.infrastructure.contentstore.processor.Base64DecoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.Base64EncoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.ContentProcessorContext;
+import org.apache.fineract.infrastructure.contentstore.processor.DataUrlDecoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor;
+import org.apache.fineract.infrastructure.documentmanagement.command.ImageCreateCommand;
+import org.apache.fineract.infrastructure.documentmanagement.command.ImageDeleteCommand;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
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.util.StreamResponseUtil;
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;
+// NOTE: left for backward compatibility only, could be unified with documents
+@Deprecated
@Slf4j
@RequiredArgsConstructor
@Component
-@Path("/v1/{entity}/{entityId}/images")
+@Path("/v1/{entityType}/{entityId}/images")
public class ImagesApiResource {
- private final PlatformSecurityContext context;
private final ImageReadPlatformService imageReadPlatformService;
- private final ImageWritePlatformService imageWritePlatformService;
+ private final CommandPipeline commandPipeline;
+ private final ImageResizeContentProcessor imageResizeContentProcessor;
+ private final Base64EncoderContentProcessor base64EncoderContentProcessor;
+ private final DataUrlEncoderContentProcessor dataUrlEncoderContentProcessor;
+ private final Base64DecoderContentProcessor base64DecoderContentProcessor;
+ private final DataUrlDecoderContentProcessor dataUrlDecoderContentProcessor;
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 CommandProcessingResult 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());
-
- return imageWritePlatformService.saveOrUpdateImage(entityName, entityId, fileDetails.getFileName(), inputStream, fileSize);
- }
-
- /**
- * 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 CommandProcessingResult addNewClientImage(@PathParam("entity") final String entityName,
- @PathParam("entityId") final Long entityId, final String jsonRequestBody) {
- validateEntityTypeforImage(entityName);
-
- final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody);
-
- return imageWritePlatformService.saveOrUpdateImage(entityName, entityId, base64EncodedImage);
- }
-
- /**
- * 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)) {
- context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE");
- } else if (EntityTypeForImages.STAFF.toString().equalsIgnoreCase(entityName)) {
- context.authenticatedUser().validateHasReadPermission("STAFFIMAGE");
+ public Response retrieveImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityName,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @QueryParam(IMAGE_API_PARAM_MAX_WIDTH) final Integer maxWidth,
+ @QueryParam(IMAGE_API_PARAM_MAX_HEIGHT) final Integer maxHeight, @QueryParam(IMAGE_API_PARAM_OUTPUT) String output,
+ @HeaderParam(ACCEPT) String acceptHeader) {
+
+ // TODO: pass resize information here and do all the processing in the service
+ final var content = imageReadPlatformService.retrieveImage(entityName, entityId);
+
+ String dispositionType = null;
+ ContentProcessorContext ctx = new ContentProcessorContext(content.getStream());
+ String type;
+
+ // prevent clients sending non-sense
+ if (!"octet".equalsIgnoreCase(output) && !"inline_octet".equalsIgnoreCase(output)) {
+ output = null;
}
- final FileData imageData = 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 (MediaType.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();
+ if ((StringUtils.isNotEmpty(output) && output.contains("octet")) || APPLICATION_OCTET_STREAM_VALUE.equalsIgnoreCase(acceptHeader)) {
+ if (maxWidth != null && maxHeight != null) {
+ ctx = imageResizeContentProcessor.process(content.getStream(), Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, maxWidth,
+ IMAGE_RESIZE_PARAM_MAX_HEIGHT, maxHeight, IMAGE_RESIZE_PARAM_FORMAT, content.getFormat()));
}
- } catch (IOException e) {
- throw new ContentManagementException(imageData.name(), e.getMessage(), e);
+
+ if (IMAGE_API_VALUE_OUTPUT_INLINE_OCTET.equalsIgnoreCase(output)) {
+ dispositionType = DISPOSITION_TYPE_INLINE;
+ } else {
+ dispositionType = DISPOSITION_TYPE_ATTACHMENT;
+ }
+
+ type = content.getContentType();
+ } else {
+ // else stream base64 encoded original format
+ if (maxWidth != null && maxHeight != null) {
+ ctx = imageResizeContentProcessor.then(base64EncoderContentProcessor).then(dataUrlEncoderContentProcessor).process(
+ content.getStream(),
+ Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, maxWidth, IMAGE_RESIZE_PARAM_MAX_HEIGHT, maxHeight, IMAGE_RESIZE_PARAM_FORMAT,
+ content.getFormat(), DATA_URL_ENCODE_PARAM_CONTENT_TYPE, content.getContentType(),
+ DATA_URL_ENCODE_PARAM_ENCODING, IMAGE_API_VALUE_ENCODING_BASE64));
+ } else {
+ ctx = base64EncoderContentProcessor.then(dataUrlEncoderContentProcessor).process(content.getStream(),
+ Map.of(DATA_URL_ENCODE_PARAM_CONTENT_TYPE, content.getContentType(), DATA_URL_ENCODE_PARAM_ENCODING,
+ IMAGE_API_VALUE_ENCODING_BASE64));
+ }
+
+ type = TEXT_PLAIN_VALUE;
}
+
+ // make sure we use the transformed input stream ("ctx.getInputStream()")
+ return StreamResponseUtil.ok(StreamResponseUtil.StreamResponseData.builder().fileName(content.getDisplayName()).type(type)
+ .stream(ctx.getInputStream()).dispositionType(dispositionType).build());
+ }
+
+ @POST
+ @Consumes({ MediaType.MULTIPART_FORM_DATA })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)")
+ public ImageCreateResponse createImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @HeaderParam(CONTENT_LENGTH) final Long fileSize,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream is,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart filePart) {
+
+ fileUploadValidator.validate(fileSize, is, fileDetails, filePart);
+
+ final var command = new ImageCreateCommand();
+
+ command.setPayload(
+ ImageCreateRequest.builder().entityId(entityId).entityType(entityType).fileName(fileDetails.getFileName()).size(fileSize)
+ .type(Optional.ofNullable(filePart.getMediaType()).map(MediaType::toString).orElse(null)).stream(is).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ return response.get();
+ }
+
+ @POST
+ @Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
+ public ImageCreateResponse createImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, final InputStream body) {
+
+ requireNonNull(body, "Missing input stream");
+
+ final var command = new ImageCreateCommand();
+
+ var ctx = dataUrlDecoderContentProcessor.then(base64DecoderContentProcessor).process(new ContentProcessorContext(body));
+
+ command.setPayload(ImageCreateRequest.builder().entityId(entityId).entityType(entityType).stream(ctx.getInputStream()).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ return response.get();
}
- /**
- * 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 CommandProcessingResult 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);
+ @RequestBody(description = "Update image", content = { @Content(mediaType = MediaType.MULTIPART_FORM_DATA) })
+ public ImageCreateResponse updateImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityName,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @HeaderParam(CONTENT_LENGTH) final Long fileSize,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream inputStream,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
+ @FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart bodyPart) {
+ return createImage(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 CommandProcessingResult updateClientImage(@PathParam("entity") final String entityName,
- @PathParam("entityId") final Long entityId, final String jsonRequestBody) {
- return addNewClientImage(entityName, entityId, jsonRequestBody);
+ public ImageCreateResponse updateImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityName,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, final InputStream body) {
+ return createImage(entityName, entityId, body);
}
@DELETE
- @Consumes(MediaType.APPLICATION_JSON)
- @Produces(MediaType.APPLICATION_JSON)
- public CommandProcessingResult deleteClientImage(@PathParam("entity") final String entityName,
- @PathParam("entityId") final Long entityId) {
- validateEntityTypeforImage(entityName);
- imageWritePlatformService.deleteImage(entityName, entityId);
- return CommandProcessingResult.resourceResult(entityId);
- }
+ public ImageDeleteResponse deleteImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId) {
- /*** Entities for document Management **/
- public enum EntityTypeForImages {
+ final var command = new ImageDeleteCommand();
- STAFF, //
- CLIENTS; //
+ command.setPayload(ImageDeleteRequest.builder().entityId(entityId).entityType(entityType).build());
- @Override
- public String toString() {
- return name().toLowerCase();
- }
- }
+ final Supplier response = commandPipeline.send(command);
- 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;
+ return response.get();
}
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCommand.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCommand.java
index 0c430c8fd..ecfa5a294 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCommand.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCommand.java
@@ -20,9 +20,7 @@ package org.apache.fineract.infrastructure.documentmanagement.command;
import java.util.Set;
-/**
- * Immutable command for creating or updating details of a client identifier.
- */
+@Deprecated(forRemoval = true)
public class DocumentCommand {
private final Long id;
@@ -35,13 +33,9 @@ public class DocumentCommand {
private Long size;
private String type;
private String location;
- private Integer storageType;
-
- private final Set modifiedParameters;
public DocumentCommand(final Set 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;
@@ -89,10 +83,6 @@ public class DocumentCommand {
return this.location;
}
- public Set getModifiedParameters() {
- return this.modifiedParameters;
- }
-
public void setFileName(final String fileName) {
this.fileName = fileName;
}
@@ -108,37 +98,4 @@ public class DocumentCommand {
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");
- }
-
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCommandValidator.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCommandValidator.java
deleted file mode 100644
index 4cb094397..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCommandValidator.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * 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 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 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);
- }
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCreateCommand.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCreateCommand.java
new file mode 100644
index 000000000..a8b0d1e65
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentCreateCommand.java
@@ -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.command;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class DocumentCreateCommand extends Command {}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentDeleteCommand.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentDeleteCommand.java
new file mode 100644
index 000000000..1f0b5dc90
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentDeleteCommand.java
@@ -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.command;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class DocumentDeleteCommand extends Command {}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentUpdateCommand.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentUpdateCommand.java
new file mode 100644
index 000000000..609b76bab
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/DocumentUpdateCommand.java
@@ -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.command;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class DocumentUpdateCommand extends Command {}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/ImageCreateCommand.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/ImageCreateCommand.java
new file mode 100644
index 000000000..2018da75e
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/ImageCreateCommand.java
@@ -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.command;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImageCreateCommand extends Command {}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/ImageDeleteCommand.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/ImageDeleteCommand.java
new file mode 100644
index 000000000..e510df660
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/command/ImageDeleteCommand.java
@@ -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.command;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImageDeleteCommand extends Command {}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepository.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepository.java
deleted file mode 100644
index 831e7b682..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepository.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * 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();
-
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java
deleted file mode 100644
index b17803e02..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * 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 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)));
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java
deleted file mode 100644
index d13d08b72..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/**
- * 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 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);
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentPathSanitizer.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentPathSanitizer.java
deleted file mode 100644
index 9d7c55889..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentPathSanitizer.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * 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 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");
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java
deleted file mode 100644
index 6d546ba25..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * 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.getLocation());
-
- final File file = new File(sanitizedPath);
- return new FileData(Files.asByteSource(file), documentData.getFileName(), documentData.getType());
- }
-
- @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().replace(" ", "").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().replace(" ", "").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);
- }
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java
deleted file mode 100644
index 180a75121..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/**
- * 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.getLocation()).build(), ResponseTransformer.toBytes()).asInputStream();
- }
- }, documentData.getFileName(), documentData.getType());
- }
-
- @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 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);
- }
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentContent.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentContent.java
new file mode 100644
index 000000000..f56a57120
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentContent.java
@@ -0,0 +1,48 @@
+/**
+ * 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.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.Hidden;
+import java.io.InputStream;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentContent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String displayName;
+ private String fileName;
+ private String contentType;
+ private String format;
+ private Long size;
+ @Hidden
+ @JsonIgnore
+ private InputStream stream;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentCreateRequest.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentCreateRequest.java
new file mode 100644
index 000000000..9fe12c67a
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentCreateRequest.java
@@ -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.data;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.Hidden;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.io.InputStream;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentCreateRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Size(max = 50, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String entityType;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long entityId;
+ @Size(max = 250, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String name;
+ @Size(max = 250, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String description;
+ @Size(max = 250, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String fileName;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Min(value = 1, message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long size;
+ private String type;
+ @Hidden
+ @JsonIgnore
+ private InputStream stream;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/FileData.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentCreateResponse.java
similarity index 58%
rename from fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/FileData.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentCreateResponse.java
index a17c5c3f0..d2a3d197c 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/FileData.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentCreateResponse.java
@@ -18,29 +18,22 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.data;
-import com.google.common.io.ByteSource;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
-public class FileData {
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentCreateResponse implements Serializable {
- private final String fileName;
- private final String contentType;
- private final ByteSource byteSource;
+ @Serial
+ private static final long serialVersionUID = 1L;
- 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;
- }
+ private Long resourceId;
+ private String resourceIdentifier;
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentData.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentData.java
similarity index 59%
rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentData.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentData.java
index ec4879822..3621cd35c 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentData.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentData.java
@@ -20,32 +20,28 @@ package org.apache.fineract.infrastructure.documentmanagement.data;
import java.io.Serial;
import java.io.Serializable;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
-/**
- * Immutable data object representing a user document being managed on the platform.
- */
-@Getter
-@RequiredArgsConstructor
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
public class DocumentData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- private final Long id;
- private final String parentEntityType;
- private final Long parentEntityId;
- private final String name;
- private final String fileName;
- private final Long size;
- private final String type;
- private final String location;
- private final String description;
- private final Integer storageType;
-
- public StorageType storageType() {
- return StorageType.fromInt(this.storageType);
- }
+ private Long id;
+ private String parentEntityType;
+ private Long parentEntityId;
+ private String name;
+ private String fileName;
+ private Long size;
+ private String type;
+ private String location;
+ private String description;
+ private Integer storageType;
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentDeleteRequest.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentDeleteRequest.java
new file mode 100644
index 000000000..5557cbe36
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentDeleteRequest.java
@@ -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 jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentDeleteRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long id;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Size(max = 50, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String entityType;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long entityId;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentDeleteResponse.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentDeleteResponse.java
new file mode 100644
index 000000000..59f3858a1
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentDeleteResponse.java
@@ -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.data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentDeleteResponse implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Long resourceId;
+ private String resourceIdentifier;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentUpdateRequest.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentUpdateRequest.java
new file mode 100644
index 000000000..2d9a17f49
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentUpdateRequest.java
@@ -0,0 +1,63 @@
+/**
+ * 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.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.Hidden;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.io.InputStream;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentUpdateRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long id;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Size(max = 50, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String entityType;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long entityId;
+ @Size(max = 250, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String name;
+ @Size(max = 250, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String description;
+ @Size(max = 250, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String fileName;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Min(value = 1, message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long size;
+ private String type;
+ @Hidden
+ @JsonIgnore
+ private InputStream stream;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentUpdateResponse.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentUpdateResponse.java
new file mode 100644
index 000000000..16a34efe9
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentUpdateResponse.java
@@ -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.data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentUpdateResponse implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Long resourceId;
+ private String resourceIdentifier;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageCreateRequest.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageCreateRequest.java
new file mode 100644
index 000000000..a9f517d0c
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageCreateRequest.java
@@ -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.data;
+
+import static jakarta.validation.constraints.Pattern.Flag.CASE_INSENSITIVE;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.Hidden;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import java.io.InputStream;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageCreateRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Size(max = 50, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String entityType;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long entityId;
+ @Size(max = 250, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String fileName;
+ // @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Min(value = 1, message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long size;
+ @Pattern(regexp = "^(image\\/gif|image\\/jpeg|image\\/png)$", flags = CASE_INSENSITIVE, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String type;
+ @Hidden
+ @JsonIgnore
+ private InputStream stream;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageCreateResponse.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageCreateResponse.java
new file mode 100644
index 000000000..fea6695ce
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageCreateResponse.java
@@ -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.data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageCreateResponse implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Long resourceId;
+ private String resourceIdentifier;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageData.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageData.java
deleted file mode 100644
index 18993eb62..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageData.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * 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;
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageDeleteRequest.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageDeleteRequest.java
new file mode 100644
index 000000000..7ef53439c
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageDeleteRequest.java
@@ -0,0 +1,44 @@
+/**
+ * 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 jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageDeleteRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ @Size(max = 50, message = "{org.apache.fineract.document.xxx.not-null}")
+ private String entityType;
+ @NotNull(message = "{org.apache.fineract.document.xxx.not-null}")
+ private Long entityId;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageDeleteResponse.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageDeleteResponse.java
new file mode 100644
index 000000000..af2430a34
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageDeleteResponse.java
@@ -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.data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageDeleteResponse implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Long resourceId;
+ private String resourceIdentifier;
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageResizer.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageResizer.java
deleted file mode 100644
index dc6903630..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/ImageResizer.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * 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 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 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()));
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Document.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Document.java
index 2e3ea9ecd..6d0a25bd0 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Document.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Document.java
@@ -18,150 +18,58 @@
*/
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;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.experimental.FieldNameConstants;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
-@Entity
-@Table(name = "m_document")
-public class Document extends AbstractPersistableCustom {
+@Table("m_document")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+@FieldNameConstants
+public final class Document implements Serializable {
- @Column(name = "parent_entity_type", length = 50)
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column("id")
+ private Long id;
+
+ @Column("parent_entity_type")
private String parentEntityType;
- @Column(name = "parent_entity_id", length = 1000)
+ @Column("parent_entity_id")
private Long parentEntityId;
- @Column(name = "name", length = 250)
+ @Column("name")
private String name;
- @Column(name = "file_name", length = 250)
+ @Column("file_name")
private String fileName;
- @Column(name = "size")
+ @Column("size")
private Long size;
- @Column(name = "type", length = 50)
+ @Column("type")
private String type;
- @Column(name = "description", length = 1000)
+ @Column("description")
private String description;
- @Column(name = "location", length = 500)
+ @Column("location")
private String location;
- @Column(name = "storage_type_enum")
+ @Column("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);
- }
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/DocumentRepository.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/DocumentRepository.java
index 2f4a2b16c..327777663 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/DocumentRepository.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/DocumentRepository.java
@@ -18,9 +18,14 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.domain;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
-public interface DocumentRepository extends JpaRepository, JpaSpecificationExecutor {
- // no added behaviour
+public interface DocumentRepository extends ListCrudRepository, QueryByExampleExecutor {
+
+ List findAllByParentEntityTypeAndParentEntityId(String parentEntityType, Long parentEntityId);
+
+ Optional findByIdAndParentEntityTypeAndParentEntityId(Long id, String entityType, Long entityId);
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Image.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Image.java
similarity index 51%
rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Image.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Image.java
index 190137a84..90bef819a 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Image.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/Image.java
@@ -18,44 +18,37 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.domain;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
-import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.experimental.FieldNameConstants;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
-@Entity
-@Table(name = "m_image")
-public final class Image extends AbstractPersistableCustom {
+@Table("m_image")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+@FieldNameConstants
+public final class Image implements Serializable {
- @Column(name = "location", length = 500)
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column("id")
+ private Long id;
+
+ @Column("location")
private String location;
- @Column(name = "storage_type_enum")
+ @Column("storage_type_enum")
private Integer storageType;
-
- public Image(final String location, final StorageType storageType) {
- this.location = location;
- this.storageType = storageType.getValue();
- }
-
- Image() {
-
- }
-
- public String getLocation() {
- return this.location;
- }
-
- public Integer getStorageType() {
- return this.storageType;
- }
-
- public void setLocation(final String location) {
- this.location = location;
- }
-
- public void setStorageType(final Integer storageType) {
- this.storageType = storageType;
- }
-
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/ImageRepository.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/ImageRepository.java
index fd1e051b1..023216807 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/ImageRepository.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/domain/ImageRepository.java
@@ -18,8 +18,7 @@
*/
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;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
-public interface ImageRepository extends JpaRepository, JpaSpecificationExecutor {}
+public interface ImageRepository extends ListCrudRepository, QueryByExampleExecutor {}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/ContentManagementException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/ContentManagementException.java
deleted file mode 100644
index 7526b870a..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/ContentManagementException.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * 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);
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/InvalidEntityTypeForImageManagementException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentInvalidRequestException.java
similarity index 70%
rename from fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/InvalidEntityTypeForImageManagementException.java
rename to fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentInvalidRequestException.java
index cd0e21ee7..702e6c157 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/InvalidEntityTypeForImageManagementException.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentInvalidRequestException.java
@@ -18,14 +18,11 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.exception;
-import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
-/**
- * Runtime exception for invalid image types
- */
-public class InvalidEntityTypeForImageManagementException extends AbstractPlatformResourceNotFoundException {
+public class DocumentInvalidRequestException extends AbstractPlatformDomainRuleException {
- public InvalidEntityTypeForImageManagementException(String imageType) {
- super("error.imagemanagement.entitytype.invalid", "Image Management is not support for the Entity Type: " + imageType);
+ public DocumentInvalidRequestException(Exception e) {
+ super("error.msg.document.request.invalid", e.getMessage(), e);
}
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java
index cbe3ced4c..578201f1d 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java
@@ -18,24 +18,16 @@
*/
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;
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
-public class DocumentNotFoundException extends AbstractPlatformResourceNotFoundException {
+public class DocumentNotFoundException extends AbstractPlatformDomainRuleException {
+
+ public DocumentNotFoundException(final Long id) {
+ super("error.msg.document.id.invalid", "Document with identifier " + id + " does not exist", id);
+ }
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);
- }
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentCreateCommandHandler.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentCreateCommandHandler.java
new file mode 100644
index 000000000..3250e214d
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentCreateCommandHandler.java
@@ -0,0 +1,51 @@
+/**
+ * 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.handler;
+
+import io.github.resilience4j.retry.annotation.Retry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandHandler;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DocumentCreateCommandHandler implements CommandHandler {
+
+ private final DocumentWritePlatformService writePlatformService;
+
+ @Retry(name = "commandDocumentCreate", fallbackMethod = "fallback")
+ @Transactional
+ @Override
+ public DocumentCreateResponse handle(final Command command) {
+ return writePlatformService.createDocument(command.getPayload());
+ }
+
+ @Override
+ public DocumentCreateResponse fallback(Command command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentDeleteCommandHandler.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentDeleteCommandHandler.java
new file mode 100644
index 000000000..58d812275
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentDeleteCommandHandler.java
@@ -0,0 +1,51 @@
+/**
+ * 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.handler;
+
+import io.github.resilience4j.retry.annotation.Retry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandHandler;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
+import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DocumentDeleteCommandHandler implements CommandHandler {
+
+ private final DocumentWritePlatformService writePlatformService;
+
+ @Retry(name = "commandDocumentDelete", fallbackMethod = "fallback")
+ @Transactional
+ @Override
+ public DocumentDeleteResponse handle(final Command command) {
+ return writePlatformService.deleteDocument(command.getPayload());
+ }
+
+ @Override
+ public DocumentDeleteResponse fallback(Command command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentUpdateCommandHandler.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentUpdateCommandHandler.java
new file mode 100644
index 000000000..a55cb5d7e
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/DocumentUpdateCommandHandler.java
@@ -0,0 +1,51 @@
+/**
+ * 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.handler;
+
+import io.github.resilience4j.retry.annotation.Retry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandHandler;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DocumentUpdateCommandHandler implements CommandHandler {
+
+ private final DocumentWritePlatformService writePlatformService;
+
+ @Retry(name = "commandDocumentUpdate", fallbackMethod = "fallback")
+ @Transactional
+ @Override
+ public DocumentUpdateResponse handle(final Command command) {
+ return writePlatformService.updateDocument(command.getPayload());
+ }
+
+ @Override
+ public DocumentUpdateResponse fallback(Command command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/ImageCreateCommandHandler.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/ImageCreateCommandHandler.java
new file mode 100644
index 000000000..16eb696c0
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/ImageCreateCommandHandler.java
@@ -0,0 +1,51 @@
+/**
+ * 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.handler;
+
+import io.github.resilience4j.retry.annotation.Retry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandHandler;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.service.ImageWritePlatformService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ImageCreateCommandHandler implements CommandHandler {
+
+ private final ImageWritePlatformService writePlatformService;
+
+ @Retry(name = "commandImageCreate", fallbackMethod = "fallback")
+ @Transactional
+ @Override
+ public ImageCreateResponse handle(final Command command) {
+ return writePlatformService.createImage(command.getPayload());
+ }
+
+ @Override
+ public ImageCreateResponse fallback(Command command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/ImageDeleteCommandHandler.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/ImageDeleteCommandHandler.java
new file mode 100644
index 000000000..b302b433d
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/handler/ImageDeleteCommandHandler.java
@@ -0,0 +1,51 @@
+/**
+ * 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.handler;
+
+import io.github.resilience4j.retry.annotation.Retry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandHandler;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
+import org.apache.fineract.infrastructure.documentmanagement.service.ImageWritePlatformService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ImageDeleteCommandHandler implements CommandHandler {
+
+ private final ImageWritePlatformService writePlatformService;
+
+ @Retry(name = "commandImageDelete", fallbackMethod = "fallback")
+ @Transactional
+ @Override
+ public ImageDeleteResponse handle(final Command command) {
+ return writePlatformService.deleteImage(command.getPayload());
+ }
+
+ @Override
+ public ImageDeleteResponse fallback(Command command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/mapping/DocumentMapper.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/mapping/DocumentMapper.java
new file mode 100644
index 000000000..635480d97
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/mapping/DocumentMapper.java
@@ -0,0 +1,43 @@
+/**
+ * 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.mapping;
+
+import static org.mapstruct.InjectionStrategy.CONSTRUCTOR;
+import static org.mapstruct.MappingConstants.ComponentModel.SPRING;
+
+import java.io.InputStream;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
+import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(componentModel = SPRING, injectionStrategy = CONSTRUCTOR)
+public interface DocumentMapper {
+
+ DocumentData map(Document source);
+
+ @Mapping(source = "source.type", target = "contentType")
+ @Mapping(source = "source.name", target = "displayName")
+ @Mapping(source = "source.fileName", target = "fileName")
+ @Mapping(source = "source.size", target = "size")
+ @Mapping(ignore = true, target = "format")
+ @Mapping(source = "stream", target = "stream")
+ DocumentContent map(Document source, InputStream stream);
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformService.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformService.java
index 93e791407..1dab3be03 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformService.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformService.java
@@ -19,14 +19,16 @@
package org.apache.fineract.infrastructure.documentmanagement.service;
import java.util.List;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
-import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
public interface DocumentReadPlatformService {
List retrieveAllDocuments(String entityType, Long entityId);
- FileData retrieveFileData(String entityType, Long entityId, Long documentId);
+ DocumentData retrieveDocument(Long id);
+
+ DocumentContent retrieveDocumentContent(String entityType, Long entityId, Long documentId);
DocumentData retrieveDocument(String entityType, Long entityId, Long documentId);
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java
index 84c4e1260..2437d810f 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java
@@ -18,108 +18,50 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
-import java.sql.ResultSet;
-import java.sql.SQLException;
import java.util.List;
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.contentstore.service.ContentStoreService;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
-import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
+import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
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.apache.fineract.infrastructure.documentmanagement.mapping.DocumentMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformService {
- private final JdbcTemplate jdbcTemplate;
- private final PlatformSecurityContext context;
- private final ContentRepositoryFactory contentRepositoryFactory;
+ private final ContentStoreService storeService;
+ private final DocumentRepository documentRepository;
+ private final DocumentMapper documentMapper;
@Override
public List retrieveAllDocuments(final String entityType, final Long entityId) {
+ final var docs = documentRepository.findAllByParentEntityTypeAndParentEntityId(entityType, 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
+ return docs.stream().map(documentMapper::map).toList();
}
@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);
- }
+ public DocumentData retrieveDocument(final Long id) {
+ return documentRepository.findById(id).map(documentMapper::map).orElseThrow(() -> new DocumentNotFoundException(id));
+ }
+
+ @Override
+ public DocumentContent retrieveDocumentContent(final String entityType, final Long entityId, final Long documentId) {
+ final var doc = documentRepository.findByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
+ .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
+ final var is = storeService.download(doc.getLocation());
+
+ return documentMapper.map(doc, is);
}
@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);
- }
- }
+ final var doc = documentRepository.findByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
+ .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
- 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 {
-
- 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, location, description,
- storageType);
- }
+ return documentMapper.map(doc);
}
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformService.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformService.java
index 807a8d977..d583fe872 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformService.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformService.java
@@ -18,24 +18,18 @@
*/
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;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
public interface DocumentWritePlatformService {
- @PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_DOCUMENT')")
- Long createDocument(DocumentCommand documentCommand, InputStream inputStream);
+ DocumentCreateResponse createDocument(DocumentCreateRequest request);
- @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);
+ DocumentUpdateResponse updateDocument(DocumentUpdateRequest request);
+ DocumentDeleteResponse deleteDocument(DocumentDeleteRequest request);
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java
new file mode 100644
index 000000000..576781d6c
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java
@@ -0,0 +1,175 @@
+/**
+ * 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 static java.util.Objects.requireNonNull;
+
+import jakarta.validation.Valid;
+import java.util.Objects;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
+import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
+import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
+import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
+import org.apache.fineract.infrastructure.documentmanagement.mapping.DocumentMapper;
+import org.apache.fineract.infrastructure.event.business.domain.document.DocumentCreatedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.document.DocumentDeletedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformService {
+
+ private static final String STORE_PREFIX = "documents";
+
+ private final DocumentRepository documentRepository;
+ private final DocumentMapper documentMapper;
+ private final ContentStoreService storeService;
+ private final BusinessEventNotifierService businessEventNotifierService;
+ private final FineractProperties properties;
+
+ @Transactional
+ @Override
+ public DocumentCreateResponse createDocument(@Valid final DocumentCreateRequest request) {
+ try {
+ requireNonNull(request, "Request parameter required");
+ requireNonNull(request.getFileName(), "File name parameter required");
+ requireNonNull(request.getStream(), "Document content required");
+
+ // TODO: make "prefix" configurable
+ var path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), request.getFileName(),
+ storeService.getDelimiter());
+
+ path = storeService.upload(path, request.getStream(), request.getType());
+
+ final var doc = new Document().setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId())
+ .setName(Optional.ofNullable(request.getName()).orElse(request.getFileName())).setFileName(request.getFileName())
+ .setSize(request.getSize()).setType(request.getType()).setDescription(request.getDescription()).setLocation(path)
+ .setStorageType(storeService.getType().getValue());
+
+ documentRepository.save(doc);
+
+ businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(documentMapper.map(doc)));
+
+ return DocumentCreateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
+ } catch (final Exception e) {
+ throw ErrorHandler.getMappable(e, "error.msg.document.unknown.data.integrity.issue",
+ "Unknown data integrity issue with resource.");
+ }
+ }
+
+ @Transactional
+ @Override
+ public DocumentUpdateResponse updateDocument(@Valid final DocumentUpdateRequest request) {
+ try {
+ requireNonNull(request, "Request parameter required");
+
+ String path = null;
+
+ if (StringUtils.isNotEmpty(request.getFileName())) {
+ // TODO: make "prefix" configurable
+ path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), request.getFileName(),
+ storeService.getDelimiter());
+ }
+
+ final var doc = this.documentRepository.findById(request.getId())
+ .orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), request.getId()));
+
+ doc.setStorageType(storeService.getType().getValue());
+
+ if (StringUtils.isNotEmpty(request.getFileName())) {
+ // these two only make sense if we have an actual file
+
+ if (request.getStream() != null && StringUtils.isNotEmpty(path) && !Strings.CI.equals(doc.getLocation(), path)) {
+ storeService.delete(doc.getLocation());
+ storeService.upload(path, request.getStream(), request.getType());
+ doc.setLocation(path);
+ doc.setFileName(FilenameUtils.getName(path));
+ }
+ } else if (request.getStream() != null) {
+ storeService.upload(doc.getLocation(), request.getStream(), request.getType());
+ }
+ if (StringUtils.isNotEmpty(request.getDescription()) && !Strings.CI.equals(doc.getDescription(), request.getDescription())) {
+ doc.setDescription(request.getDescription());
+ }
+ if (StringUtils.isNotEmpty(request.getName()) && !Strings.CI.equals(doc.getName(), request.getName())) {
+ doc.setName(request.getName());
+ }
+ if (Objects.requireNonNullElse(request.getSize(), 0L) > 1L && !Objects.equals(doc.getSize(), request.getSize())) {
+ doc.setSize(request.getSize());
+ }
+
+ documentRepository.save(doc);
+
+ // TODO: shouldn't we send a business event?
+ // businessEventNotifierService.notifyPostBusinessEvent(new DocumentUpdateBusinessEvent(doc));
+
+ return DocumentUpdateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
+ } catch (final Exception e) {
+ throw ErrorHandler.getMappable(e, "error.msg.document.unknown.data.integrity.issue",
+ "Unknown data integrity issue with resource.");
+ }
+ }
+
+ @Transactional
+ @Override
+ public DocumentDeleteResponse deleteDocument(@Valid final DocumentDeleteRequest request) {
+ try {
+ final var doc = this.documentRepository.findById(request.getId())
+ .orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), request.getId()));
+
+ storeService.delete(doc.getLocation());
+
+ documentRepository.deleteById(request.getId());
+
+ businessEventNotifierService.notifyPostBusinessEvent(new DocumentDeletedBusinessEvent(documentMapper.map(doc)));
+
+ return DocumentDeleteResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
+ } catch (final Exception e) {
+ throw ErrorHandler.getMappable(e, "error.msg.document.unknown.data.integrity.issue",
+ "Unknown data integrity issue with resource.");
+ }
+ }
+
+ private String getPath(final String prefix, final String entityType, final Long entityId, final String fileName, String delimiter) {
+ requireNonNull(prefix);
+ requireNonNull(entityType);
+ requireNonNull(entityId);
+ requireNonNull(fileName);
+ requireNonNull(delimiter);
+
+ return String.join(delimiter, prefix, entityType, String.valueOf(entityId), fileName);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java
deleted file mode 100644
index 0ae4f5998..000000000
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/**
- * 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.event.business.domain.document.DocumentCreatedBusinessEvent;
-import org.apache.fineract.infrastructure.event.business.domain.document.DocumentDeletedBusinessEvent;
-import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
-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;
- private final BusinessEventNotifierService businessEventNotifierService;
-
- @Autowired
- public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DocumentRepository documentRepository,
- final ContentRepositoryFactory documentStoreFactory, BusinessEventNotifierService businessEventNotifierService) {
- this.context = context;
- this.documentRepository = documentRepository;
- this.contentRepositoryFactory = documentStoreFactory;
- this.businessEventNotifierService = businessEventNotifierService;
- }
-
- @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);
- businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(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);
-
- return createDocument(documentCommand, inputStream);
-
- }
-
- @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());
- businessEventNotifierService.notifyPostBusinessEvent(new DocumentDeletedBusinessEvent(document));
- 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().toLowerCase();
- }
- }
-}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformService.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformService.java
index 608f64762..9684e77e8 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformService.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformService.java
@@ -18,10 +18,10 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
-import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
public interface ImageReadPlatformService {
- FileData retrieveImage(String entityType, Long entityId);
+ DocumentContent retrieveImage(String entityType, Long entityId);
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
new file mode 100644
index 000000000..056a11122
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
@@ -0,0 +1,69 @@
+/**
+ * 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.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
+import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
+import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
+import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentInvalidRequestException;
+import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ImageReadPlatformServiceImpl implements ImageReadPlatformService {
+
+ @Deprecated
+ private final List imageIdAdapters;
+ private final ContentStoreService storeService;
+ private final ImageRepository imageRepository;
+ private final ContentDetectorManager contentDetectorManager;
+
+ @Override
+ public DocumentContent retrieveImage(final String entityType, final Long entityId) {
+ try {
+ return imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(entityType)).findFirst()
+ .flatMap(imageIdAdapter -> imageIdAdapter.get(entityId))
+ .flatMap(
+ imageIdResult -> imageRepository.findById(imageIdResult.getId())
+ .map(image -> DocumentContent
+ .builder().fileName(
+ FilenameUtils.getName(image.getLocation()))
+ .format(FilenameUtils.getExtension(image.getLocation()))
+ .displayName(imageIdResult.getDisplayName())
+ .contentType(
+ contentDetectorManager
+ .detect(ContentDetectorContext.builder()
+ .fileName(FilenameUtils.getName(image.getLocation())).build())
+ .getMimeType())
+ .stream(storeService.download(image.getLocation())).build()))
+ .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, -1L));
+ } catch (final Exception e) {
+ throw new DocumentInvalidRequestException(e);
+ }
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformService.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformService.java
index 4b30b59b6..f976338ff 100755
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformService.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformService.java
@@ -18,19 +18,14 @@
*/
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;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
public interface ImageWritePlatformService {
- @PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE','CREATE_STAFFIMAGE')")
- CommandProcessingResult saveOrUpdateImage(String entityName, Long entityId, String imageName, InputStream inputStream, Long fileSize);
+ ImageCreateResponse createImage(ImageCreateRequest request);
- @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);
+ ImageDeleteResponse deleteImage(ImageDeleteRequest request);
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java
new file mode 100644
index 000000000..1538788be
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java
@@ -0,0 +1,147 @@
+/**
+ * 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 static java.util.Objects.requireNonNull;
+import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
+import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
+import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
+import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
+import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentInvalidRequestException;
+import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ImageWritePlatformServiceImpl implements ImageWritePlatformService {
+
+ private static final String STORE_PREFIX = "images";
+ private static final String DEFAULT_ENTITY_TYPE = "clients";
+ private static final String DEFAULT_EXTENSION = ".jpg";
+
+ @Deprecated
+ private final List imageIdAdapters;
+ private final ContentStoreService storeService;
+ private final ImageRepository imageRepository;
+ private final ContentDetectorManager contentDetectorManager;
+
+ @Transactional
+ @Override
+ public ImageCreateResponse createImage(final ImageCreateRequest request) {
+ try {
+ if (StringUtils.isEmpty(request.getEntityType())) {
+ // TODO: keeping the path segment always "clients" not consistent how this works with documents
+ request.setEntityType(DEFAULT_ENTITY_TYPE);
+ }
+
+ if (StringUtils.isEmpty(request.getFileName())) {
+ // NOTE: defacto limiting the uploads to JPEG files, same behavior as before
+ request.setFileName(UUID.randomUUID() + DEFAULT_EXTENSION);
+ }
+
+ if (StringUtils.isEmpty(request.getType())) {
+ final var type = Optional
+ .of(contentDetectorManager.detect(ContentDetectorContext.builder().fileName(request.getFileName()).build()))
+ .filter(contentDetectorContext -> StringUtils.isNotEmpty(contentDetectorContext.getMimeType()))
+ .filter(contentDetectorContext -> !APPLICATION_OCTET_STREAM_VALUE
+ .equalsIgnoreCase(contentDetectorContext.getMimeType()))
+ .map(ContentDetectorContext::getMimeType).orElse(APPLICATION_OCTET_STREAM_VALUE);
+
+ request.setType(type);
+ }
+
+ // TODO: make "prefix" configurable?
+ var path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), request.getFileName(),
+ storeService.getDelimiter());
+
+ final var imagePath = storeService.upload(path, request.getStream(), request.getType());
+
+ final var result = imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(request.getEntityType())).findFirst()
+ .flatMap(imageIdAdapter -> imageIdAdapter.get(request.getEntityId()))
+ .flatMap(imageIdResult -> imageRepository.findById(imageIdResult.getId())).map(image -> {
+ // delete old image
+ storeService.delete(image.getLocation());
+
+ return image;
+ }).or(() -> Optional.of(new Image()))
+ .map(image -> image.setLocation(imagePath).setStorageType(storeService.getType().getValue())).map(imageRepository::save)
+ .map(image -> ImageCreateResponse.builder().resourceId(image.getId()).build());
+
+ imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(request.getEntityType())).findFirst()
+ .ifPresent(imageIdAdapter -> result.ifPresent(
+ imageCreateResponse -> imageIdAdapter.set(request.getEntityId(), imageCreateResponse.getResourceId())));
+
+ return result.orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), -1L));
+ } catch (final Exception e) {
+ throw new DocumentInvalidRequestException(e);
+ }
+ }
+
+ @Transactional
+ @Override
+ public ImageDeleteResponse deleteImage(final ImageDeleteRequest request) {
+ try {
+ return delete(request.getEntityType(), request.getEntityId())
+ .orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), -1L));
+ } catch (final Exception e) {
+ throw new DocumentInvalidRequestException(e);
+ }
+ }
+
+ private Optional delete(final String entityType, final Long entityId) {
+ return imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(entityType)).findFirst()
+ .flatMap(imageIdAdapter -> imageIdAdapter.set(entityId, null))
+ .flatMap(imageIdResult -> imageRepository.findById(imageIdResult.getId())).map(image -> {
+ requireNonNull(image.getId(), "Image ID required");
+ requireNonNull(image.getLocation(), "Image location required");
+
+ storeService.delete(image.getLocation());
+
+ imageRepository.deleteById(image.getId());
+
+ return ImageDeleteResponse.builder().resourceId(entityId).resourceIdentifier(entityType).build();
+ });
+ }
+
+ private String getPath(final String prefix, final String entityType, final Long entityId, final String fileName, String delimiter) {
+ requireNonNull(prefix);
+ requireNonNull(entityType);
+ requireNonNull(entityId);
+ requireNonNull(fileName);
+ requireNonNull(delimiter);
+
+ return String.join(delimiter, prefix, entityType, String.valueOf(entityId), fileName);
+ }
+}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentBusinessEvent.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentBusinessEvent.java
index dc0aca7d4..0993476dc 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentBusinessEvent.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentBusinessEvent.java
@@ -18,14 +18,14 @@
*/
package org.apache.fineract.infrastructure.event.business.domain.document;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent;
-public abstract class DocumentBusinessEvent extends AbstractBusinessEvent {
+public abstract class DocumentBusinessEvent extends AbstractBusinessEvent {
private static final String CATEGORY = "Document";
- protected DocumentBusinessEvent(Document value) {
+ protected DocumentBusinessEvent(DocumentData value) {
super(value);
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentCreatedBusinessEvent.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentCreatedBusinessEvent.java
index b323857e7..1c9c5bb9b 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentCreatedBusinessEvent.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentCreatedBusinessEvent.java
@@ -18,13 +18,13 @@
*/
package org.apache.fineract.infrastructure.event.business.domain.document;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
public class DocumentCreatedBusinessEvent extends DocumentBusinessEvent {
private static final String TYPE = "DocumentCreatedBusinessEvent";
- public DocumentCreatedBusinessEvent(Document value) {
+ public DocumentCreatedBusinessEvent(DocumentData value) {
super(value);
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentDeletedBusinessEvent.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentDeletedBusinessEvent.java
index 14184b59e..fb2d4c47e 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentDeletedBusinessEvent.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/event/business/domain/document/DocumentDeletedBusinessEvent.java
@@ -18,13 +18,13 @@
*/
package org.apache.fineract.infrastructure.event.business.domain.document;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
public class DocumentDeletedBusinessEvent extends DocumentBusinessEvent {
private static final String TYPE = "DocumentDeletedBusinessEvent";
- public DocumentDeletedBusinessEvent(Document value) {
+ public DocumentDeletedBusinessEvent(DocumentData value) {
super(value);
}
diff --git a/fineract-document/src/main/resources/jpa/static-weaving/module/fineract-document/persistence.xml b/fineract-document/src/main/resources/jpa/static-weaving/module/fineract-document/persistence.xml
deleted file mode 100644
index 7b0325007..000000000
--- a/fineract-document/src/main/resources/jpa/static-weaving/module/fineract-document/persistence.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
- org.eclipse.persistence.jpa.PersistenceProvider
-
-
- org.apache.fineract.useradministration.domain.Role
- org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
- org.apache.fineract.organisation.workingdays.domain.WorkingDays
- org.apache.fineract.useradministration.domain.Permission
- org.apache.fineract.useradministration.domain.AppUserClientMapping
- org.apache.fineract.commands.domain.CommandSource
- org.apache.fineract.useradministration.domain.AppUser
- org.apache.fineract.accounting.glaccount.domain.GLAccount
- org.apache.fineract.organisation.monetary.domain.OrganisationCurrency
- org.apache.fineract.organisation.staff.domain.Staff
- org.apache.fineract.portfolio.rate.domain.Rate
- org.apache.fineract.organisation.monetary.domain.ApplicationCurrency
- org.apache.fineract.portfolio.calendar.domain.CalendarInstance
- org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail
- org.apache.fineract.portfolio.calendar.domain.Calendar
- org.apache.fineract.portfolio.calendar.domain.CalendarHistory
- org.apache.fineract.portfolio.client.domain.ClientIdentifier
- org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket
- org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange
- org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory
- org.apache.fineract.portfolio.group.domain.Group
- org.apache.fineract.portfolio.client.domain.Client
- org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration
- org.apache.fineract.portfolio.group.domain.GroupRole
- org.apache.fineract.portfolio.paymenttype.domain.PaymentType
- org.apache.fineract.portfolio.group.domain.GroupLevel
- org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEvent
- org.apache.fineract.organisation.office.domain.Office
- org.apache.fineract.organisation.holiday.domain.Holiday
- org.apache.fineract.infrastructure.cache.domain.PlatformCache
- org.apache.fineract.infrastructure.codes.domain.Code
- org.apache.fineract.infrastructure.businessdate.domain.BusinessDate
- org.apache.fineract.infrastructure.codes.domain.CodeValue
-
-
- org.apache.fineract.infrastructure.documentmanagement.domain.Document
-
- false
-
-
-
-
-
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/TestConfiguration.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/TestConfiguration.java
new file mode 100644
index 000000000..32cafe977
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/TestConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * 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;
+
+import static org.apache.fineract.infrastructure.contentstore.processor.ContentProcessor.BEAN_NAME_EXECUTOR;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+@Configuration
+@EnableConfigurationProperties({ FineractProperties.class })
+@PropertySource("classpath:application-test.properties")
+@ComponentScan({ "org.apache.fineract.infrastructure.contentstore.util", "org.apache.fineract.infrastructure.contentstore.detector",
+ "org.apache.fineract.infrastructure.contentstore.processor", "org.apache.fineract.infrastructure.contentstore.policy",
+ "org.apache.fineract.infrastructure.contentstore.service" })
+public class TestConfiguration {
+
+ @Bean(BEAN_NAME_EXECUTOR)
+ public ExecutorService contentProcessorExecutor() {
+ return Executors.newCachedThreadPool();
+ }
+}
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/detector/FileContentDetectorTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/detector/FileContentDetectorTest.java
new file mode 100644
index 000000000..1c6830828
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/detector/FileContentDetectorTest.java
@@ -0,0 +1,52 @@
+/**
+ * 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.contentstore.detector;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.TestConfiguration;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+
+@Slf4j
+@SpringBootTest
+@ContextConfiguration(classes = TestConfiguration.class)
+class FileContentDetectorTest {
+
+ @Autowired
+ private FileContentDetector fileContentDetector;
+
+ @Test
+ void detectByFileName() {
+ var ctx = fileContentDetector.detect(ContentDetectorContext.builder().fileName("test.png").build());
+
+ assertEquals("png", ctx.getFormat());
+ assertEquals(".png", ctx.getExtension());
+ assertEquals("image/png", ctx.getMimeType());
+
+ ctx = fileContentDetector.detect(ContentDetectorContext.builder().fileName("test.jpg").build());
+
+ assertEquals("jpg", ctx.getFormat());
+ assertEquals(".jpg", ctx.getExtension());
+ assertEquals("image/jpeg", ctx.getMimeType());
+ }
+}
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetectorTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetectorTest.java
new file mode 100644
index 000000000..d283c6806
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetectorTest.java
@@ -0,0 +1,134 @@
+/**
+ * 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.contentstore.detector;
+
+import static com.github.romankh3.image.comparison.model.ImageComparisonState.MATCH;
+import static java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.github.romankh3.image.comparison.ImageComparison;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import javax.imageio.ImageIO;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+import org.apache.fineract.infrastructure.TestConfiguration;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentDetectorException;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+
+@Slf4j
+@SpringBootTest
+@ContextConfiguration(classes = TestConfiguration.class)
+class TikaContentDetectorTest {
+
+ @Autowired
+ private TikaContentDetector tikaContentDetector;
+
+ @Test
+ void detectByFileName() {
+ var ctx = tikaContentDetector.detect(ContentDetectorContext.builder().fileName("test.png").build());
+
+ assertEquals("png", ctx.getFormat());
+ assertEquals(".png", ctx.getExtension());
+ assertEquals("image/png", ctx.getMimeType());
+
+ ctx = tikaContentDetector.detect(ContentDetectorContext.builder().fileName("test.jpg").build());
+
+ assertEquals("jpg", ctx.getFormat());
+ assertEquals(".jpg", ctx.getExtension());
+ assertEquals("image/jpeg", ctx.getMimeType());
+ }
+
+ @Test
+ void detectByInputStream() {
+ var ctx = tikaContentDetector.detect(
+ ContentDetectorContext.builder().inputStream(TikaContentDetectorTest.class.getClassLoader().getResourceAsStream("test.png"))
+ .inputStreamEnabled(true).build());
+
+ assertEquals("png", ctx.getFormat());
+ assertEquals(".png", ctx.getExtension());
+ assertEquals("image/png", ctx.getMimeType());
+
+ write(ctx); // make sure we can still consume the input stream
+
+ ctx = tikaContentDetector.detect(
+ ContentDetectorContext.builder().inputStream(TikaContentDetectorTest.class.getClassLoader().getResourceAsStream("test.jpg"))
+ .inputStreamEnabled(true).build());
+
+ assertEquals("jpg", ctx.getFormat());
+ assertEquals(".jpg", ctx.getExtension());
+ assertEquals("image/jpeg", ctx.getMimeType());
+
+ write(ctx);
+ }
+
+ @Test
+ void reuseStream() throws IOException {
+ var ctx = tikaContentDetector.detect(
+ ContentDetectorContext.builder().inputStream(TikaContentDetectorTest.class.getClassLoader().getResourceAsStream("test.png"))
+ .inputStreamEnabled(true).build());
+
+ // we can run the detection again with the same stream
+
+ ctx = tikaContentDetector
+ .detect(ContentDetectorContext.builder().inputStream(ctx.getInputStream()).inputStreamEnabled(true).build());
+
+ assertEquals("image/png", ctx.getMimeType());
+
+ IOUtils.copy(ctx.getInputStream(), new FileOutputStream("build/test.png"));
+
+ try (var expectedIs = TikaContentDetectorTest.class.getClassLoader().getResourceAsStream("test.png");
+ var actualIs = new FileInputStream("build/test.png")) {
+ requireNonNull(expectedIs);
+
+ var expectedImage = ImageIO.read(expectedIs);
+ var actualImage = ImageIO.read(actualIs);
+
+ var result = new ImageComparison(expectedImage, actualImage).setAllowingPercentOfDifferentPixels(0).compareImages();
+
+ log.info("Image diff percentage: {}", result.getDifferencePercent());
+
+ assertEquals(MATCH, result.getImageComparisonState(), "The images should be identical");
+ }
+ }
+
+ @Test
+ void illegalArguments() {
+ var exception = assertThrows(ContentDetectorException.class, () -> {
+ tikaContentDetector.detect(ContentDetectorContext.builder()
+ .inputStream(TikaContentDetectorTest.class.getClassLoader().getResourceAsStream("test.png")).build());
+ });
+
+ assertNotNull(exception);
+ }
+
+ private void write(ContentDetectorContext ctx) {
+ try (var is = ctx.getInputStream()) {
+ IOUtils.copy(is, new FileOutputStream("build/detector" + ctx.getExtension()));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/policy/MimeContentPolicyTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/policy/MimeContentPolicyTest.java
new file mode 100644
index 000000000..ff68039b1
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/policy/MimeContentPolicyTest.java
@@ -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.contentstore.policy;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.TestConfiguration;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+
+@Slf4j
+@SpringBootTest
+@ContextConfiguration(classes = TestConfiguration.class)
+class MimeContentPolicyTest {
+
+ @Autowired
+ private MimeContentPolicy mimeContentPolicy;
+
+ @Test
+ void mimeTypeMismatch() {
+ var path = "tmp/test-gif.png";
+ var mime = "image/png";
+
+ var ctx = ContentPolicyContext.builder().path(path).mimeType(mime)
+ .inputStream(MimeContentPolicyTest.class.getClassLoader().getResourceAsStream("test-gif.png")).build();
+
+ var exception = assertThrows(ContentPolicyException.class, () -> {
+ mimeContentPolicy.check(ctx);
+ });
+
+ assertThat(exception).isNotNull();
+
+ log.info("Error message: {}", exception.getMessage());
+ }
+}
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/policy/TraversalContentPolicyTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/policy/TraversalContentPolicyTest.java
new file mode 100644
index 000000000..d7064c482
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/policy/TraversalContentPolicyTest.java
@@ -0,0 +1,63 @@
+/**
+ * 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.contentstore.policy;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.TestConfiguration;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+
+@Slf4j
+@SpringBootTest
+@ContextConfiguration(classes = TestConfiguration.class)
+class TraversalContentPolicyTest {
+
+ @Autowired
+ private TraversalContentPolicy traversalContentPolicy;
+
+ @Test
+ void pathTraversal() {
+ var paths = List.of("/tmp/../17/michael.vorburger-crepes.jpg", "../17/michael.vorburger-crepes.jpg",
+ "/tmp/..//17//michael.vorburger-crepes.jpg", "/tmp/..//17/michael.vorburger-crepes.jpg",
+ "..//17/michael.vorburger-crepes.jpg", "../../../../../../../../../../tmp/michael.vorburger-crepes.jpg",
+ "/tmp/../../../../../../../../../../tmp/image-text-wrong-content.jsp",
+ "../../../../../../../../../../tmp/image-text-wrong-content.jsp", "/test/../tmp/image-text-wrong-content.jsp");
+
+ paths.forEach(path -> {
+ log.info("Checking path: {}", path);
+
+ var exception = assertThrows(ContentPolicyException.class, () -> {
+ var ctx = ContentPolicyContext.builder().path(path).mimeType("image/jpeg").build();
+
+ traversalContentPolicy.check(ctx);
+ });
+
+ assertThat(exception).isNotNull();
+
+ assertThat(exception.getMessage()).startsWith("Trying to overwrite a sibling file:");
+ });
+ }
+}
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorTest.java
new file mode 100644
index 000000000..353bfc0c1
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorTest.java
@@ -0,0 +1,170 @@
+/**
+ * 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.contentstore.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlDecoderContentProcessor.DATA_URL_DECODE_RESULT_CONTENT_TYPE;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_CONTENT_TYPE;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_ENCODING;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_FORMAT;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_HEIGHT;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_WIDTH;
+import static org.apache.fineract.infrastructure.contentstore.processor.SizeContentProcessor.SIZE_RESULT_VALUE;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileOutputStream;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+import org.apache.fineract.infrastructure.TestConfiguration;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+
+@Slf4j
+@SpringBootTest
+@ContextConfiguration(classes = TestConfiguration.class)
+class ContentProcessorTest {
+
+ @Autowired
+ private Base64EncoderContentProcessor base64EncoderContentProcessor;
+
+ @Autowired
+ private Base64DecoderContentProcessor base64DecoderContentProcessor;
+
+ @Autowired
+ private DataUrlEncoderContentProcessor dataUrlEncoderContentProcessor;
+
+ @Autowired
+ private DataUrlDecoderContentProcessor dataUrlDecoderContentProcessor;
+
+ @Autowired
+ private ImageResizeContentProcessor imageResizeContentProcessor;
+
+ @Autowired
+ private SizeContentProcessor sizeContentProcessor;
+
+ static final byte[] DATA_BASE64 = """
+ iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
+ bWFnZVJlYWR5ccllPAAAAodJREFUeNrs3NFtg0AQRVGPRRNWyoCeUhM9LWWkjUw6iIQyCRvmnAKQ
+ F3S1/nqRmQ/gbzy9AhAcCA4QHAgOEBwIDgQHCA4EBwgOBAeCAwQHggMEB4IDBAeCg/tbvAJuJaJ2
+ pCcz3HDgLyUgOBAcCA4QHAgOEBwIDhAcCA4EBwgOBAcIDgQHggMEB4IDTmm3aRKxlW5erOva6v2N
+ sUft96jdIMndDQcIDgQHggMEB4IDBAeCA8EBggPBAYIDwQGCA8GB4ADBgeAAwcHFpt806bZBMvvv
+ 27b32g2SzOgUnBsOBAeCAwQHggMEB4IDwQGCA8EBggPBgeAAwYHgAMGB4ADBgeCggcjM2gcWb5BU
+ m30zpNt5j+Mofd4Y+9QbKW44EBwIDhAcCA4QHAgOBAcIDgQHCA4EB4IDBAeCAwQHggMEB4KDBpZu
+ B67e0Oi2GdJtE8YNB4IDBAeCA8EBggPBAYIDwQGCA8GB4ADBgeAAwYHgQHCA4EBwwGmRmbUPjLfa
+ Bz5erT6IzZCfGWMPNxwgOBAcCA4QHAgOEBwIDgQHCA4EBwgOBAeC8wpAcCA4QHAgOEBwcKl/sGlS
+ 7eWr31jmsGkCCA4EB4IDBAeCAwQHggPBAYIDwQGCA8EBggPBgeAAwYHgAMHBtco3Tcp/4PQbKdV6
+ ba7MvkHihgPBAYIDwYHgAMGB4ADBgeAAwYHgQHCA4EBwgOBAcCA4QHAgOOC06TdNyg8cz+IDd9sg
+ +QjZuOFAcIDgQHAgOEBwIDhAcCA4EBwgOBAcIDgQHCA4EBwIDhAcCA74xtLtwJmfNjlww4HgAMGB
+ 4ADBgeBAcIDgQHCA4EBwgOBAcCA4QHAgOEBwIDgQHPB7vgQYAPVcRHV5+lfgAAAAAElFTkSuQmCC""".getBytes(UTF_8);
+
+ private static final byte[] DATA_URL = """
+ data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
+ bWFnZVJlYWR5ccllPAAAAodJREFUeNrs3NFtg0AQRVGPRRNWyoCeUhM9LWWkjUw6iIQyCRvmnAKQ
+ F3S1/nqRmQ/gbzy9AhAcCA4QHAgOEBwIDgQHCA4EBwgOBAeCAwQHggMEB4IDBAeCg/tbvAJuJaJ2
+ pCcz3HDgLyUgOBAcCA4QHAgOEBwIDhAcCA4EBwgOBAcIDgQHggMEB4IDTmm3aRKxlW5erOva6v2N
+ sUft96jdIMndDQcIDgQHggMEB4IDBAeCA8EBggPBAYIDwQGCA8GB4ADBgeAAwcHFpt806bZBMvvv
+ 27b32g2SzOgUnBsOBAeCAwQHggMEB4IDwQGCA8EBggPBgeAAwYHgAMGB4ADBgeCggcjM2gcWb5BU
+ m30zpNt5j+Mofd4Y+9QbKW44EBwIDhAcCA4QHAgOBAcIDgQHCA4EB4IDBAeCAwQHggMEB4KDBpZu
+ B67e0Oi2GdJtE8YNB4IDBAeCA8EBggPBAYIDwQGCA8GB4ADBgeAAwYHgQHCA4EBwwGmRmbUPjLfa
+ Bz5erT6IzZCfGWMPNxwgOBAcCA4QHAgOEBwIDgQHCA4EBwgOBAeC8wpAcCA4QHAgOEBwcKl/sGlS
+ 7eWr31jmsGkCCA4EB4IDBAeCAwQHggPBAYIDwQGCA8EBggPBgeAAwYHgAMHBtco3Tcp/4PQbKdV6
+ ba7MvkHihgPBAYIDwYHgAMGB4ADBgeAAwYHgQHCA4EBwgOBAcCA4QHAgOOC06TdNyg8cz+IDd9sg
+ +QjZuOFAcIDgQHAgOEBwIDhAcCA4EBwgOBAcIDgQHCA4EBwIDhAcCA74xtLtwJmfNjlww4HgAMGB
+ 4ADBgeBAcIDgQHCA4EBwgOBAcCA4QHAgOEBwIDgQHPB7vgQYAPVcRHV5+lfgAAAAAElFTkSuQmCC""".getBytes(UTF_8);
+
+ @Test
+ void process() {
+
+ final var ctx = sizeContentProcessor.then(base64DecoderContentProcessor).process(new ByteArrayInputStream(DATA_BASE64));
+
+ write(ctx, "process.png");
+
+ String type = ctx.getResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE);
+ Long size = ctx.getResult(SIZE_RESULT_VALUE);
+
+ log.info("Result: {} of size {}", type, size);
+ }
+
+ @Test
+ void dataUrlDecode() {
+ final var ctx = dataUrlDecoderContentProcessor.then(base64DecoderContentProcessor).then(imageResizeContentProcessor)
+ .then(sizeContentProcessor).process(new ContentProcessorContext(new ByteArrayInputStream(DATA_URL),
+ Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, 110, IMAGE_RESIZE_PARAM_MAX_HEIGHT, 110, IMAGE_RESIZE_PARAM_FORMAT, "png")));
+
+ write(ctx, "data-url-decode.png");
+
+ String type = ctx.getResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE);
+ Long size = ctx.getResult(SIZE_RESULT_VALUE);
+
+ log.info("Result: {} of size {}", type, size);
+ }
+
+ @Test
+ void dataUrlEncodePng() {
+ final var ctx = imageResizeContentProcessor.then(base64EncoderContentProcessor).then(dataUrlEncoderContentProcessor)
+ .process(new ContentProcessorContext(ContentProcessorTest.class.getClassLoader().getResourceAsStream("test.png"),
+ Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, 110, IMAGE_RESIZE_PARAM_MAX_HEIGHT, 110, IMAGE_RESIZE_PARAM_FORMAT, "png",
+ DATA_URL_ENCODE_PARAM_CONTENT_TYPE, "image/png", DATA_URL_ENCODE_PARAM_ENCODING, "base64")));
+
+ write(ctx, "data-url-encode-png.txt");
+
+ String type = ctx.getResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE);
+ Long size = ctx.getResult(SIZE_RESULT_VALUE);
+
+ log.info("Result: {} of size {}", type, size);
+ }
+
+ @Test
+ void dataUrlEncodeJpeg() {
+ final var ctx = imageResizeContentProcessor.then(base64EncoderContentProcessor).then(dataUrlEncoderContentProcessor)
+ .process(new ContentProcessorContext(ContentProcessorTest.class.getClassLoader().getResourceAsStream("test-large.jpg"),
+ Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, 300, IMAGE_RESIZE_PARAM_MAX_HEIGHT, 300, IMAGE_RESIZE_PARAM_FORMAT, "jpg",
+ DATA_URL_ENCODE_PARAM_CONTENT_TYPE, "image/png", DATA_URL_ENCODE_PARAM_ENCODING, "base64")));
+
+ write(ctx, "data-url-encode-jpg.txt");
+
+ String type = ctx.getResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE);
+ Long size = ctx.getResult(SIZE_RESULT_VALUE);
+
+ log.info("Result: {} of size {}", type, size);
+ }
+
+ @Test
+ void base64() {
+ final var ctx = base64DecoderContentProcessor.then(sizeContentProcessor).process(new ByteArrayInputStream(DATA_BASE64));
+
+ write(ctx, "base64.png");
+
+ String type = ctx.getResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE);
+ Long size = ctx.getResult(SIZE_RESULT_VALUE);
+
+ log.info("Result: {} of size {}", type, size);
+ }
+
+ private void write(ContentProcessorContext ctx, String fileName) {
+ try (var is = ctx.getInputStream()) {
+ IOUtils.copy(is, new FileOutputStream("build/" + fileName));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/service/FileContentStoreServiceTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/service/FileContentStoreServiceTest.java
new file mode 100644
index 000000000..2c543d08b
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/service/FileContentStoreServiceTest.java
@@ -0,0 +1,85 @@
+/**
+ * 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.contentstore.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.TestConfiguration;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+
+@Slf4j
+@SpringBootTest
+@ContextConfiguration(classes = TestConfiguration.class)
+class FileContentStoreServiceTest {
+
+ @Autowired
+ private FileContentStoreService fileContentStoreService;
+
+ @Autowired
+ private FineractProperties properties;
+
+ @Test
+ void upload() {
+ setTenant();
+
+ var path = fileContentStoreService.upload("some/path/test.jpg",
+ FileContentStoreServiceTest.class.getClassLoader().getResourceAsStream("test.jpg"), "image/jpeg");
+
+ var rootFolder = getRootFolder();
+
+ log.info("Path: {} ({})", path, rootFolder);
+
+ var p = rootFolder.resolve(path);
+
+ log.info("Complete path: {}", p);
+
+ assertTrue(Files.exists(p));
+ }
+
+ @Test
+ void relativePath() {
+ var rootPath = Path.of("/tmp/fineract/DefaultDemoTenant");
+ var fullPath = Path.of("/tmp/fineract/DefaultDemoTenant/images/staff/1/PwVhJDLHyxUppiby/michael.vorburger-crepes.jpg");
+ var relativePath = rootPath.relativize(fullPath);
+
+ assertEquals(Path.of("images/staff/1/PwVhJDLHyxUppiby/michael.vorburger-crepes.jpg"), relativePath);
+
+ log.info("Relative path: {}", relativePath);
+ }
+
+ private Path getRootFolder() {
+ return Path.of(properties.getContent().getFilesystem().getRootFolder(),
+ ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim());
+ }
+
+ private void setTenant() {
+ ThreadLocalContextUtil
+ .setTenant(FineractPlatformTenant.builder().id(1L).name("Test Tenant").tenantIdentifier("test-tenant").build());
+ }
+}
diff --git a/fineract-document/src/test/resources/application-test.properties b/fineract-document/src/test/resources/application-test.properties
new file mode 100644
index 000000000..075fd5a53
--- /dev/null
+++ b/fineract-document/src/test/resources/application-test.properties
@@ -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.
+#
+
+debug=false
+
+fineract.content.regex-whitelist-enabled=true
+fineract.content.regex-whitelist=.*\\.pdf$,.*\\.doc,.*\\.docx,.*\\.xls,.*\\.xlsx,.*\\.jpg,.*\\.jpeg,.*\\.png
+fineract.content.mime-whitelist-enabled=true
+fineract.content.mime-whitelist=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,image/jpeg,image/png
+
+fineract.content.filesystem.enabled=true
+fineract.content.filesystem.rootFolder=build/.fineract
+
+fineract.content.s3.enabled=false
+fineract.content.s3.bucketName=
+fineract.content.s3.accessKey=
+fineract.content.s3.secretKey=
+fineract.content.s3.region=
+fineract.content.s3.endpoint=
+fineract.content.s3.path-style-addressing-enabled=false
diff --git a/fineract-document/src/test/resources/test-gif.png b/fineract-document/src/test/resources/test-gif.png
new file mode 100644
index 000000000..89b951662
Binary files /dev/null and b/fineract-document/src/test/resources/test-gif.png differ
diff --git a/fineract-document/src/test/resources/test-large.jpg b/fineract-document/src/test/resources/test-large.jpg
new file mode 100644
index 000000000..c5f4bb6c5
Binary files /dev/null and b/fineract-document/src/test/resources/test-large.jpg differ
diff --git a/fineract-document/src/test/resources/test.jpg b/fineract-document/src/test/resources/test.jpg
new file mode 100644
index 000000000..1c61cde77
Binary files /dev/null and b/fineract-document/src/test/resources/test.jpg differ
diff --git a/fineract-document/src/test/resources/test.png b/fineract-document/src/test/resources/test.png
new file mode 100644
index 000000000..227c547ac
Binary files /dev/null and b/fineract-document/src/test/resources/test.png differ
diff --git a/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml b/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml
index 3daf8ed78..7c1cef3bc 100644
--- a/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml
+++ b/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml b/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml
index edc5b8c27..61abb20e2 100644
--- a/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml
+++ b/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml b/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml
index 33d37d489..f3d4b2ed2 100644
--- a/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml
+++ b/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/api/BulkImportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/api/BulkImportApiResource.java
index d0c5bebff..35c474410 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/api/BulkImportApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/api/BulkImportApiResource.java
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.bulkimport.api;
+import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_ATTACHMENT;
+
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
@@ -30,6 +32,7 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
import org.apache.fineract.infrastructure.bulkimport.data.ImportData;
@@ -39,7 +42,9 @@ import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
-import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
+import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
+import org.apache.fineract.util.StreamResponseUtil;
import org.springframework.stereotype.Component;
@Path("/v1/imports")
@@ -48,10 +53,8 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class BulkImportApiResource {
- private static final String RESOURCE_NAME_FOR_PERMISSION = "IMPORT";
-
- private final PlatformSecurityContext context;
private final BulkImportWorkbookService bulkImportWorkbookService;
+ private final DocumentReadPlatformService documentReadPlatformService;
private final DefaultToApiJsonSerializer toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
@@ -59,23 +62,26 @@ public class BulkImportApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveImportDocuments(@Context final UriInfo uriInfo, @QueryParam("entityType") final String entityType) {
-
- this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
Collection importData = new ArrayList<>();
+
if (entityType.equals(GlobalEntityType.CLIENT.getCode())) {
- final Collection importForClientEntity = this.bulkImportWorkbookService.getImports(GlobalEntityType.CLIENTS_ENTITY);
- final Collection importForClientPerson = this.bulkImportWorkbookService.getImports(GlobalEntityType.CLIENTS_PERSON);
+ final var importForClientEntity = this.bulkImportWorkbookService.getImports(GlobalEntityType.CLIENTS_ENTITY);
+ final var importForClientPerson = this.bulkImportWorkbookService.getImports(GlobalEntityType.CLIENTS_PERSON);
+
if (importForClientEntity != null) {
importData.addAll(importForClientEntity);
}
+
if (importForClientPerson != null) {
importData.addAll(importForClientPerson);
}
} else {
final GlobalEntityType type = GlobalEntityType.fromCode(entityType);
+
if (type == null) {
throw new ImportTypeNotFoundException(entityType);
}
+
importData = this.bulkImportWorkbookService.getImports(type);
}
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
@@ -84,17 +90,28 @@ public class BulkImportApiResource {
@GET
@Path("getOutputTemplateLocation")
- public String retriveOutputTemplateLocation(@QueryParam("importDocumentId") final String importDocumentId) {
- this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
- final DocumentData documentData = this.bulkImportWorkbookService.getOutputTemplateLocation(importDocumentId);
- return this.toApiJsonSerializer.serialize(documentData.getLocation());
+ public String retriveOutputTemplateLocation(@QueryParam("importDocumentId") final Long importDocumentId) {
+ final var imporData = bulkImportWorkbookService.getImport(importDocumentId);
+
+ return Optional.ofNullable(imporData)
+ .flatMap(importData -> Optional.ofNullable(documentReadPlatformService.retrieveDocument(imporData.getDocumentId())))
+ .map(DocumentData::getLocation).orElse(null);
}
@GET
@Path("downloadOutputTemplate")
@Produces("application/vnd.ms-excel")
- public Response getOutputTemplate(@QueryParam("importDocumentId") final String importDocumentId) {
- return bulkImportWorkbookService.getOutputTemplate(importDocumentId);
- }
+ public Response getOutputTemplate(@QueryParam("importDocumentId") final Long importDocumentId) {
+ // TODO: can we avoid 2 calls by introducing a new function? clean code more important now
+ final var docs = documentReadPlatformService.retrieveAllDocuments("IMPORT", importDocumentId);
+ final var docData = docs.stream().findFirst();
+
+ final var d = docData.orElseThrow(() -> new DocumentNotFoundException("IMPORT", importDocumentId, -1L));
+ final var content = documentReadPlatformService.retrieveDocumentContent(d.getParentEntityType(), d.getParentEntityId(), d.getId());
+ final var streamResponseData = StreamResponseUtil.StreamResponseData.builder().type(content.getContentType())
+ .fileName(content.getFileName()).stream(content.getStream()).dispositionType(DISPOSITION_TYPE_ATTACHMENT).build();
+
+ return StreamResponseUtil.ok(streamResponseData);
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/BulkImportEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/BulkImportEvent.java
index 95f7e9c0c..1d115148d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/BulkImportEvent.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/data/BulkImportEvent.java
@@ -18,48 +18,38 @@
*/
package org.apache.fineract.infrastructure.bulkimport.data;
+import lombok.Getter;
+import org.apache.fineract.infrastructure.bulkimport.domain.ImportDocument;
import org.apache.fineract.infrastructure.core.domain.FineractContext;
import org.apache.fineract.infrastructure.core.domain.FineractEvent;
import org.apache.poi.ss.usermodel.Workbook;
+@Getter
public final class BulkImportEvent extends FineractEvent {
private final Workbook workbook;
- private final Long importId;
+ private final ImportDocument importDocument;
+
+ private final String fileName;
+
+ private final String fileType;
private final String locale;
private final String dateFormat;
- private BulkImportEvent(final Object source, final Workbook workbook, final Long importId, final String locale, final String dateFormat,
- FineractContext context) {
+ private final Long entityId;
+
+ public BulkImportEvent(final Object source, final Workbook workbook, final String fileName, String fileType,
+ final ImportDocument importDocument, final String locale, final String dateFormat, FineractContext context, Long entityId) {
super(source, context);
this.workbook = workbook;
- this.importId = importId;
+ this.fileName = fileName;
+ this.fileType = fileType;
+ this.importDocument = importDocument;
this.locale = locale;
this.dateFormat = dateFormat;
+ this.entityId = entityId;
}
-
- public static BulkImportEvent instance(final Object source, final Workbook workbook, final Long importId, final String locale,
- final String dateFormat, FineractContext context) {
- return new BulkImportEvent(source, workbook, importId, locale, dateFormat, context);
- }
-
- public Workbook getWorkbook() {
- return workbook;
- }
-
- public Long getImportId() {
- return importId;
- }
-
- public String getDateFormat() {
- return dateFormat;
- }
-
- public String getLocale() {
- return locale;
- }
-
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/domain/ImportDocument.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/domain/ImportDocument.java
index 259aac1c3..48e75cd00 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/domain/ImportDocument.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/domain/ImportDocument.java
@@ -22,21 +22,26 @@ import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
-import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
import org.apache.fineract.useradministration.domain.AppUser;
@Entity
@Table(name = "m_import_document")
-public class ImportDocument extends AbstractPersistableCustom {
+@Getter
+@Setter
+@NoArgsConstructor
+@Accessors(chain = true)
+public final class ImportDocument extends AbstractPersistableCustom {
- @OneToOne
- @JoinColumn(name = "document_id")
- private Document document;
+ @Column(name = "document_id")
+ private Long documentId;
@Column(name = "import_time")
private LocalDateTime importTime;
@@ -63,11 +68,7 @@ public class ImportDocument extends AbstractPersistableCustom {
@Column(name = "failure_count", nullable = true)
private Integer failureCount;
- protected ImportDocument() {
-
- }
-
- public static ImportDocument instance(final Document document, final LocalDateTime importTime, final Integer entityType,
+ public static ImportDocument instance(final Long documentId, final LocalDateTime importTime, final Integer entityType,
final AppUser createdBy, final Integer totalRecords) {
final Boolean completed = Boolean.FALSE;
@@ -75,14 +76,14 @@ public class ImportDocument extends AbstractPersistableCustom {
final Integer failureCount = 0;
final LocalDateTime endTime = DateUtils.getLocalDateTimeOfTenant();
- return new ImportDocument(document, importTime, endTime, completed, entityType, createdBy, totalRecords, successCount,
+ return new ImportDocument(documentId, importTime, endTime, completed, entityType, createdBy, totalRecords, successCount,
failureCount);
}
- private ImportDocument(final Document document, final LocalDateTime importTime, final LocalDateTime endTime, Boolean completed,
+ private ImportDocument(final Long documentId, final LocalDateTime importTime, final LocalDateTime endTime, Boolean completed,
final Integer entityType, final AppUser createdBy, final Integer totalRecords, final Integer successCount,
final Integer failureCount) {
- this.document = document;
+ this.documentId = documentId;
this.importTime = importTime;
this.endTime = endTime;
this.completed = completed;
@@ -100,13 +101,4 @@ public class ImportDocument extends AbstractPersistableCustom {
this.successCount = successCount;
this.failureCount = errorCount;
}
-
- public Document getDocument() {
- return this.document;
- }
-
- public Integer getEntityType() {
- return this.entityType;
- }
-
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/mapping/ImportDocumentMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/mapping/ImportDocumentMapper.java
new file mode 100644
index 000000000..6c79d076e
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/mapping/ImportDocumentMapper.java
@@ -0,0 +1,44 @@
+/**
+ * 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.bulkimport.mapping;
+
+import static org.mapstruct.InjectionStrategy.CONSTRUCTOR;
+import static org.mapstruct.MappingConstants.ComponentModel.SPRING;
+
+import org.apache.fineract.infrastructure.bulkimport.data.ImportData;
+import org.apache.fineract.infrastructure.bulkimport.domain.ImportDocument;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(componentModel = SPRING, injectionStrategy = CONSTRUCTOR)
+public interface ImportDocumentMapper {
+
+ @Mapping(source = "id", target = "importId")
+ @Mapping(source = "documentId", target = "documentId")
+ @Mapping(ignore = true, target = "name")
+ @Mapping(source = "entityType", target = "entityType")
+ @Mapping(source = "importTime", target = "importTime")
+ @Mapping(source = "completed", target = "completed")
+ @Mapping(source = "endTime", target = "endTime")
+ @Mapping(source = "createdBy.id", target = "createdBy")
+ @Mapping(source = "failureCount", target = "failureCount")
+ @Mapping(source = "successCount", target = "successCount")
+ @Mapping(source = "totalRecords", target = "totalRecords")
+ ImportData map(ImportDocument source);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportEventListener.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportEventListener.java
index 2597c7f78..b289b2a60 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportEventListener.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportEventListener.java
@@ -18,27 +18,21 @@
*/
package org.apache.fineract.infrastructure.bulkimport.service;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.net.URLConnection;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.infrastructure.bulkimport.data.BulkImportEvent;
-import org.apache.fineract.infrastructure.bulkimport.data.Count;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
-import org.apache.fineract.infrastructure.bulkimport.domain.ImportDocument;
import org.apache.fineract.infrastructure.bulkimport.domain.ImportDocumentRepository;
import org.apache.fineract.infrastructure.bulkimport.importhandler.ImportHandler;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
-import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
-import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
-import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.fineract.infrastructure.documentmanagement.command.DocumentUpdateCommand;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service;
@@ -50,14 +44,15 @@ public class BulkImportEventListener implements ApplicationListener this.applicationContext.getBean("officeImportHandler", ImportHandler.class);
@@ -85,38 +80,28 @@ public class BulkImportEventListener implements ApplicationListener modifiedParams = new HashSet<>();
- modifiedParams.add("fileName");
- modifiedParams.add("size");
- modifiedParams.add("type");
- modifiedParams.add("location");
- Document document = importDocument.getDocument();
+ event.getImportDocument().update(DateUtils.getLocalDateTimeOfTenant(), count.getSuccessCount(), count.getErrorCount());
- DocumentCommand documentCommand = new DocumentCommand(modifiedParams, document.getId(), entityType.name(), null,
- document.getName(), document.getFileName(), document.getSize(),
- URLConnection.guessContentTypeFromName(document.getFileName()), null, null);
+ final var pipedInputStream = pipe.pipe(output -> {
+ event.getWorkbook().write(output);
+ });
- final ByteArrayOutputStream bos = new ByteArrayOutputStream();
- try {
- try {
- workbook.write(bos);
- } finally {
- bos.close();
- }
- } catch (IOException io) {
- log.error("Problem occurred in onApplicationEvent function", io);
- }
- byte[] bytes = bos.toByteArray();
- ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
- this.documentService.updateDocument(documentCommand, bis);
+ final var command = new DocumentUpdateCommand();
+
+ command.setPayload(DocumentUpdateRequest.builder().id(event.getImportDocument().getDocumentId()).entityId(event.getEntityId())
+ .entityType("IMPORT").stream(pipedInputStream).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ response.get();
+
+ importRepository.saveAndFlush(event.getImportDocument());
+ } catch (Exception e) {
+ log.error("Bulk import error:", e);
} finally {
ThreadLocalContextUtil.reset();
}
}
-
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
index 7aea2ed0f..78cf8d35c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
@@ -18,70 +18,58 @@
*/
package org.apache.fineract.infrastructure.bulkimport.service;
-import jakarta.ws.rs.core.Response;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.net.URLConnection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.Collection;
+import java.util.function.Supplier;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
+import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.infrastructure.bulkimport.data.BulkImportEvent;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
import org.apache.fineract.infrastructure.bulkimport.data.ImportData;
import org.apache.fineract.infrastructure.bulkimport.domain.ImportDocument;
import org.apache.fineract.infrastructure.bulkimport.domain.ImportDocumentRepository;
import org.apache.fineract.infrastructure.bulkimport.importhandler.ImportHandlerUtils;
+import org.apache.fineract.infrastructure.bulkimport.mapping.ImportDocumentMapper;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
-import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
-import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
-import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
-import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformServiceJpaRepositoryImpl;
+import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCreateCommand;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.tika.Tika;
import org.apache.tika.io.TikaInputStream;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
+@Slf4j
+@RequiredArgsConstructor
@Service
public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService {
- private static final Logger LOG = LoggerFactory.getLogger(BulkImportWorkbookServiceImpl.class);
private final ApplicationContext applicationContext;
private final PlatformSecurityContext securityContext;
- private final DocumentWritePlatformService documentWritePlatformService;
- private final DocumentRepository documentRepository;
private final ImportDocumentRepository importDocumentRepository;
private final JdbcTemplate jdbcTemplate;
-
- @Autowired
- public BulkImportWorkbookServiceImpl(final ApplicationContext applicationContext, final PlatformSecurityContext securityContext,
- final DocumentWritePlatformService documentWritePlatformService, final DocumentRepository documentRepository,
- final ImportDocumentRepository importDocumentRepository, final JdbcTemplate jdbcTemplate) {
- this.applicationContext = applicationContext;
- this.securityContext = securityContext;
- this.documentWritePlatformService = documentWritePlatformService;
- this.documentRepository = documentRepository;
- this.importDocumentRepository = importDocumentRepository;
- this.jdbcTemplate = jdbcTemplate;
- }
+ private final ContentPipe pipe;
+ private final CommandPipeline commandPipeline;
+ private final ImportDocumentMapper mapper;
@Override
public Long importWorkbook(String entity, InputStream inputStream, FormDataContentDisposition fileDetail, final String locale,
@@ -169,35 +157,56 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
throw new GeneralPlatformDomainRuleException("error.msg.unable.to.find.resource", "Unable to find requested resource");
}
- return publishEvent(primaryColumn, fileDetail, bis, entityType, workbook, locale, dateFormat);
+ return publishEvent(primaryColumn, fileDetail, fileType, entityType, workbook, locale, dateFormat);
}
throw new GeneralPlatformDomainRuleException("error.msg.null", "One or more of the given parameters not found");
} catch (IOException e) {
- LOG.error("Problem occurred in importWorkbook function", e);
+ log.error("Problem occurred in importWorkbook function", e);
throw new GeneralPlatformDomainRuleException("error.msg.io.exception",
"IO exception occured with " + fileDetail.getFileName() + " " + e.getMessage(), e);
}
}
- private Long publishEvent(final Integer primaryColumn, final FormDataContentDisposition fileDetail,
- final InputStream clonedInputStreamWorkbook, final GlobalEntityType entityType, final Workbook workbook, final String locale,
- final String dateFormat) {
+ private DocumentCreateResponse createDocument(final Workbook workbook, final FormDataContentDisposition fileDetail,
+ final String fileType) {
+ final var pipedInputStream = pipe.pipe(output -> {
+ workbook.write(output);
+ });
- final String fileName = fileDetail.getFileName();
+ final var command = new DocumentCreateCommand();
- final Long documentId = this.documentWritePlatformService.createInternalDocument(
- DocumentWritePlatformServiceJpaRepositoryImpl.DocumentManagementEntity.IMPORT.name(),
- this.securityContext.authenticatedUser().getId(), null, clonedInputStreamWorkbook,
- URLConnection.guessContentTypeFromName(fileName), fileName, null, fileName);
- final Document document = this.documentRepository.findById(documentId).orElse(null);
+ // TODO: does it really make sense to use the current user's ID?
+ // TODO: wouldn't it be better to use "entityType.name()" as entity name?
+ command.setPayload(DocumentCreateRequest.builder().entityId(this.securityContext.authenticatedUser().getId()).entityType("IMPORT")
+ .name(fileDetail.getFileName()).description(fileDetail.getFileName()).fileName(fileDetail.getFileName())
+ .size(fileDetail.getSize()).type(fileType).stream(pipedInputStream).build());
+
+ final Supplier response = commandPipeline.send(command);
+ return response.get();
+ }
+
+ private Long publishEvent(final Integer primaryColumn, final FormDataContentDisposition fileDetail, final String fileType,
+ final GlobalEntityType entityType, final Workbook workbook, final String locale, final String dateFormat) {
+
+ final var importDocument = ImportDocument.instance(null, DateUtils.getLocalDateTimeOfTenant(), entityType.getValue(),
+ securityContext.authenticatedUser(), ImportHandlerUtils.getNumberOfRows(workbook.getSheetAt(0), primaryColumn));
+
+ final var response = createDocument(workbook, fileDetail, fileType);
+
+ importDocument.setDocumentId(response.getResourceId());
+
+ // TODO: should we mark as "not yet processed by import handler"?
+ importDocumentRepository.saveAndFlush(importDocument);
+
+ final var event = new BulkImportEvent(this, workbook, fileDetail.getFileName(), fileType, importDocument, locale, dateFormat,
+ ThreadLocalContextUtil.getContext(), this.securityContext.authenticatedUser().getId());
- final ImportDocument importDocument = ImportDocument.instance(document, DateUtils.getLocalDateTimeOfTenant(), entityType.getValue(),
- this.securityContext.authenticatedUser(), ImportHandlerUtils.getNumberOfRows(workbook.getSheetAt(0), primaryColumn));
- this.importDocumentRepository.saveAndFlush(importDocument);
- BulkImportEvent event = BulkImportEvent.instance(this, workbook, importDocument.getId(), locale, dateFormat,
- ThreadLocalContextUtil.getContext());
applicationContext.publishEvent(event);
+
+ // TODO: remove when done
+ log.warn("Import document ID: {} ({})", importDocument.getId(), importDocument.getDocumentId());
+
return importDocument.getId();
}
@@ -208,7 +217,12 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
final ImportMapper rm = new ImportMapper();
final String sql = "select " + rm.schema() + " order by i.id desc";
- return this.jdbcTemplate.query(sql, rm, new Object[] { type.getValue() }); // NOSONAR
+ return this.jdbcTemplate.query(sql, rm, type.getValue());
+ }
+
+ @Override
+ public ImportData getImport(Long id) {
+ return importDocumentRepository.findById(id).map(mapper::map).orElse(null);
}
private static final class ImportMapper implements RowMapper {
@@ -240,50 +254,4 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
failureCount);
}
}
-
- @Override
- public DocumentData getOutputTemplateLocation(String importDocumentId) {
- this.securityContext.authenticatedUser();
- final ImportTemplateLocationMapper importTemplateLocationMapper = new ImportTemplateLocationMapper();
- final String sql = "select " + importTemplateLocationMapper.schema();
-
- return this.jdbcTemplate.queryForObject(sql, importTemplateLocationMapper, new Object[] { Long.parseLong(importDocumentId) }); // NOSONAR
- }
-
- @Override
- public Response getOutputTemplate(String importDocumentId) {
- this.securityContext.authenticatedUser();
- final ImportTemplateLocationMapper importTemplateLocationMapper = new ImportTemplateLocationMapper();
- final String sql = "select " + importTemplateLocationMapper.schema();
- DocumentData documentData = this.jdbcTemplate.queryForObject(sql, importTemplateLocationMapper,
- new Object[] { Long.parseLong(importDocumentId) }); // NOSONAR
- return buildResponse(documentData);
- }
-
- private Response buildResponse(DocumentData documentData) {
- String fileName = "Output" + documentData.getFileName();
- String fileLocation = documentData.getLocation();
- File file = new File(fileLocation);
- final Response.ResponseBuilder response = Response.ok(file);
- response.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
- response.header("Content-Type", "application/vnd.ms-excel");
- return response.build();
- }
-
- private static final class ImportTemplateLocationMapper implements RowMapper {
-
- public String schema() {
- final StringBuilder sql = new StringBuilder();
- sql.append("d.location,d.file_name ").append("from m_import_document i inner join m_document d on i.document_id=d.id ")
- .append("where i.id= ? ");
- return sql.toString();
- }
-
- @Override
- public DocumentData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
- final String location = rs.getString("location");
- final String fileName = rs.getString("file_name");
- return new DocumentData(null, null, null, null, fileName, null, null, location, null, null);
- }
- }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java
index f510cef42..2577f396d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java
@@ -204,6 +204,92 @@ public class SecurityConfig {
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_GROUPNOTE")
+ // document: clients
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/clients/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/clients/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/clients/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_DOCUMENT")
+ // document: client_identifiers
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/client_identifiers/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/client_identifiers/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/client_identifiers/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/client_identifiers/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_DOCUMENT")
+ // document: staff
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/staff/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/staff/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/staff/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/staff/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_DOCUMENT")
+ // document: loans
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/loans/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/loans/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/loans/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/loans/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_DOCUMENT")
+ // document: savings
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/savings/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/savings/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/savings/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/savings/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_DOCUMENT")
+ // document: groups
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/groups/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/groups/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/groups/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_DOCUMENT")
+ // document: import
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/import/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/import/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/import/*/documents"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_DOCUMENT")
+ // image: clients
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/clients/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_CLIENTIMAGE")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/clients/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CLIENTIMAGE")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/clients/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_CLIENTIMAGE")
+ // image: staff
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/staff/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_DOCUMENT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/staff/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_STAFFIMAGE")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/staff/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_STAFFIMAGE")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/staff/*/images"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_STAFFIMAGE")
+ // bulk import
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_IMPORT")
+ .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import/*"))
+ .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_IMPORT")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated()
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java
index 50bdb5975..0bb82fa95 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java
@@ -55,7 +55,7 @@ public class JerseyConfig extends ResourceConfig {
@PostConstruct
public void setup() {
- appCtx.getBeansWithAnnotation(Path.class).values().forEach(component -> register(component.getClass()));
+ appCtx.getBeansWithAnnotation(Path.class).values().forEach(this::register);
appCtx.getBeansWithAnnotation(Provider.class).values().forEach(this::register);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
deleted file mode 100644
index e164e5ef0..000000000
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * 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 org.apache.fineract.infrastructure.core.domain.JdbcSupport;
-import org.apache.fineract.infrastructure.documentmanagement.api.ImagesApiResource.EntityTypeForImages;
-import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepository;
-import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory;
-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.organisation.staff.domain.Staff;
-import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
-import org.apache.fineract.portfolio.client.domain.Client;
-import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
-import org.apache.fineract.portfolio.client.exception.ImageNotFoundException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.dao.EmptyResultDataAccessException;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.jdbc.core.RowMapper;
-import org.springframework.stereotype.Service;
-
-@Service
-public class ImageReadPlatformServiceImpl implements ImageReadPlatformService {
-
- private final JdbcTemplate jdbcTemplate;
- private final ContentRepositoryFactory contentRepositoryFactory;
- private final ClientRepositoryWrapper clientRepositoryWrapper;
- private final StaffRepositoryWrapper staffRepositoryWrapper;
-
- @Autowired
- public ImageReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, final ContentRepositoryFactory documentStoreFactory,
- final ClientRepositoryWrapper clientRepositoryWrapper, StaffRepositoryWrapper staffRepositoryWrapper) {
- this.staffRepositoryWrapper = staffRepositoryWrapper;
- this.jdbcTemplate = jdbcTemplate;
- this.contentRepositoryFactory = documentStoreFactory;
- this.clientRepositoryWrapper = clientRepositoryWrapper;
- }
-
- private static final class ImageMapper implements RowMapper {
-
- private final String entityDisplayName;
-
- ImageMapper(final String entityDisplayName) {
- this.entityDisplayName = entityDisplayName;
- }
-
- public String schema(String entityType) {
- StringBuilder builder = new StringBuilder(
- "image.id as id, image.location as location, image.storage_type_enum as storageType ");
- if (EntityTypeForImages.CLIENTS.toString().equalsIgnoreCase(entityType)) {
- builder.append(" from m_image image , m_client client " + " where client.image_id = image.id and client.id=?");
- } else if (EntityTypeForImages.STAFF.toString().equalsIgnoreCase(entityType)) {
- builder.append("from m_image image , m_staff staff " + " where staff.image_id = image.id and staff.id=?");
- }
- return builder.toString();
- }
-
- @Override
- public ImageData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
- final String location = rs.getString("location");
- final Integer storageTypeInt = JdbcSupport.getInteger(rs, "storageType");
- StorageType storageType = storageTypeInt != null ? StorageType.fromInt(storageTypeInt) : null;
- return new ImageData(location, storageType, this.entityDisplayName);
- }
- }
-
- @Override
- public FileData retrieveImage(String entityType, final Long entityId) {
- try {
- String displayName;
- if (EntityTypeForImages.CLIENTS.toString().equalsIgnoreCase(entityType)) {
- Client owner = this.clientRepositoryWrapper.findOneWithNotFoundDetection(entityId);
- displayName = owner.getDisplayName();
- } else if (EntityTypeForImages.STAFF.toString().equalsIgnoreCase(entityType)) {
- Staff owner = this.staffRepositoryWrapper.findOneWithNotFoundDetection(entityId);
- displayName = owner.displayName();
- } else {
- displayName = "UnknownEntityType:" + entityType;
- }
- final ImageMapper imageMapper = new ImageMapper(displayName);
-
- final String sql = "select " + imageMapper.schema(entityType);
-
- final ImageData imageData = this.jdbcTemplate.queryForObject(sql, imageMapper, entityId); // NOSONAR
- final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(imageData.storageType());
- return contentRepository.fetchImage(imageData);
- } catch (final EmptyResultDataAccessException e) {
- throw new ImageNotFoundException("clients", entityId, e);
- }
- }
-}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java
deleted file mode 100644
index 09f75bcac..000000000
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/**
- * 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.apache.fineract.infrastructure.documentmanagement.api.ImagesApiResource.EntityTypeForImages;
-import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepository;
-import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
-import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
-import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
-import org.apache.fineract.organisation.staff.domain.Staff;
-import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
-import org.apache.fineract.portfolio.client.domain.Client;
-import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-@Service
-public class ImageWritePlatformServiceJpaRepositoryImpl implements ImageWritePlatformService {
-
- private final ContentRepositoryFactory contentRepositoryFactory;
- private final ClientRepositoryWrapper clientRepositoryWrapper;
- private final ImageRepository imageRepository;
- private final StaffRepositoryWrapper staffRepositoryWrapper;
-
- @Autowired
- public ImageWritePlatformServiceJpaRepositoryImpl(final ContentRepositoryFactory documentStoreFactory,
- final ClientRepositoryWrapper clientRepositoryWrapper, final ImageRepository imageRepository,
- StaffRepositoryWrapper staffRepositoryWrapper) {
- this.contentRepositoryFactory = documentStoreFactory;
- this.clientRepositoryWrapper = clientRepositoryWrapper;
- this.imageRepository = imageRepository;
- this.staffRepositoryWrapper = staffRepositoryWrapper;
- }
-
- @Transactional
- @Override
- public CommandProcessingResult saveOrUpdateImage(String entityName, final Long clientId, final String imageName,
- final InputStream inputStream, final Long fileSize) {
- Object owner = deletePreviousImage(entityName, clientId);
-
- final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository();
- final String imageLocation = contentRepository.saveImage(inputStream, clientId, imageName, fileSize);
- return updateImage(owner, imageLocation, contentRepository.getStorageType());
- }
-
- @Transactional
- @Override
- public CommandProcessingResult saveOrUpdateImage(String entityName, final Long clientId, final Base64EncodedImage encodedImage) {
- Object owner = deletePreviousImage(entityName, clientId);
-
- final ContentRepository contenRepository = this.contentRepositoryFactory.getRepository();
- final String imageLocation = contenRepository.saveImage(encodedImage, clientId, "image");
-
- return updateImage(owner, imageLocation, contenRepository.getStorageType());
- }
-
- @Transactional
- @Override
- public CommandProcessingResult deleteImage(String entityName, final Long clientId) {
- Object owner = null;
- Image image = null;
- if (EntityTypeForImages.CLIENTS.toString().equals(entityName)) {
- owner = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
- Client client = (Client) owner;
- image = client.getImage();
- client.setImage(null);
- this.clientRepositoryWrapper.save(client);
-
- } else if (EntityTypeForImages.STAFF.toString().equals(entityName)) {
- owner = this.staffRepositoryWrapper.findOneWithNotFoundDetection(clientId);
- Staff staff = (Staff) owner;
- image = staff.getImage();
- staff.setImage(null);
- this.staffRepositoryWrapper.save(staff);
-
- }
- // delete image from the file system
- if (image != null) {
- final ContentRepository contentRepository = this.contentRepositoryFactory
- .getRepository(StorageType.fromInt(image.getStorageType()));
- contentRepository.deleteImage(image.getLocation());
- this.imageRepository.delete(image);
- }
-
- return CommandProcessingResult.resourceResult(clientId);
- }
-
- /**
- * @param entityName
- * @param entityId
- * @return
- */
- private Object deletePreviousImage(String entityName, final Long entityId) {
- Object owner = null;
- Image image = null;
- if (EntityTypeForImages.CLIENTS.toString().equals(entityName)) {
- Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(entityId);
- image = client.getImage();
- owner = client;
- } else if (EntityTypeForImages.STAFF.toString().equals(entityName)) {
- Staff staff = this.staffRepositoryWrapper.findOneWithNotFoundDetection(entityId);
- image = staff.getImage();
- owner = staff;
- }
- if (image != null) {
- final ContentRepository contentRepository = this.contentRepositoryFactory
- .getRepository(StorageType.fromInt(image.getStorageType()));
- contentRepository.deleteImage(image.getLocation());
- }
- return owner;
- }
-
- private CommandProcessingResult updateImage(final Object owner, final String imageLocation, final StorageType storageType) {
- Image image = null;
- Long clientId = null;
- if (owner instanceof Client client) {
- image = client.getImage();
- clientId = client.getId();
- image = createImage(image, imageLocation, storageType);
- client.setImage(image);
- this.clientRepositoryWrapper.save(client);
- } else if (owner instanceof Staff staff) {
- image = staff.getImage();
- clientId = staff.getId();
- image = createImage(image, imageLocation, storageType);
- staff.setImage(image);
- this.staffRepositoryWrapper.save(staff);
- }
-
- if (image != null) {
- this.imageRepository.save(image);
- }
- return CommandProcessingResult.resourceResult(clientId);
- }
-
- private Image createImage(Image image, final String imageLocation, final StorageType storageType) {
- if (image == null) {
- image = new Image(imageLocation, storageType);
- } else {
- image.setLocation(imageLocation);
- image.setStorageType(storageType.getValue());
- }
- return image;
- }
-
-}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializer.java
index a2b68d1cf..3326b571b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializer.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializer.java
@@ -19,28 +19,23 @@
package org.apache.fineract.infrastructure.event.external.service.serialization.serializer.document;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.apache.avro.generic.GenericContainer;
import org.apache.fineract.avro.document.v1.DocumentDataV1;
import org.apache.fineract.avro.generator.ByteBufferSerializable;
-import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
-import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentDeletedBusinessEvent;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.document.DocumentDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
+@Slf4j
@Component
@RequiredArgsConstructor
public class DocumentBusinessEventSerializer implements BusinessEventSerializer {
- private static final Logger log = LoggerFactory.getLogger(DocumentBusinessEventSerializer.class);
- private final DocumentReadPlatformService service;
private final DocumentDataMapper mapper;
@Override
@@ -51,35 +46,9 @@ public class DocumentBusinessEventSerializer implements BusinessEventSerializer
@Override
public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) {
- DocumentBusinessEvent event = (DocumentBusinessEvent) rawEvent;
- Document entity = event.get(); // domain entity
+ var event = (DocumentBusinessEvent) rawEvent;
- DocumentData dto = null;
- if (rawEvent instanceof DocumentCreatedBusinessEvent) {
- try {
- dto = service.retrieveDocument(entity.getParentEntityType(), entity.getParentEntityId(), entity.getId());
- } catch (Exception ex) {
- // log at DEBUG and fall back to entity mapping
- log.debug("DocumentData not found, falling back to entity", ex);
- }
- }
-
- // If we have the DTO, let MapStruct do the work. Otherwise, build from the entity
-
- DocumentDataV1 avro;
- if (dto != null) {
- avro = mapper.map(dto);
- } else {
- avro = DocumentDataV1.newBuilder().setId(entity.getId()).setParentEntityType(entity.getParentEntityType())
- .setParentEntityId(entity.getParentEntityId()).setName(entity.getName()).setFileName(entity.getFileName())
- .setSize(entity.getSize()).setType(entity.getType()).setDescription(entity.getDescription()).build();
- }
-
- Integer storageTypeCode = (dto != null && dto.getStorageType() != null && dto.storageType() != null) ? dto.getStorageType()
- : (entity.storageType() != null ? entity.storageType().getValue() : null);
- avro.setStorageType(storageTypeCode);
-
- return avro;
+ return mapper.map(event.get());
}
@Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/adapter/StaffImageIdAdapter.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/adapter/StaffImageIdAdapter.java
new file mode 100644
index 000000000..768c47012
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/adapter/StaffImageIdAdapter.java
@@ -0,0 +1,66 @@
+/**
+ * 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.organisation.staff.adapter;
+
+import static java.util.Objects.nonNull;
+
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
+import org.apache.fineract.organisation.staff.domain.StaffRepository;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+@Deprecated
+class StaffImageIdAdapter implements EntityImageIdAdapter {
+
+ private static final String ENTITY_TYPE = "staff";
+
+ private final StaffRepository repository;
+
+ @Override
+ public boolean accept(String entityType) {
+ return ENTITY_TYPE.equalsIgnoreCase(entityType);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Optional get(Long entityId) {
+ return repository.findById(entityId).filter(staff -> nonNull(staff.getImageId()))
+ .map(staff -> ImageIdResult.builder().id(staff.getImageId()).displayName(staff.getDisplayName()).build());
+ }
+
+ @Override
+ @Transactional
+ public Optional set(Long entityId, Long imageId) {
+ final var result = get(entityId);
+
+ if (imageId == null) {
+ repository.removeImageId(entityId);
+ } else {
+ repository.updateByIdAndImageId(entityId, imageId);
+ }
+
+ return result;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java
index 2f8c20410..3c6b10fdd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java
@@ -20,6 +20,7 @@ package org.apache.fineract.organisation.staff.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -27,10 +28,14 @@ public interface StaffRepository extends JpaRepository, JpaSpecific
String FIND_BY_OFFICE_QUERY = "select s from Staff s where s.id = :id AND s.office.id = :officeId";
- /**
- * Find staff by officeid.
- */
@Query(FIND_BY_OFFICE_QUERY)
Staff findByOffice(@Param("id") Long id, @Param("officeId") Long officeId);
+ @Modifying
+ @Query("UPDATE Staff staff SET staff.imageId = :imageId WHERE staff.id = :staffId")
+ void updateByIdAndImageId(@Param("staffId") Long staffId, @Param("imageId") Long imageId);
+
+ @Modifying
+ @Query("UPDATE Staff staff SET staff.imageId = null WHERE staff.id = :staffId")
+ void removeImageId(@Param("staffId") Long staffId);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java
index 56df58435..f8849cfaf 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java
@@ -34,22 +34,22 @@ import org.mapstruct.Named;
@Mapper(config = MapstructMapperConfig.class)
public interface ClientMapper {
- @Mapping(target = "accountNo", source = "source.accountNumber")
+ @Mapping(target = "accountNo", source = "accountNumber")
@Mapping(target = "status", source = "source", qualifiedByName = "clientStatusEnum")
@Mapping(target = "subStatus", source = "source", qualifiedByName = "clientSubStatusCode")
- @Mapping(target = "officeId", source = "source.office.id")
- @Mapping(target = "officeName", source = "source.office.name")
- @Mapping(target = "transferToOfficeId", source = "source.transferToOffice.id")
- @Mapping(target = "transferToOfficeName", source = "source.transferToOffice.name")
- @Mapping(target = "externalId", source = "source.externalId")
+ @Mapping(target = "officeId", source = "office.id")
+ @Mapping(target = "officeName", source = "office.name")
+ @Mapping(target = "transferToOfficeId", source = "transferToOffice.id")
+ @Mapping(target = "transferToOfficeName", source = "transferToOffice.name")
+ @Mapping(target = "externalId", source = "externalId")
@Mapping(target = "gender", source = "source", qualifiedByName = "clientGenderCode")
- @Mapping(target = "imageId", source = "source.image.id")
- @Mapping(target = "staffId", source = "source.staff.id")
- @Mapping(target = "staffName", source = "source.staff.displayName")
+ @Mapping(target = "imageId", source = "imageId")
+ @Mapping(target = "staffId", source = "staff.id")
+ @Mapping(target = "staffName", source = "staff.displayName")
@Mapping(target = "timeline", source = "source", qualifiedByName = "clientTimelineData")
- @Mapping(target = "savingsProductId", source = "source.savingsProductId")
- @Mapping(target = "savingsProductName", source = "source.id")
- @Mapping(target = "savingsAccountId", source = "source.savingsAccountId")
+ @Mapping(target = "savingsProductId", source = "savingsProductId")
+ @Mapping(target = "savingsProductName", source = "id")
+ @Mapping(target = "savingsAccountId", source = "savingsAccountId")
@Mapping(target = "clientType", source = "source", qualifiedByName = "clientTypeCode")
@Mapping(target = "clientClassification", source = "source", qualifiedByName = "clientClassificationCode")
@Mapping(target = "legalForm", source = "source", qualifiedByName = "clientLegalFormEnum")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/client/api/SelfClientsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/client/api/SelfClientsApiResource.java
index 6bcd28663..9037547bb 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/client/api/SelfClientsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/client/api/SelfClientsApiResource.java
@@ -18,6 +18,16 @@
*/
package org.apache.fineract.portfolio.self.client.api;
+import static java.util.Objects.requireNonNull;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlDecoderContentProcessor.DATA_URL_DECODE_RESULT_CONTENT_TYPE;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_CONTENT_TYPE;
+import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_ENCODING;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_FORMAT;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_HEIGHT;
+import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_WIDTH;
+import static org.apache.fineract.infrastructure.contentstore.processor.SizeContentProcessor.SIZE_RESULT_VALUE;
+import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
+
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
@@ -40,10 +50,28 @@ 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.Map;
+import java.util.UUID;
+import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.command.core.CommandPipeline;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.processor.Base64DecoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.Base64EncoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.ContentProcessorContext;
+import org.apache.fineract.infrastructure.contentstore.processor.DataUrlDecoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.SizeContentProcessor;
import org.apache.fineract.infrastructure.core.data.UploadRequest;
-import org.apache.fineract.infrastructure.documentmanagement.api.ImagesApiResource;
+import org.apache.fineract.infrastructure.documentmanagement.command.ImageCreateCommand;
+import org.apache.fineract.infrastructure.documentmanagement.command.ImageDeleteCommand;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
+import org.apache.fineract.infrastructure.documentmanagement.service.ImageReadPlatformService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.api.ClientApiConstants;
import org.apache.fineract.portfolio.client.api.ClientChargesApiResource;
@@ -54,6 +82,7 @@ import org.apache.fineract.portfolio.self.client.data.SelfClientDataValidator;
import org.apache.fineract.portfolio.self.client.service.AppuserClientMapperReadService;
import org.apache.fineract.portfolio.self.config.SelfServiceModuleIsEnabledCondition;
import org.apache.fineract.useradministration.domain.AppUser;
+import org.apache.fineract.util.StreamResponseUtil;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
@@ -69,11 +98,19 @@ public class SelfClientsApiResource {
private final PlatformSecurityContext context;
private final ClientsApiResource clientApiResource;
- private final ImagesApiResource imagesApiResource;
private final ClientChargesApiResource clientChargesApiResource;
private final ClientTransactionsApiResource clientTransactionsApiResource;
private final AppuserClientMapperReadService appUserClientMapperReadService;
private final SelfClientDataValidator dataValidator;
+ private final ImageReadPlatformService imageReadPlatformService;
+ private final CommandPipeline commandPipeline;
+ private final ImageResizeContentProcessor imageResizeContentProcessor;
+ private final Base64EncoderContentProcessor base64EncoderContentProcessor;
+ private final Base64DecoderContentProcessor base64DecoderContentProcessor;
+ private final DataUrlEncoderContentProcessor dataUrlEncoderContentProcessor;
+ private final DataUrlDecoderContentProcessor dataUrlDecoderContentProcessor;
+ private final SizeContentProcessor sizeContentProcessor;
+ private final ContentDetectorManager contentDetectorManager;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@@ -149,7 +186,20 @@ public class SelfClientsApiResource {
validateAppuserClientsMapping(clientId);
- return this.imagesApiResource.retrieveImage("clients", clientId, maxWidth, maxHeight, output, MediaType.TEXT_PLAIN);
+ final var content = imageReadPlatformService.retrieveImage(ClientApiConstants.clientEntityName, clientId);
+
+ // stream base64 encoded original format
+ final var detectorCtx = contentDetectorManager.detect(ContentDetectorContext.builder().fileName(content.getFileName()).build());
+ final var ctx = imageResizeContentProcessor.then(base64EncoderContentProcessor).then(dataUrlEncoderContentProcessor)
+ .process(new ContentProcessorContext(content.getStream(),
+ Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, maxWidth, IMAGE_RESIZE_PARAM_MAX_HEIGHT, maxHeight, IMAGE_RESIZE_PARAM_FORMAT,
+ detectorCtx.getFormat(), DATA_URL_ENCODE_PARAM_CONTENT_TYPE, detectorCtx.getMimeType(),
+ DATA_URL_ENCODE_PARAM_ENCODING, "base64")));
+
+ final var streamResponseData = StreamResponseUtil.StreamResponseData.builder().fileName(content.getFileName())
+ .type(TEXT_PLAIN_VALUE).stream(ctx.getInputStream()).build();
+
+ return StreamResponseUtil.ok(streamResponseData);
}
@GET
@@ -233,36 +283,63 @@ public class SelfClientsApiResource {
@Produces({ MediaType.APPLICATION_JSON })
@RequestBody(description = "Add new client image", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
- public CommandProcessingResult addNewClientImage(@PathParam("clientId") final Long clientId,
- @HeaderParam("Content-Length") final Long fileSize, @FormDataParam("file") final InputStream inputStream,
- @FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart) {
+ public ImageCreateResponse addNewClientImage(@PathParam("clientId") final Long clientId,
+ @HeaderParam("Content-Length") final Long fileSize, @FormDataParam("file") final InputStream is,
+ @FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart filePart) {
validateAppuserClientsMapping(clientId);
- return this.imagesApiResource.addNewClientImage(ClientApiConstants.clientEntityName, clientId, fileSize, inputStream, fileDetails,
- bodyPart);
+ // TODO: add proper error messages
+ requireNonNull(fileDetails, "");
+ requireNonNull(filePart, "");
+ requireNonNull(is, "");
+
+ final var command = new ImageCreateCommand();
+
+ command.setPayload(ImageCreateRequest.builder().entityId(clientId).entityType(ClientApiConstants.clientEntityName)
+ .fileName(fileDetails.getFileName()).size(fileSize).type(filePart.getMediaType().toString()).stream(is).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ return response.get();
}
@POST
@Path("{clientId}/images")
@Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- public CommandProcessingResult addNewClientImage(@PathParam("entity") final String entityName,
- @PathParam("clientId") final Long clientId, final String jsonRequestBody) {
+ public ImageCreateResponse addNewClientImage(@PathParam("clientId") final Long clientId, final InputStream body) {
validateAppuserClientsMapping(clientId);
- return this.imagesApiResource.addNewClientImage(ClientApiConstants.clientEntityName, clientId, jsonRequestBody);
+ final var ctx = dataUrlDecoderContentProcessor.then(base64DecoderContentProcessor).then(sizeContentProcessor).process(body);
+
+ final String contentType = ctx.getResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE);
+ Long size = ctx.getResult(SIZE_RESULT_VALUE);
+
+ final var command = new ImageCreateCommand();
+
+ command.setPayload(ImageCreateRequest.builder().entityId(clientId).entityType(ClientApiConstants.clientEntityName)
+ .fileName(UUID.randomUUID().toString()).size(size).type(contentType).stream(ctx.getInputStream()).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ return response.get();
}
@DELETE
@Path("{clientId}/images")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- public CommandProcessingResult deleteClientImage(@PathParam("clientId") final Long clientId) {
-
+ public ImageDeleteResponse deleteClientImage(@PathParam("clientId") final Long clientId) {
validateAppuserClientsMapping(clientId);
- return this.imagesApiResource.deleteClientImage(ClientApiConstants.clientEntityName, clientId);
+ final var command = new ImageDeleteCommand();
+
+ command.setPayload(ImageDeleteRequest.builder().entityId(clientId).entityType(ClientApiConstants.clientEntityName).build());
+
+ final Supplier response = commandPipeline.send(command);
+
+ return response.get();
}
@GET
diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties
index 93c249625..2499cfe50 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -182,6 +182,7 @@ fineract.content.regex-whitelist-enabled=${FINERACT_CONTENT_REGEX_WHITELIST_ENAB
fineract.content.regex-whitelist=${FINERACT_CONTENT_REGEX_WHITELIST:.*\\.pdf$,.*\\.doc,.*\\.docx,.*\\.xls,.*\\.xlsx,.*\\.jpg,.*\\.jpeg,.*\\.png}
fineract.content.mime-whitelist-enabled=${FINERACT_CONTENT_MIME_WHITELIST_ENABLED:true}
fineract.content.mime-whitelist=${FINERACT_CONTENT_MIME_WHITELIST:application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,image/jpeg,image/png}
+fineract.content.default-buffer-size=${FINERACT_CONTENT_DEFAULT_BUFFER_SIZE:8192}
fineract.content.filesystem.enabled=${FINERACT_CONTENT_FILESYSTEM_ENABLED:true}
fineract.content.filesystem.rootFolder=${FINERACT_CONTENT_FILESYSTEM_ROOT_FOLDER:${user.home}/.fineract}
fineract.content.s3.enabled=${FINERACT_CONTENT_S3_ENABLED:false}
@@ -192,6 +193,9 @@ fineract.content.s3.region=${FINERACT_CONTENT_S3_REGION:}
fineract.content.s3.endpoint=${FINERACT_CONTENT_S3_ENDPOINT:}
fineract.content.s3.path-style-addressing-enabled=${FINERACT_CONTENT_S3_PATH_STYLE_ADDRESSING_ENABLED:false}
+spring.servlet.multipart.max-file-size=${FINERACT_MULTIPART_FILE_SIZE:5MB}
+spring.servlet.multipart.max-request-size=${FINERACT_MULTIPART_REQUEST_SIZE:10MB}
+
fineract.template.regex-whitelist-enabled=${FINERACT_TEMPLATE_REGEX_WHITELIST_ENABLED:true}
fineract.template.regex-whitelist=${FINERACT_TEMPLATE_REGEX_WHITELIST:}
@@ -489,55 +493,89 @@ resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POS
# business date
-resilience4j.retry.instances.commandBusinessDateUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandBusinessDateUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandBusinessDateUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandBusinessDateUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandBusinessDateUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandBusinessDateUpdate.max-attempts=${FINERACT_COMMAND_BUSINESS_DATE_UPDATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandBusinessDateUpdate.wait-duration=${FINERACT_COMMAND_BUSINESS_DATE_UPDATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandBusinessDateUpdate.enable-exponential-backoff=${FINERACT_COMMAND_BUSINESS_DATE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandBusinessDateUpdate.exponential-backoff-multiplier=${FINERACT_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandBusinessDateUpdate.retryExceptions=${FINERACT_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# cache
-resilience4j.retry.instances.commandCacheSwitch.max-attempts=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandCacheSwitch.wait-duration=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandCacheSwitch.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandCacheSwitch.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandCacheSwitch.retryExceptions=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandCacheSwitch.max-attempts=${FINERACT_COMMAND_CACHE_SWITCH_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandCacheSwitch.wait-duration=${FINERACT_COMMAND_CACHE_SWITCH_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandCacheSwitch.enable-exponential-backoff=${FINERACT_COMMAND_CACHE_SWITCH_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandCacheSwitch.exponential-backoff-multiplier=${FINERACT_COMMAND_CACHE_SWITCH_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandCacheSwitch.retryExceptions=${FINERACT_COMMAND_CACHE_SWITCH_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# currency
-resilience4j.retry.instances.commandCurrencyUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandCurrencyUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandCurrencyUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandCurrencyUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandCurrencyUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandCurrencyUpdate.max-attempts=${FINERACT_COMMAND_CURRENCY_UPDATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandCurrencyUpdate.wait-duration=${FINERACT_COMMAND_CURRENCY_UPDATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandCurrencyUpdate.enable-exponential-backoff=${FINERACT_COMMAND_CURRENCY_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandCurrencyUpdate.exponential-backoff-multiplier=${FINERACT_COMMAND_CURRENCY_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandCurrencyUpdate.retryExceptions=${FINERACT_COMMAND_CURRENCY_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# external event configuration
-resilience4j.retry.instances.commandExternalEventConfigurationUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandExternalEventConfigurationUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandExternalEventConfigurationUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandExternalEventConfigurationUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandExternalEventConfigurationUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandExternalEventConfigurationUpdate.max-attempts=${FINERACT_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandExternalEventConfigurationUpdate.wait-duration=${FINERACT_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandExternalEventConfigurationUpdate.enable-exponential-backoff=${FINERACT_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandExternalEventConfigurationUpdate.exponential-backoff-multiplier=${FINERACT_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandExternalEventConfigurationUpdate.retryExceptions=${FINERACT_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# note
-resilience4j.retry.instances.commandNoteCreate.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandNoteCreate.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandNoteCreate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandNoteCreate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandNoteCreate.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandNoteCreate.max-attempts=${FINERACT_COMMAND_NOTE_CREATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandNoteCreate.wait-duration=${FINERACT_COMMAND_NOTE_CREATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandNoteCreate.enable-exponential-backoff=${FINERACT_COMMAND_NOTE_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandNoteCreate.exponential-backoff-multiplier=${FINERACT_COMMAND_NOTE_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandNoteCreate.retryExceptions=${FINERACT_COMMAND_NOTE_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
-resilience4j.retry.instances.commandNoteUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandNoteUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandNoteUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandNoteUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandNoteUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandNoteUpdate.max-attempts=${FINERACT_COMMAND_NOTE_UPDATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandNoteUpdate.wait-duration=${FINERACT_COMMAND_NOTE_UPDATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandNoteUpdate.enable-exponential-backoff=${FINERACT_COMMAND_NOTE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandNoteUpdate.exponential-backoff-multiplier=${FINERACT_COMMAND_NOTE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandNoteUpdate.retryExceptions=${FINERACT_COMMAND_NOTE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
-resilience4j.retry.instances.commandNoteDelete.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandNoteDelete.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandNoteDelete.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandNoteDelete.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandNoteDelete.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandNoteDelete.max-attempts=${FINERACT_COMMAND_NOTE_DELETE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandNoteDelete.wait-duration=${FINERACT_COMMAND_NOTE_DELETE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandNoteDelete.enable-exponential-backoff=${FINERACT_COMMAND_NOTE_DELETE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandNoteDelete.exponential-backoff-multiplier=${FINERACT_COMMAND_NOTE_DELETE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandNoteDelete.retryExceptions=${FINERACT_COMMAND_NOTE_DELETE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+# document
+
+resilience4j.retry.instances.commandDocumentCreate.max-attempts=${FINERACT_COMMAND_DOCUMENT_CREATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandDocumentCreate.wait-duration=${FINERACT_COMMAND_DOCUMENT_CREATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandDocumentCreate.enable-exponential-backoff=${FINERACT_COMMAND_DOCUMENT_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandDocumentCreate.exponential-backoff-multiplier=${FINERACT_COMMAND_DOCUMENT_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandDocumentCreate.retryExceptions=${FINERACT_COMMAND_DOCUMENT_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+resilience4j.retry.instances.commandDocumentUpdate.max-attempts=${FINERACT_COMMAND_DOCUMENT_UPDATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandDocumentUpdate.wait-duration=${FINERACT_COMMAND_DOCUMENT_UPDATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandDocumentUpdate.enable-exponential-backoff=${FINERACT_COMMAND_DOCUMENT_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandDocumentUpdate.exponential-backoff-multiplier=${FINERACT_COMMAND_DOCUMENT_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandDocumentUpdate.retryExceptions=${FINERACT_COMMAND_DOCUMENT_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+resilience4j.retry.instances.commandDocumentDelete.max-attempts=${FINERACT_COMMAND_DOCUMENT_DELETE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandDocumentDelete.wait-duration=${FINERACT_COMMAND_DOCUMENT_DELETE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandDocumentDelete.enable-exponential-backoff=${FINERACT_COMMAND_DOCUMENT_DELETE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandDocumentDelete.exponential-backoff-multiplier=${FINERACT_COMMAND_DOCUMENT_DELETE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandDocumentDelete.retryExceptions=${FINERACT_COMMAND_DOCUMENT_DELETE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+# image
+
+resilience4j.retry.instances.commandImageCreate.max-attempts=${FINERACT_COMMAND_IMAGE_CREATE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandImageCreate.wait-duration=${FINERACT_COMMAND_IMAGE_CREATE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandImageCreate.enable-exponential-backoff=${FINERACT_COMMAND_IMAGE_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandImageCreate.exponential-backoff-multiplier=${FINERACT_COMMAND_IMAGE_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandImageCreate.retryExceptions=${FINERACT_COMMAND_IMAGE_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+resilience4j.retry.instances.commandImageDelete.max-attempts=${FINERACT_COMMAND_IMAGE_DELETE_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandImageDelete.wait-duration=${FINERACT_COMMAND_IMAGE_DELETE_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandImageDelete.enable-exponential-backoff=${FINERACT_COMMAND_IMAGE_DELETE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandImageDelete.exponential-backoff-multiplier=${FINERACT_COMMAND_IMAGE_DELETE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandImageDelete.retryExceptions=${FINERACT_COMMAND_IMAGE_DELETE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
fineract.command.enabled=true
fineract.command.executor=${FINERACT_COMMAND_EXECUTOR:sync}
@@ -551,26 +589,26 @@ fineract.command.file-dead-letter-queue-path=${FINERACT_FILE_DEAD_LETTER_QUEUE_P
# command
-resilience4j.retry.instances.commandAuditProcessing.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandAuditProcessing.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandAuditProcessing.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandAuditProcessing.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandAuditProcessing.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandAuditProcessing.max-attempts=${FINERACT_COMMAND_AUDIT_PROCESSING_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandAuditProcessing.wait-duration=${FINERACT_COMMAND_AUDIT_PROCESSING_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandAuditProcessing.enable-exponential-backoff=${FINERACT_COMMAND_AUDIT_PROCESSING_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditProcessing.exponential-backoff-multiplier=${FINERACT_COMMAND_AUDIT_PROCESSING_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditProcessing.retryExceptions=${FINERACT_COMMAND_AUDIT_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
-resilience4j.retry.instances.commandAuditProcessed.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandAuditProcessed.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandAuditProcessed.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandAuditProcessed.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandAuditProcessed.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandAuditProcessed.max-attempts=${FINERACT_COMMAND_AUDIT_PROCESSED_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandAuditProcessed.wait-duration=${FINERACT_COMMAND_AUDIT_PROCESSED_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandAuditProcessed.enable-exponential-backoff=${FINERACT_COMMAND_AUDIT_PROCESSED_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditProcessed.exponential-backoff-multiplier=${FINERACT_COMMAND_AUDIT_PROCESSED_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditProcessed.retryExceptions=${FINERACT_COMMAND_AUDIT_PROCESSED_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
-resilience4j.retry.instances.commandAuditError.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_MAX_ATTEMPTS:3}
-resilience4j.retry.instances.commandAuditError.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandAuditError.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandAuditError.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandAuditError.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandAuditError.max-attempts=${FINERACT_COMMAND_AUDIT_ERROR_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandAuditError.wait-duration=${FINERACT_COMMAND_AUDIT_ERROR_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandAuditError.enable-exponential-backoff=${FINERACT_COMMAND_AUDIT_ERROR_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditError.exponential-backoff-multiplier=${FINERACT_COMMAND_AUDIT_ERROR_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditError.retryExceptions=${FINERACT_COMMAND_AUDIT_ERROR_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
-resilience4j.retry.instances.commandAuditFallback.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_MAX_ATTEMPTS:1}
-resilience4j.retry.instances.commandAuditFallback.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_WAIT_DURATION:1s}
-resilience4j.retry.instances.commandAuditFallback.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
-resilience4j.retry.instances.commandAuditFallback.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
-resilience4j.retry.instances.commandAuditFallback.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+resilience4j.retry.instances.commandAuditFallback.max-attempts=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_MAX_ATTEMPTS:1}
+resilience4j.retry.instances.commandAuditFallback.wait-duration=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandAuditFallback.enable-exponential-backoff=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditFallback.exponential-backoff-multiplier=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditFallback.retryExceptions=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
diff --git a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml
index 7b05c2e7d..73c369c06 100644
--- a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml
+++ b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
@@ -228,9 +227,6 @@
org.apache.fineract.portfolio.loanproduct.domain.SupportedInterestRefundTypesListConverter
org.apache.fineract.portfolio.loanaccount.domain.LoanStatusConverter
-
- org.apache.fineract.infrastructure.documentmanagement.domain.Document
-
org.apache.fineract.portfolio.tax.domain.TaxComponent
org.apache.fineract.portfolio.tax.domain.TaxComponentHistory
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializerTest.java
index 8bf5bf31d..5ab60d471 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializerTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/document/DocumentBusinessEventSerializerTest.java
@@ -27,10 +27,8 @@ import static org.mockito.Mockito.when;
import org.apache.fineract.avro.document.v1.DocumentDataV1;
import org.apache.fineract.avro.generator.ByteBufferSerializable;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
-import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
-import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
-import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.document.DocumentDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
@@ -46,8 +44,6 @@ import org.mockito.quality.Strictness;
@ExtendWith(MockitoExtension.class)
class DocumentBusinessEventSerializerTest {
- @Mock
- private DocumentReadPlatformService readService;
@Mock
private DocumentDataMapper mapper;
@@ -55,7 +51,7 @@ class DocumentBusinessEventSerializerTest {
@BeforeEach
void setUp() {
- serializer = new DocumentBusinessEventSerializer(readService, mapper);
+ serializer = new DocumentBusinessEventSerializer(mapper);
}
@Test
@@ -68,9 +64,9 @@ class DocumentBusinessEventSerializerTest {
String fileName = "test_document.pdf";
String fileType = "application/pdf";
String description = "Test document description";
- Integer storageTypeInt = StorageType.FILE_SYSTEM.getValue();
+ Integer storageTypeInt = ContentStoreType.FILE_SYSTEM.getValue();
- Document document = mock(Document.class);
+ var document = mock(DocumentData.class);
when(document.getId()).thenReturn(docId);
when(document.getParentEntityType()).thenReturn(parentEntity);
when(document.getParentEntityId()).thenReturn(parentEntityId);
@@ -78,16 +74,12 @@ class DocumentBusinessEventSerializerTest {
when(document.getFileName()).thenReturn(fileName);
when(document.getType()).thenReturn(fileType);
when(document.getDescription()).thenReturn(description);
- when(document.storageType()).thenReturn(StorageType.fromInt(storageTypeInt));
DocumentCreatedBusinessEvent event = new DocumentCreatedBusinessEvent(document);
- DocumentData dtoFromReadService = mock(DocumentData.class);
- when(readService.retrieveDocument(parentEntity, parentEntityId, docId)).thenReturn(dtoFromReadService);
-
DocumentDataV1 avroFromMapper = DocumentDataV1.newBuilder().setId(docId).setParentEntityType(parentEntity)
.setParentEntityId(parentEntityId).setName(name).setFileName(fileName).setType(fileType).setDescription(description)
- .build();
+ .setStorageType(storageTypeInt).build();
when(mapper.map(any(DocumentData.class))).thenReturn(avroFromMapper);
ByteBufferSerializable serialised = serializer.toAvroDTO(event);
diff --git a/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml b/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml
index f6aa61581..8e0864c47 100644
--- a/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml
+++ b/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml b/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml
index aeb8e6522..551f6cbee 100644
--- a/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml
+++ b/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml b/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml
index 1e6e6db5b..5d2a654c3 100644
--- a/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml
+++ b/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml b/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml
index 5a440edcc..daaf89b7b 100644
--- a/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml
+++ b/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml
@@ -34,7 +34,6 @@
org.apache.fineract.useradministration.domain.Role
org.apache.fineract.portfolio.fund.domain.Fund
- org.apache.fineract.infrastructure.documentmanagement.domain.Image
org.apache.fineract.organisation.workingdays.domain.WorkingDays
org.apache.fineract.useradministration.domain.Permission
org.apache.fineract.useradministration.domain.AppUserClientMapping
diff --git a/integration-tests/dependencies.gradle b/integration-tests/dependencies.gradle
index 3e267893f..0dc39501c 100644
--- a/integration-tests/dependencies.gradle
+++ b/integration-tests/dependencies.gradle
@@ -67,4 +67,5 @@ dependencies {
testImplementation 'org.wiremock:wiremock-standalone'
testImplementation 'com.google.guava:guava'
+ testImplementation 'com.github.romankh3:image-comparison'
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/LocalContentStorageUtil.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/LocalContentStorageUtil.java
new file mode 100644
index 000000000..7ddcd4957
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/LocalContentStorageUtil.java
@@ -0,0 +1,89 @@
+/**
+ * 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.integrationtests.bulkimport.importhandler;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public final class LocalContentStorageUtil {
+
+ private LocalContentStorageUtil() {}
+
+ public static String path(String path) {
+ var currentPath = Path.of("").toAbsolutePath();
+
+ if (Files.exists(Path.of(path))) {
+ return path;
+ }
+
+ if (Files.exists(Path.of("/", path))) {
+ return "/" + path;
+ }
+
+ final var ghaPath = Path.of("/home/runner/.fineract/DefaultDemoTenant").resolve(path);
+
+ // github actions
+ if (Files.exists(ghaPath)) {
+ return ghaPath.toString();
+ }
+
+ // local dev run
+ final var devRunPath = Path.of(System.getProperty("user.home")).toAbsolutePath().resolve(".fineract/DefaultDemoTenant")
+ .resolve(path);
+
+ if (Files.exists(devRunPath)) {
+ return devRunPath.toString();
+ }
+
+ // local docker volumes
+ final var dockerPath = Path.of("").toAbsolutePath().getParent().resolve("build/fineract/tmp/DefaultDemoTenant").resolve(path);
+
+ if (Files.exists(dockerPath)) {
+ return dockerPath.toString();
+ }
+
+ throw new RuntimeException("Cannot find local fineract path: " + path + " (" + currentPath + ")");
+ }
+
+ @SuppressWarnings("UnusedMethod")
+ public static void waitFor(String path) throws InterruptedException {
+ Path file = Paths.get(path);
+ long maxWaitMillis = 10000;
+ long start = System.currentTimeMillis();
+ boolean exists = false;
+
+ while ((System.currentTimeMillis() - start) < maxWaitMillis) {
+ if (Files.exists(file)) {
+ exists = true;
+ break;
+ }
+
+ Thread.sleep(500);
+ }
+
+ if (exists) {
+ log.info("File found!");
+ } else {
+ log.warn("Timed out waiting for file.");
+ }
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/client/ClientEntityImportHandlerTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/client/ClientEntityImportHandlerTest.java
index 236854a00..c1160fc23 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/client/ClientEntityImportHandlerTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/client/ClientEntityImportHandlerTest.java
@@ -37,6 +37,7 @@ import java.util.Locale;
import org.apache.fineract.infrastructure.bulkimport.constants.ClientEntityConstants;
import org.apache.fineract.infrastructure.bulkimport.constants.TemplatePopulateImportConstants;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
+import org.apache.fineract.integrationtests.bulkimport.importhandler.LocalContentStorageUtil;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.OfficeHelper;
import org.apache.fineract.integrationtests.common.Utils;
@@ -144,7 +145,7 @@ public class ClientEntityImportHandlerTest {
Thread.sleep(10000);
// check status column of output excel
- String location = clientHelper.getOutputTemplateLocation(importDocumentId);
+ String location = LocalContentStorageUtil.path(clientHelper.getOutputTemplateLocation(importDocumentId));
FileInputStream fileInputStream = new FileInputStream(location);
Workbook outputWorkbook = new HSSFWorkbook(fileInputStream);
Sheet outputClientEntitySheet = outputWorkbook.getSheet(TemplatePopulateImportConstants.CLIENT_ENTITY_SHEET_NAME);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/loan/LoanImportHandlerTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/loan/LoanImportHandlerTest.java
index 309301a84..fe51ff169 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/loan/LoanImportHandlerTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/loan/LoanImportHandlerTest.java
@@ -45,6 +45,7 @@ import org.apache.fineract.client.models.PaymentTypeRequest;
import org.apache.fineract.client.models.PostPaymentTypesResponse;
import org.apache.fineract.infrastructure.bulkimport.constants.LoanConstants;
import org.apache.fineract.infrastructure.bulkimport.constants.TemplatePopulateImportConstants;
+import org.apache.fineract.integrationtests.bulkimport.importhandler.LocalContentStorageUtil;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
import org.apache.fineract.integrationtests.common.GroupHelper;
import org.apache.fineract.integrationtests.common.OfficeDomain;
@@ -246,7 +247,7 @@ public class LoanImportHandlerTest {
Thread.sleep(10000);
// check status column of output excel
- String location = loanTransactionHelper.getOutputTemplateLocation(importDocumentId);
+ String location = LocalContentStorageUtil.path(loanTransactionHelper.getOutputTemplateLocation(importDocumentId));
FileInputStream fileInputStream = new FileInputStream(location);
Workbook outputworkbook = new HSSFWorkbook(fileInputStream);
Sheet outputLoanSheet = outputworkbook.getSheet(TemplatePopulateImportConstants.LOANS_SHEET_NAME);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/office/OfficeImportHandlerTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/office/OfficeImportHandlerTest.java
index d8fb36acf..af538ce83 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/office/OfficeImportHandlerTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/office/OfficeImportHandlerTest.java
@@ -31,8 +31,10 @@ import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
+import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.bulkimport.constants.OfficeConstants;
import org.apache.fineract.infrastructure.bulkimport.constants.TemplatePopulateImportConstants;
+import org.apache.fineract.integrationtests.bulkimport.importhandler.LocalContentStorageUtil;
import org.apache.fineract.integrationtests.common.OfficeHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
@@ -42,13 +44,10 @@ import org.apache.poi.ss.usermodel.Workbook;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+@Slf4j
public class OfficeImportHandlerTest {
- private static final Logger LOG = LoggerFactory.getLogger(OfficeImportHandlerTest.class);
-
private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
@@ -96,14 +95,14 @@ public class OfficeImportHandlerTest {
Thread.sleep(10000);
// check status column of output excel
- String location = officeHelper.getOutputTemplateLocation(importDocumentId);
+ String location = LocalContentStorageUtil.path(officeHelper.getOutputTemplateLocation(importDocumentId));
FileInputStream fileInputStream = new FileInputStream(location);
Workbook outputWorkbook = new HSSFWorkbook(fileInputStream);
Sheet officeSheet = outputWorkbook.getSheet(TemplatePopulateImportConstants.OFFICE_SHEET_NAME);
Row row = officeSheet.getRow(1);
- LOG.info("Output location: {}", location);
- LOG.info("Failure reason column: {}", row.getCell(OfficeConstants.STATUS_COL).getStringCellValue());
+ log.info("Output location: {}", location);
+ log.info("Failure reason column: {}", row.getCell(OfficeConstants.STATUS_COL).getStringCellValue());
Assertions.assertEquals("Imported", row.getCell(OfficeConstants.STATUS_COL).getStringCellValue());
outputWorkbook.close();
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java
index a1f35aad2..13ea0d18f 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java
@@ -40,6 +40,7 @@ import java.util.Map;
import java.util.UUID;
import org.apache.fineract.infrastructure.bulkimport.constants.SavingsConstants;
import org.apache.fineract.infrastructure.bulkimport.constants.TemplatePopulateImportConstants;
+import org.apache.fineract.integrationtests.bulkimport.importhandler.LocalContentStorageUtil;
import org.apache.fineract.integrationtests.common.GroupHelper;
import org.apache.fineract.integrationtests.common.OfficeDomain;
import org.apache.fineract.integrationtests.common.OfficeHelper;
@@ -185,17 +186,17 @@ public class SavingsImportHandlerTest {
Thread.sleep(1000);
// check status column of output excel
- String location = savingsAccountHelper.getOutputTemplateLocation(importDocumentId);
+ String location = LocalContentStorageUtil.path(savingsAccountHelper.getOutputTemplateLocation(importDocumentId));
FileInputStream fileInputStream = new FileInputStream(location);
- Workbook Outputworkbook = new HSSFWorkbook(fileInputStream);
- Sheet OutputSavingsSheet = Outputworkbook.getSheet(TemplatePopulateImportConstants.SAVINGS_ACCOUNTS_SHEET_NAME);
- Row row = OutputSavingsSheet.getRow(1);
+ Workbook wb = new HSSFWorkbook(fileInputStream);
+ Sheet sheet = wb.getSheet(TemplatePopulateImportConstants.SAVINGS_ACCOUNTS_SHEET_NAME);
+ Row row = sheet.getRow(1);
LOG.info("Output location: {}", location);
LOG.info("Failure reason column: {}", row.getCell(SavingsConstants.STATUS_COL).getStringCellValue());
Assertions.assertEquals("Imported", row.getCell(SavingsConstants.STATUS_COL).getStringCellValue());
- Outputworkbook.close();
+ wb.close();
}
private void safeNumericValueSetter(Row targetRow, int targetColId, Sheet sourceSheet, int rowId, int colId) {
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/DocumentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/DocumentTest.java
index 17c2edf1e..ad87d74ab 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/DocumentTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/DocumentTest.java
@@ -38,7 +38,7 @@ import retrofit2.Response;
*
* @author Michael Vorburger.ch
*/
-public class DocumentTest extends IntegrationTest {
+class DocumentTest extends IntegrationTest {
final File testFile = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile());
@@ -87,7 +87,11 @@ public class DocumentTest extends IntegrationTest {
try (ResponseBody body = r.body()) {
assertThat(body.contentType()).isEqualTo(MediaType.get("image/jpeg"));
assertThat(body.bytes().length).isEqualTo(testFile.length());
- assertThat(body.contentLength()).isEqualTo(testFile.length());
+ // NOTE: now that everything is properly streamed and NOT loaded into memory the framework (Jersey) uses
+ // chunked encoding to serve dynamic aka large content; this is more efficient and outweighs the presenće of
+ // this information beforehand; the user can always count bytes after the download of the content; just to
+ // say: this here is a feature and intentional
+ // assertThat(body.contentLength()).isEqualTo(testFile.length());
}
assertThat(Parts.fileName(r)).hasValue(testFile.getName());
}
@@ -113,7 +117,7 @@ public class DocumentTest extends IntegrationTest {
ok(fineractClient().documents.deleteDocument("clients", clientId, documentId));
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> ok(fineractClient().documents.getDocument("clients", clientId, documentId)));
- assertEquals(404, exception.getResponse().code());
+ assertEquals(403, exception.getResponse().code());
}
@Order(9999)
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java
index b5a322d2d..e7585007f 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java
@@ -30,7 +30,6 @@ import java.nio.file.Files;
import org.apache.fineract.client.feign.FeignException;
import org.apache.fineract.client.feign.FineractMultipartEncoder;
import org.apache.fineract.client.models.DocumentData;
-import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
@@ -63,8 +62,7 @@ public class FeignDocumentTest extends FeignIntegrationTest {
FineractMultipartEncoder.MultipartData multipartData = new FineractMultipartEncoder.MultipartData()
.addFile("file", testFile.getName(), fileData, "image/jpeg").addText("name", name).addText("description", description);
- PostEntityTypeEntityIdDocumentsResponse response = ok(
- () -> fineractClient().documentsFixed().createDocument("clients", clientId, multipartData));
+ var response = ok(() -> fineractClient().documentsFixed().createDocument("clients", clientId, multipartData));
assertThat(response).isNotNull();
assertThat(response.getResourceId()).isNotNull();
@@ -135,7 +133,7 @@ public class FeignDocumentTest extends FeignIntegrationTest {
FeignException exception = assertThrows(FeignException.class,
() -> fineractClient().documentsFixed().getDocument("clients", clientId, documentId));
- assertEquals(404, exception.status());
+ assertEquals(403, exception.status());
}
@Test
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java
index 04399c62d..a90947c05 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java
@@ -18,21 +18,27 @@
*/
package org.apache.fineract.integrationtests.client;
+import static com.github.romankh3.image.comparison.model.ImageComparisonState.MATCH;
+import static java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import com.github.romankh3.image.comparison.ImageComparison;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import javax.imageio.ImageIO;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
+import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.fineract.client.services.ImagesApi;
import org.apache.fineract.client.util.Parts;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import retrofit2.Call;
-import retrofit2.Response;
import retrofit2.http.GET;
import retrofit2.http.Headers;
@@ -42,9 +48,12 @@ import retrofit2.http.Headers;
* @author Michael Vorburger.ch
*/
@Slf4j
-public class ImageTest extends IntegrationTest {
+class ImageTest extends IntegrationTest {
- final MultipartBody.Part testPart = createPart("michael.vorburger-crepes.jpg", "michael.vorburger-crepes.jpg", "image/jpeg");
+ static final String TEST_RESOURCE = "michael.vorburger-crepes.jpg";
+ static final int TEST_IMAGE_DIFF_PERCENTAGE = 2;
+
+ final MultipartBody.Part testPart = createPart(TEST_RESOURCE, TEST_RESOURCE, "image/jpeg");
Long clientId = new ClientTest().getClientId();
Long staffId = new StaffTest().getStaffId();
@@ -59,65 +68,66 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(2)
void getOriginalSize() throws IOException {
- ResponseBody r = ok(fineractClient().images.get("staff", staffId, 3505, 1972, null));
+ var r = ok(fineractClient().images.get("staff", staffId, 3505, 1972, null));
assertThat(r.contentType()).isEqualTo(MediaType.get("text/plain"));
- String encodedImage = r.string();
+ var encodedImage = r.string();
assertThat(encodedImage).startsWith("data:image/jpeg;base64,");
- assertThat(encodedImage).hasSize(2846549);
assertThat(r.contentLength()).isEqualTo(-1);
+ assertImage(encodedImage);
}
@Test
@Order(3)
void getSmallerSize() throws IOException {
- ResponseBody r = ok(fineractClient().images.get("staff", staffId, 128, 128, null));
- assertThat(r.string()).hasSize(6591);
+ var r = ok(fineractClient().images.get("staff", staffId, 128, 128, null));
+ assertThat(r.string()).hasSize(7251);
}
@Test
@Order(4)
void getBiggerSize() throws IOException {
- ResponseBody r = ok(fineractClient().images.get("staff", staffId, 9000, 6000, null));
- assertThat(r.string()).hasSize(2846549);
+ var r = ok(fineractClient().images.get("staff", staffId, 9000, 6000, null));
+ assertImage(r.string());
}
@Test
@Order(5)
void getInlineOctetOutput() throws IOException {
// 3505x1972 is the exact original size of testFile
- Response r = okR(fineractClient().images.get("staff", staffId, 3505, 1972, "inline_octet"));
- try (ResponseBody body = r.body()) {
+ var r = okR(fineractClient().images.get("staff", staffId, 3505, 1972, "inline_octet"));
+ try (var body = r.body()) {
assertThat(body.contentType()).isEqualTo(MediaType.get("image/jpeg"));
- assertThat(body.bytes().length).isEqualTo(testPart.body().contentLength());
- assertThat(body.contentLength()).isEqualTo(testPart.body().contentLength());
+ assertImage(body);
}
var staff = ok(fineractClient().staff.retrieveOne8(staffId));
- String expectedFileName = staff.getDisplayName() + "JPEG"; // without dot!
- assertThat(Parts.fileName(r)).hasValue(expectedFileName);
+ assertThat(Parts.fileName(r)).hasValue(staff.getDisplayName());
}
@Test
@Order(6)
void getOctetOutput() throws IOException {
- ResponseBody r = ok(fineractClient().images.get("staff", staffId, 3505, 1972, "octet"));
+ var r = ok(fineractClient().images.get("staff", staffId, 3505, 1972, "octet"));
assertThat(r.contentType()).isEqualTo(MediaType.get("image/jpeg"));
- assertThat(r.bytes().length).isEqualTo(testPart.body().contentLength());
- assertThat(r.contentLength()).isEqualTo(testPart.body().contentLength());
+ // NOTE: content length is not a reliable criteria; the server removes metadata (see it as a security feature)
+ // which makes the file immediately only half the size, but pixel wise the images are still the same
+ assertImage(r);
}
@Test
@Order(7)
void getAnotherOutput() throws IOException {
- ResponseBody r = ok(fineractClient().images.get("staff", staffId, 3505, 1972, "abcd"));
+ var r = ok(fineractClient().images.get("staff", staffId, 3505, 1972, "abcd"));
assertThat(r.contentType()).isEqualTo(MediaType.get("text/plain"));
- assertThat(r.string()).startsWith("data:image/jpeg;base64,");
+ var content = r.string();
+ assertThat(content).startsWith("data:image/jpeg;base64,");
+ assertImage(content);
}
@Test
@Order(8)
void getText() throws IOException {
- ResponseBody r = ok(fineractClient().createService(ImagesApiWithHeadersForTest.class).getText("staff", staffId, 3505, 1972, null));
+ var r = ok(fineractClient().createService(ImagesApiWithHeadersForTest.class).getText("staff", staffId, 3505, 1972, null));
assertThat(r.contentType()).isEqualTo(MediaType.get("text/plain"));
assertThat(r.string()).startsWith("data:image/jpeg;base64,");
}
@@ -125,9 +135,9 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(9)
void getBytes() throws IOException {
- ResponseBody r = ok(fineractClient().createService(ImagesApiWithHeadersForTest.class).getBytes("staff", staffId, 3505, 1972, null));
+ var r = ok(fineractClient().createService(ImagesApiWithHeadersForTest.class).getBytes("staff", staffId, 3505, 1972, null));
assertThat(r.contentType()).isEqualTo(MediaType.get("image/jpeg"));
- assertThat(r.bytes().length).isEqualTo(testPart.body().contentLength());
+ assertImage(r.bytes());
}
@Test
@@ -146,12 +156,12 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(100)
void pathTraversalJsp() {
- final MultipartBody.Part part = createPart("image-text-wrong-content.jsp",
- "../../../../../../../../../../tmp/image-text-wrong-content.jsp", "image/gif");
+ final var part = createPart("image-text-wrong-content.jsp", "../../../../../../../../../../tmp/image-text-wrong-content.jsp",
+ "image/gif");
assertThat(part).isNotNull();
- Exception exception = assertThrows(Exception.class, () -> {
+ var exception = assertThrows(Exception.class, () -> {
ok(fineractClient().images.create("clients", clientId, part));
});
@@ -163,11 +173,11 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(101)
void gifWithPngExtension() {
- final MultipartBody.Part part = createPart("image-gif-wrong-extension.png", "image-gif-wrong-extension.png", "image/png");
+ final var part = createPart("image-gif-wrong-extension.png", "image-gif-wrong-extension.png", "image/png");
assertThat(part).isNotNull();
- Exception exception = assertThrows(Exception.class, () -> {
+ var exception = assertThrows(Exception.class, () -> {
ok(fineractClient().images.create("clients", clientId, part));
});
@@ -179,11 +189,11 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(102)
void gifImage() {
- final MultipartBody.Part part = createPart("image-gif-correct-extension.gif", "image-gif-correct-extension.gif", "image/png");
+ final var part = createPart("image-gif-correct-extension.gif", "image-gif-correct-extension.gif", "image/png");
assertThat(part).isNotNull();
- Exception exception = assertThrows(Exception.class, () -> {
+ var exception = assertThrows(Exception.class, () -> {
ok(fineractClient().images.create("clients", clientId, part));
});
@@ -195,12 +205,12 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(103)
void pathTraversalJpg() {
- final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg",
- "../../../../../../../../../../tmp/michael.vorburger-crepes.jpg", "image/jpeg");
+ final var part = createPart("michael.vorburger-crepes.jpg", "../../../../../../../../../../tmp/michael.vorburger-crepes.jpg",
+ "image/jpeg");
assertThat(part).isNotNull();
- Exception exception = assertThrows(Exception.class, () -> {
+ var exception = assertThrows(Exception.class, () -> {
ok(fineractClient().images.create("clients", clientId, part));
});
@@ -212,11 +222,12 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(104)
void pathTraversalWithAbsolutePathJpg() {
- final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg", "../17/michael.vorburger-crepes.jpg", "image/jpeg");
+ create();
+ final var part = createPart("michael.vorburger-crepes.jpg", "../17/michael.vorburger-crepes.jpg", "image/jpeg");
assertThat(part).isNotNull();
- Exception exception = assertThrows(Exception.class, () -> {
+ var exception = assertThrows(Exception.class, () -> {
ok(fineractClient().images.create("clients", clientId, part));
});
@@ -228,11 +239,11 @@ public class ImageTest extends IntegrationTest {
@Test
@Order(105)
void pathTraversalWithAbsolutePathJpg2() {
- final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg", "..//17//michael.vorburger-crepes.jpg", "image/jpeg");
+ final var part = createPart("michael.vorburger-crepes.jpg", "..//17//michael.vorburger-crepes.jpg", "image/jpeg");
assertThat(part).isNotNull();
- Exception exception = assertThrows(Exception.class, () -> {
+ var exception = assertThrows(Exception.class, () -> {
ok(fineractClient().images.create("clients", clientId, part));
});
@@ -244,7 +255,7 @@ public class ImageTest extends IntegrationTest {
private MultipartBody.Part createPart(String fileResource, String fileName, String mediaType) {
try {
byte[] data = IOUtils.toByteArray(ImageTest.class.getClassLoader().getResourceAsStream(fileResource));
- RequestBody rb = RequestBody.create(data, MediaType.get(mediaType));
+ var rb = RequestBody.create(data, MediaType.get(mediaType));
return MultipartBody.Part.createFormData("file", fileName, rb);
} catch (Exception e) {
log.error("Error creating file part.", e);
@@ -253,6 +264,43 @@ public class ImageTest extends IntegrationTest {
return null;
}
+ private void assertImage(String content) {
+ assertImage(content, TEST_IMAGE_DIFF_PERCENTAGE);
+ }
+
+ private void assertImage(String content, double diffPercent) {
+ if (content.contains(",")) {
+ content = content.substring(content.indexOf(",") + 1);
+ }
+ assertImage(new Base64().decode(content), diffPercent);
+ }
+
+ private void assertImage(ResponseBody r) throws IOException {
+ assertImage(r.bytes(), TEST_IMAGE_DIFF_PERCENTAGE);
+ }
+
+ private void assertImage(byte[] data) {
+ assertImage(data, TEST_IMAGE_DIFF_PERCENTAGE);
+ }
+
+ private void assertImage(byte[] data, double diffPercent) {
+ try (var resource = ImageTest.class.getClassLoader().getResourceAsStream(TEST_RESOURCE)) {
+ requireNonNull(resource);
+
+ var expectedImage = ImageIO.read(resource);
+ var actualImage = ImageIO.read(new ByteArrayInputStream(data));
+
+ var result = new ImageComparison(expectedImage, actualImage).setAllowingPercentOfDifferentPixels(diffPercent).compareImages();
+ // result.writeResultTo(new File("build/diff.png"));
+
+ log.info("Image diff percentage: {}", result.getDifferencePercent());
+
+ assertEquals(MATCH, result.getImageComparisonState(), "The images should be identical");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
interface ImagesApiWithHeadersForTest extends ImagesApi {
@Headers("Accept: text/plain")
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
index a0a1e3593..7b3bbea71 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
@@ -494,9 +494,8 @@ public final class Utils {
@Deprecated(forRemoval = true)
public static String performServerOutputTemplateLocationGet(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, final String getURL, final String importDocumentId) {
- final String templateLocation = given().spec(requestSpec).queryParam("importDocumentId", importDocumentId).expect()
- .spec(responseSpec).log().ifError().when().get(getURL).andReturn().asString();
- return templateLocation.substring(1, templateLocation.length() - 1);
+ return given().spec(requestSpec).queryParam("importDocumentId", importDocumentId).expect().spec(responseSpec).log().ifError().when()
+ .get(getURL).andReturn().asString();
}
@Deprecated(forRemoval = true)