diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
index cab710d68db..f5eea68e197 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:'
}
diff --git a/config/docker/compose/fineract.yml b/config/docker/compose/fineract.yml
index 801b73fc654..4d92fdf49fa 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 4b0dc246501..78ac9951f6e 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-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 39fd221569d..1d9aad2360b 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.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 @@ PostEntityTypeEntityIdDocumentsResponse createDocument(@Param("entityType") Stri
* @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 @@ DocumentData getDocument(@Param("entityType") String entityType, @Param("entityI
* @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 d19703607ac..b59be072c4d 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.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 @@ void testDeleteDocument() {
.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 @@ void testDeleteDocumentForLoan() {
.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 3017b6dfda6..4acfe358a29 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 8d56912fb06..6470541775f 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 @@
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 @@ Call createDocument(@retrofit2.http.Pat
* @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 @@ Call> retrieveAllDocuments(@retrofit2.http.Path("entityType")
*/
@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-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 410560f9a5c..631a043adf5 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
@@ -138,7 +138,17 @@ void onStartup() {
@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 +158,12 @@ private void fallback(Command> command, Throwable t) throws Exception {
}
@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-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 445f3667f85..2b8dc3a6859 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 30dd64e46a9..68889f9dd1f 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 @@ Long importWorkbook(String entityType, InputStream inputStream, FormDataContentD
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 9730467be0a..26100f3902f 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 static class FineractContentProperties {
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 00000000000..5b7d6eb1a65
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntityImageIdAdapter.java
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.documentmanagement.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);
+
+ default void set(Long entityId) {
+ set(entityId, null);
+ }
+
+ void 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 3123448625a..7fc73a2fcb4 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 7830c40f642..477c3df131f 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.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.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 Office office() {
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 00000000000..d78605f2524
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientImageIdAdapter.java
@@ -0,0 +1,62 @@
+/**
+ * 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 void set(Long entityId, Long imageId) {
+ if (imageId == null) {
+ repository.removeImageId(entityId);
+ } else {
+ repository.updateByIdAndImageId(entityId, imageId);
+ }
+ }
+}
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 60fafa98af1..95112867962 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.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.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 Long officeId() {
return this.office.getId();
}
- public void setImage(final Image image) {
- this.image = image;
- }
-
public String mobileNo() {
return this.mobileNo;
}
@@ -510,10 +500,6 @@ public void setMobileNo(final String mobileNo) {
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 f3a9ff11218..05578965323 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.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 @@ Client fetchByClientIdAndHierarchy(@Param("clientId") Long clientId, @Param("off
@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 00000000000..b12d5392222
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/util/StreamResponseUtil.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.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;
+ }
+}
diff --git a/fineract-document/dependencies.gradle b/fineract-document/dependencies.gradle
index 0f4c8facd23..7bc3074e5b9 100644
--- a/fineract-document/dependencies.gradle
+++ b/fineract-document/dependencies.gradle
@@ -24,11 +24,15 @@ 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-security',
+ 'org.springframework.boot:spring-boot-starter-data-jdbc',
'jakarta.ws.rs:jakarta.ws.rs-api',
'org.glassfish.jersey.media:jersey-media-multipart',
@@ -53,6 +57,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'
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/config/ContentExecutorConfig.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/config/ContentExecutorConfig.java
new file mode 100644
index 00000000000..2e85715eacc
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/config/ContentExecutorConfig.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
+public class ContentExecutorConfig {
+
+ @Bean(BEAN_NAME_EXECUTOR)
+ public 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 2bd697eb1cd..306952d60b8 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 1535c0b542f..323b3c79893 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 00000000000..d24794e3ea3
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/ContentDetectorContext.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.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;
+ 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.fileName, mimeType, extension, format);
+ }
+
+ public ContentDetectorContext clone(InputStream inputStream, String mimeType, String extension, String format) {
+ return new ContentDetectorContext(inputStream, this.fileName, mimeType, extension, format);
+ }
+}
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 00000000000..048bc1a321e
--- /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 00000000000..f9b7234b0e1
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetector.java
@@ -0,0 +1,78 @@
+/**
+ * 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.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) {
+ try {
+ if (ctx.getInputStream() != null) {
+ 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();
+
+ return ctx.clone(stream, mimeType, extension, extension.substring(1));
+ } else {
+ 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);
+ }
+ }
+
+ 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 655f7710673..d2fba0c5344 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 00000000000..56f14934905
--- /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/ContentProcessorException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentProcessorException.java
new file mode 100644
index 00000000000..1fe80600b82
--- /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 00000000000..9dc81396b1f
--- /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/exception/ContentStorePolicyException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentStorePolicyException.java
new file mode 100644
index 00000000000..2689c51b977
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/exception/ContentStorePolicyException.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 ContentStorePolicyException extends AbstractPlatformException {
+
+ public ContentStorePolicyException(String message) {
+ super("error.msg.content.policy", message);
+ }
+}
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 00000000000..db6c7666a62
--- /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 00000000000..9b27922ac75
--- /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/SiblingOverwriteContentPolicy.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/SiblingOverwriteContentPolicy.java
new file mode 100644
index 00000000000..a6740ebaa14
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/SiblingOverwriteContentPolicy.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.policy;
+
+import java.util.regex.Pattern;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentStorePolicyException;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+@ConditionalOnProperty(value = "fineract.content.mime-whitelist-enabled", havingValue = "true")
+public class SiblingOverwriteContentPolicy implements ContentPolicy {
+
+ private final Pattern overwriteSiblingsPattern = Pattern.compile(".*\\.\\./+[0-9]+/+.*");
+
+ private final FineractProperties properties;
+
+ @Override
+ public void check(ContentPolicyContext ctx) {
+ if (overwriteSiblingsPattern.matcher(ctx.getPath()).matches()) {
+ throw new ContentStorePolicyException(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 00000000000..3f16268f945
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/policy/WhitelistContentPolicy.java
@@ -0,0 +1,78 @@
+/**
+ * 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.ContentStorePolicyException;
+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 ContentStorePolicyException(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 ContentStorePolicyException(String.format("Could not detect mime type for filename %s!", fileName));
+ }
+
+ if (!properties.getContent().getMimeWhitelist().contains(ctx.getMimeType())) {
+ throw new ContentStorePolicyException(
+ String.format("Detected mime type %s for filename %s not allowed!", ctx.getMimeType(), fileName));
+ }
+
+ // TODO: figure out how to do this best
+ /*
+ * if (!type.equalsIgnoreCase(streamType)) { throw new ContentStorePolicyException(String.
+ * format("Detected filename (%s) and content (%s) mime type do not match!", streamType, type)); }
+ */
+ }
+ }
+}
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 00000000000..8a9245868c4
--- /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 00000000000..b870a94cb03
--- /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 00000000000..6a882f4153a
--- /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 00000000000..71cdc82a786
--- /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 00000000000..608efe4d8f5
--- /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 00000000000..6163cea173d
--- /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 00000000000..aa0e7eecfd0
--- /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 00000000000..a3a0d7bb224
--- /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 00000000000..aefedf8b5f7
--- /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;
+
+@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 {
+ try (var iis = ImageIO.createImageInputStream(is)) {
+ final var readers = ImageIO.getImageReaders(iis);
+
+ if (!readers.hasNext()) {
+ throw new ContentProcessorException("No image reader found for format");
+ }
+
+ final var reader = readers.next();
+
+ try {
+ reader.setInput(iis, 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 00000000000..3c7b9575fb2
--- /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 00000000000..b042893b0a8
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/ContentStoreService.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.contentstore.service;
+
+import java.io.InputStream;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
+
+public interface ContentStoreService {
+
+ String EXTENSION_REGEX = "^.+\\..+$";
+ String DELIMITER = "/";
+ String DOT = ".";
+
+ InputStream download(String path);
+
+ String upload(String path, InputStream is);
+
+ 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 00000000000..67b1974b1c2
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/FileContentStoreService.java
@@ -0,0 +1,166 @@
+/**
+ * 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.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.TikaContentDetector;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentStoreException;
+import org.apache.fineract.infrastructure.contentstore.policy.ContentPolicyContext;
+import org.apache.fineract.infrastructure.contentstore.policy.SiblingOverwriteContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.WhitelistContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathRandomizer;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathSanitizer;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+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 TikaContentDetector tikaContentDetector;
+ private final WhitelistContentPolicy whitelistPolicy;
+ private final SiblingOverwriteContentPolicy siblingOverwritePolicy;
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public InputStream download(String path) {
+ final var safePath = pathSanitizer.sanitize(path);
+
+ check(safePath);
+
+ try {
+ final var target = getPath(safePath);
+
+ return Files.newInputStream(target);
+ } catch (IOException e) {
+ throw new ContentStoreException(e);
+ }
+ }
+
+ @Override
+ public String upload(String path, InputStream is) {
+ var safePath = pathSanitizer.sanitize(path);
+
+ // if we don't have a proper file extension then let's detect the file type and add one
+ if (!safePath.matches(EXTENSION_REGEX)) {
+ // TODO: working in the unit tests, but not here
+ // final var ctx = tikaContentDetector.detect(ContentDetectorContext.builder().inputStream(is).build());
+ // safePath += ctx.getExtension();
+ // inputStream = ctx.getInputStream();
+
+ // note: same strategy as before, not great, but good enough until we fix the content detector
+ safePath += ".jpg";
+ }
+
+ check(safePath);
+
+ try (var in = is) {
+ final var target = getPath(safePath);
+
+ IOUtils.copy(in, new FileOutputStream(target.toString()));
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+
+ return safePath;
+ }
+
+ @Override
+ public void delete(String path) {
+ final var safePath = pathSanitizer.sanitize(path);
+
+ check(safePath);
+
+ try {
+ final var target = getPath(safePath);
+
+ 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) {
+ var p = Path.of(properties.getContent().getFilesystem().getRootFolder(),
+ ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim(), path);
+
+ try {
+ if (Files.notExists(p.getParent())) {
+ Files.createDirectories(p.getParent());
+ }
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+
+ return p;
+ }
+
+ private void check(String path) {
+ // don't do stream processing if not necessary
+ final var ctx = tikaContentDetector.detect(ContentDetectorContext.builder().fileName(FilenameUtils.getName(path)).build());
+
+ final var policyContext = ContentPolicyContext.builder().mimeType(ctx.getMimeType()).path(path).build();
+
+ whitelistPolicy.check(policyContext);
+ siblingOverwritePolicy.check(policyContext);
+ }
+
+ 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 00000000000..f0bf0367265
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/contentstore/service/S3ContentStoreService.java
@@ -0,0 +1,122 @@
+/**
+ * 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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
+import org.apache.fineract.infrastructure.contentstore.detector.TikaContentDetector;
+import org.apache.fineract.infrastructure.contentstore.exception.ContentStoreException;
+import org.apache.fineract.infrastructure.contentstore.policy.ContentPolicyContext;
+import org.apache.fineract.infrastructure.contentstore.policy.SiblingOverwriteContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.policy.WhitelistContentPolicy;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathRandomizer;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathSanitizer;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
+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 {
+ // TODO: add some security mechanics as in file system store
+
+ private final S3Client s3Client;
+ private final ContentPathSanitizer pathSanitizer;
+ private final ContentPathRandomizer pathRandomizer;
+ private final TikaContentDetector tikaContentDetector;
+ private final WhitelistContentPolicy whitelistPolicy;
+ private final SiblingOverwriteContentPolicy siblingOverwritePolicy;
+ private final ContentPipe pipe;
+ private final FineractProperties properties;
+
+ @Override
+ public InputStream download(String path) {
+ final var safePath = pathSanitizer.sanitize(path);
+
+ check(safePath);
+
+ try {
+ return s3Client.getObject(GetObjectRequest.builder().bucket(properties.getContent().getS3().getBucketName()).key(path).build(),
+ ResponseTransformer.toBytes()).asInputStream();
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+ }
+
+ @Override
+ public String upload(String path, InputStream is) {
+ var safePath = pathSanitizer.sanitize(path);
+
+ if (!safePath.matches(EXTENSION_REGEX)) {
+ final var ctx = tikaContentDetector.detect(ContentDetectorContext.builder().inputStream(is).build());
+ safePath += ctx.getMimeType();
+ }
+
+ check(safePath);
+
+ try {
+ final var p = safePath;
+ final var ctx = tikaContentDetector.detect(ContentDetectorContext.builder().fileName(FilenameUtils.getName(safePath)).build());
+
+ s3Client.putObject(builder -> builder.bucket(properties.getContent().getS3().getBucketName()).key(p),
+ RequestBody.fromContentProvider(ContentStreamProvider.fromInputStream(is), ctx.getMimeType()));
+ } catch (Exception e) {
+ throw new ContentStoreException(e);
+ }
+
+ return path;
+ }
+
+ @Override
+ public void delete(String path) {
+ final var safePath = pathSanitizer.sanitize(path);
+
+ check(safePath);
+
+ 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;
+ }
+
+ private void check(String path) {
+ final var policyContext = ContentPolicyContext.builder().path(path).build();
+
+ whitelistPolicy.check(policyContext);
+ siblingOverwritePolicy.check(policyContext);
+ }
+}
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 00000000000..6ac73bd703d
--- /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 415532fcdb1..ef07ee76992 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 00000000000..131c50e4044
--- /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 00000000000..90da0da8340
--- /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 00000000000..2827480d60b
--- /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 905d2a614fe..00000000000
--- 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 00000000000..59fe93273e2
--- /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 00000000000..3e4499899e6
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentApiResource.java
@@ -0,0 +1,244 @@
+/**
+ * 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 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.UUID;
+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.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 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()).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();
+
+ command.setPayload(DocumentCreateRequest.builder().entityId(entityId).entityType(entityType).name(name).description(description)
+ .fileName(filePart.getFileName().orElseGet(() -> UUID.randomUUID().toString())).size(fileSize)
+ .type(filePart.getMediaType().toString()).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 c44d4fcaa7a..00000000000
--- 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 91ad2f2c94f..00000000000
--- 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 00000000000..54c8d473e3a
--- /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 2c3f257c2c0..b31ccdae809 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,8 +18,27 @@
*/
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 io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
@@ -33,130 +52,134 @@
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.UUID;
+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.Base64EncoderContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.processor.ContentProcessorContext;
+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
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
@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 FileUploadValidator fileUploadValidator;
- private final ImageResizer imageResizer;
+ private final CommandPipeline commandPipeline;
+ private final ImageResizeContentProcessor imageResizeContentProcessor;
+ private final Base64EncoderContentProcessor base64EncoderContentProcessor;
+ private final DataUrlEncoderContentProcessor dataUrlEncoderContentProcessor;
- /**
- * 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);
+ @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(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) final 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());
+
+ 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()));
+ }
+
+ if (IMAGE_API_VALUE_OUTPUT_INLINE_OCTET.equalsIgnoreCase(output)) {
+ dispositionType = DISPOSITION_TYPE_INLINE;
+ } else {
+ dispositionType = DISPOSITION_TYPE_ATTACHMENT;
+ }
+ } 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));
+ }
+ }
+
+ // make sure we use the transformed input stream ("ctx.getInputStream()")
+ final var streamResponseData = StreamResponseUtil.StreamResponseData.builder().fileName(content.getFileName())
+ .type(content.getContentType()).stream(ctx.getInputStream()).dispositionType(dispositionType).build();
+
+ return StreamResponseUtil.ok(streamResponseData);
}
- /**
- * 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);
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @RequestBody(description = "Upload new client image")
+ 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) {
+
+ requireNonNull(fileDetails, "Missing file details");
+ requireNonNull(filePart, "Missing file part");
+ requireNonNull(is, "Missing input stream");
+
+ final var command = new ImageCreateCommand();
+
+ command.setPayload(ImageCreateRequest.builder().entityId(entityId).entityType(entityType).fileName(fileDetails.getFileName())
+ .size(fileSize).type(filePart.getMediaType().toString()).stream(is).build());
- final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody);
+ final Supplier response = commandPipeline.send(command);
- return imageWritePlatformService.saveOrUpdateImage(entityName, entityId, base64EncodedImage);
+ return response.get();
}
/**
- * Returns a images, either as Base64 encoded text/plain or as inline or attachment with image MIME type as
- * Content-Type.
+ * Upload image as a Data URL (essentially a base64 encoded stream)
*/
- @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");
- }
+ @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) {
+ // final var inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
- final FileData imageData = imageReadPlatformService.retrieveImage(entityName, entityId);
- final FileData resizedImage = imageResizer.resize(imageData, maxWidth, maxHeight);
+ final var command = new ImageCreateCommand();
- // 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");
- }
+ command.setPayload(ImageCreateRequest.builder().entityId(entityId).entityType(entityType).fileName(UUID.randomUUID().toString())
+ .stream(body).build());
- // 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();
- }
+ final Supplier response = commandPipeline.send(command);
- try {
- byte[] resizedImageBytes = resizedImage.getByteSource().read();
- if (resizedImageBytes != null) {
- final String clientImageAsBase64Text = imageDataURISuffix + Base64.getMimeEncoder().encodeToString(resizedImageBytes);
- return Response.ok(clientImageAsBase64Text, MediaType.TEXT_PLAIN_TYPE).build();
- } else {
- log.error("resizedImageBytes is null for entityName={}, entityId={}, maxWidth={}, maxHeight={}", entityName, entityId,
- maxWidth, maxHeight);
- return Response.serverError().build();
- }
- } catch (IOException e) {
- throw new ContentManagementException(imageData.name(), e.getMessage(), e);
- }
+ return response.get();
}
/**
@@ -165,64 +188,35 @@ public Response retrieveImage(@PathParam("entity") final String entityName, @Pat
*/
@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)
+ * 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);
- }
-
- /*** Entities for document Management **/
- public enum EntityTypeForImages {
-
- STAFF, //
- CLIENTS; //
+ public ImageDeleteResponse deleteImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
+ @PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId) {
+ final var command = new ImageDeleteCommand();
- @Override
- public String toString() {
- return name().toLowerCase();
- }
- }
+ command.setPayload(ImageDeleteRequest.builder().entityId(entityId).entityType(entityType).build());
- private void validateEntityTypeforImage(final String entityName) {
- if (!checkValidEntityType(entityName)) {
- throw new InvalidEntityTypeForImageManagementException(entityName);
- }
- }
+ final Supplier response = commandPipeline.send(command);
- 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 0c430c8fdc3..ecfa5a29402 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 @@
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 String getLocation() {
return this.location;
}
- public Set getModifiedParameters() {
- return this.modifiedParameters;
- }
-
public void setFileName(final String fileName) {
this.fileName = fileName;
}
@@ -108,37 +98,4 @@ public void setType(final String type) {
public void setLocation(final String location) {
this.location = location;
}
-
- public Integer getStorageType() {
- return this.storageType;
- }
-
- public void setStorageType(final Integer storageType) {
- this.storageType = storageType;
- }
-
- public boolean isNameChanged() {
- return this.modifiedParameters.contains("name");
- }
-
- public boolean isFileNameChanged() {
- return this.modifiedParameters.contains("fileName");
- }
-
- public boolean isSizeChanged() {
- return this.modifiedParameters.contains("size");
- }
-
- public boolean isFileTypeChanged() {
- return this.modifiedParameters.contains("type");
- }
-
- public boolean isDescriptionChanged() {
- return this.modifiedParameters.contains("description");
- }
-
- public boolean isLocationChanged() {
- return this.modifiedParameters.contains("location");
- }
-
}
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 4cb0943970d..00000000000
--- 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 00000000000..a8b0d1e65a3
--- /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 00000000000..1f0b5dc90a5
--- /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 00000000000..609b76bab71
--- /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 00000000000..2018da75e22
--- /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 00000000000..e510df6607c
--- /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 831e7b68271..00000000000
--- 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 b17803e029b..00000000000
--- 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 d13d08b721e..00000000000
--- 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 9d7c5588921..00000000000
--- 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 6d546ba259a..00000000000
--- 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 180a7512193..00000000000
--- 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 00000000000..3d00403a68d
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/data/DocumentContent.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.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;
+ @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 00000000000..9fe12c67ad9
--- /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 a17c5c3f051..d2a3d197c3f 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 ec487982299..3621cd35cc2 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 @@
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 00000000000..5557cbe36e0
--- /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 00000000000..59f3858a1b9
--- /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 00000000000..2d9a17f49dd
--- /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 00000000000..16a34efe9d0
--- /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 00000000000..a9f517d0cd9
--- /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 00000000000..fea6695cee6
--- /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 18993eb62bd..00000000000
--- 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 00000000000..7ef53439c1b
--- /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 00000000000..af2430a34b7
--- /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 dc6903630e0..00000000000
--- 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 2e3ea9ecd9e..37ec43d830e 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
@@ -21,13 +21,21 @@
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
-import org.apache.commons.lang3.StringUtils;
+import lombok.AllArgsConstructor;
+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.documentmanagement.command.DocumentCommand;
@Entity
@Table(name = "m_document")
-public class Document extends AbstractPersistableCustom {
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public final class Document extends AbstractPersistableCustom {
@Column(name = "parent_entity_type", length = 50)
private String parentEntityType;
@@ -55,113 +63,4 @@ public class Document extends AbstractPersistableCustom {
@Column(name = "storage_type_enum")
private Integer storageType;
-
- public Document() {}
-
- public static Document createNew(final String parentEntityType, final Long parentEntityId, final String name, final String fileName,
- final Long size, final String type, final String description, final String location, final StorageType storageType) {
- return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location, storageType);
- }
-
- private Document(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size,
- final String type, final String description, final String location, final StorageType storageType) {
- this.parentEntityType = StringUtils.defaultIfEmpty(parentEntityType, null);
- this.parentEntityId = parentEntityId;
- this.name = StringUtils.defaultIfEmpty(name, null);
- this.fileName = StringUtils.defaultIfEmpty(fileName, null);
- this.size = size;
- this.type = StringUtils.defaultIfEmpty(type, null);
- this.description = StringUtils.defaultIfEmpty(description, null);
- this.location = StringUtils.defaultIfEmpty(location, null);
- this.storageType = storageType.getValue();
- }
-
- public void update(final DocumentCommand command) {
- if (command.isDescriptionChanged()) {
- this.description = command.getDescription();
- }
- if (command.isFileNameChanged()) {
- this.fileName = command.getFileName();
- }
- if (command.isFileTypeChanged()) {
- this.type = command.getType();
- }
- if (command.isLocationChanged()) {
- this.location = command.getLocation();
- }
- if (command.isNameChanged()) {
- this.name = command.getName();
- }
- if (command.isSizeChanged()) {
- this.size = command.getSize();
- }
- }
-
- public String getParentEntityType() {
- return this.parentEntityType;
- }
-
- public void setParentEntityType(final String parentEntityType) {
- this.parentEntityType = parentEntityType;
- }
-
- public Long getParentEntityId() {
- return this.parentEntityId;
- }
-
- public void setParentEntityId(final Long parentEntityId) {
- this.parentEntityId = parentEntityId;
- }
-
- public String getName() {
- return this.name;
- }
-
- public void setName(final String name) {
- this.name = name;
- }
-
- public String getFileName() {
- return this.fileName;
- }
-
- public void setFileName(final String fileName) {
- this.fileName = fileName;
- }
-
- public Long getSize() {
- return this.size;
- }
-
- public void setSize(final Long size) {
- this.size = size;
- }
-
- public String getType() {
- return this.type;
- }
-
- public void setType(final String type) {
- this.type = type;
- }
-
- public String getDescription() {
- return this.description;
- }
-
- public void setDescription(final String description) {
- this.description = description;
- }
-
- public String getLocation() {
- return this.location;
- }
-
- public void setLocation(final String location) {
- this.location = location;
- }
-
- public StorageType storageType() {
- return StorageType.fromInt(this.storageType);
- }
}
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 2f4a2b16cbb..ac0e1ffde3c 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 java.util.List;
+import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface DocumentRepository extends JpaRepository, JpaSpecificationExecutor {
- // no added behaviour
+
+ List findAllByParentEntityTypeAndParentEntityId(String parentEntityType, Long parentEntityId);
+
+ Optional findOneByIdAndParentEntityTypeAndParentEntityId(Long id, String parentEntityType, Long parentEntityId);
}
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 70%
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 190137a845e..460619d263d 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
@@ -21,10 +21,20 @@
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@Entity
@Table(name = "m_image")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
public final class Image extends AbstractPersistableCustom {
@Column(name = "location", length = 500)
@@ -32,30 +42,4 @@ public final class Image extends AbstractPersistableCustom {
@Column(name = "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 fd1e051b136..72aa4fe4f23 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;
-public interface ImageRepository extends JpaRepository, JpaSpecificationExecutor {}
+public interface ImageRepository extends JpaRepository, JpaSpecificationExecutor {}
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 7526b870ab0..00000000000
--- 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 cd0e21ee787..166bb9b24bd 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.AbstractPlatformException;
-/**
- * Runtime exception for invalid image types
- */
-public class InvalidEntityTypeForImageManagementException extends AbstractPlatformResourceNotFoundException {
+public class DocumentInvalidRequestException extends AbstractPlatformException {
- 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 cbe3ced4ca7..b3e8ef5adc0 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
@@ -24,6 +24,10 @@
public class DocumentNotFoundException extends AbstractPlatformResourceNotFoundException {
+ 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);
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 00000000000..3250e214d86
--- /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 00000000000..58d8122753c
--- /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 00000000000..a55cb5d7e11
--- /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 00000000000..16eb696c03d
--- /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 00000000000..b302b433d9a
--- /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 00000000000..48dad457c73
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/mapping/DocumentMapper.java
@@ -0,0 +1,42 @@
+/**
+ * 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(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 93e7914079a..1dab3be03c5 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 84c4e126004..59fbbb0820b 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 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);
- }
- }
+ public DocumentContent retrieveDocumentContent(final String entityType, final Long entityId, final Long documentId) {
+ final var doc = documentRepository.findOneByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
+ .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
+ final var is = storeService.download(doc.getLocation());
- 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
+ return documentMapper.map(doc, is);
}
- 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 retrieveDocument(final String entityType, final Long entityId, final Long documentId) {
+ final var doc = documentRepository.findOneByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
+ .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
- @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 807a8d977e9..d583fe872d2 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 00000000000..57e1e498284
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java
@@ -0,0 +1,178 @@
+/**
+ * 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 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.contentstore.util.ContentPathRandomizer;
+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 ContentPathRandomizer pathRandomizer;
+ 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(), pathRandomizer.randomize(),
+ request.getFileName(), storeService.getDelimiter());
+
+ path = storeService.upload(path, request.getStream());
+
+ final var doc = new Document().setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId())
+ .setName(request.getName()).setFileName(request.getFileName()).setSize(request.getSize()).setType(request.getType())
+ .setDescription(request.getDescription()).setLocation(path).setStorageType(storeService.getType().getValue());
+
+ documentRepository.saveAndFlush(doc);
+
+ businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(documentMapper.map(doc)));
+
+ return DocumentCreateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
+ } catch (final Exception e) {
+ log.error("Error occured.", 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(), pathRandomizer.randomize(),
+ 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());
+ doc.setLocation(path);
+ doc.setFileName(FilenameUtils.getName(path));
+ }
+ } else if (request.getStream() != null) {
+ storeService.upload(doc.getLocation(), request.getStream());
+ }
+ 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.saveAndFlush(doc);
+
+ // TODO: shouldn't we send a business event?
+ // businessEventNotifierService.notifyPostBusinessEvent(new DocumentUpdateBusinessEvent(doc));
+
+ return DocumentUpdateResponse.builder().resourceId(doc.getId())
+ // .resourceIdentifier("...")
+ .build();
+ } catch (final Exception e) {
+ log.error("Error occured.", 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) {
+ 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("...")
+ .build();
+ }
+
+ private String getPath(final String prefix, final String entityType, final Long entityId, final String path, final String fileName,
+ String delimiter) {
+ requireNonNull(prefix);
+ requireNonNull(entityType);
+ requireNonNull(entityId);
+ requireNonNull(path);
+ requireNonNull(fileName);
+ requireNonNull(delimiter);
+
+ return String.join(delimiter, prefix, entityType, String.valueOf(entityId), path, 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 0ae4f599807..00000000000
--- 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 608f64762bf..9684e77e832 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 00000000000..a7798b5d9f0
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
@@ -0,0 +1,58 @@
+/**
+ * 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.TikaContentDetector;
+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.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 TikaContentDetector tikaContentDetector; // TODO: get rid of this by saving mime type with image
+
+ @Override
+ public DocumentContent retrieveImage(final String entityType, final Long entityId) {
+ 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.getName(image.getLocation()).substring(1)).displayName(imageIdResult.getDisplayName())
+ .contentType(tikaContentDetector
+ .detect(ContentDetectorContext.builder().fileName(FilenameUtils.getName(image.getLocation())).build())
+ .getMimeType())
+ .stream(storeService.download(image.getLocation())).build()))
+ .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, -1L));
+ }
+}
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 4b30b59b695..f976338ffd2 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 00000000000..bed93f9fb87
--- /dev/null
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java
@@ -0,0 +1,152 @@
+/**
+ * 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.nonNull;
+import static java.util.Objects.requireNonNull;
+
+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.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.SizeContentProcessor;
+import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
+import org.apache.fineract.infrastructure.contentstore.util.ContentPathRandomizer;
+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.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";
+
+ @Deprecated
+ private final List imageIdAdapters;
+ private final ContentStoreService storeService;
+ private final ContentPathRandomizer pathRandomizer;
+ private final ImageRepository imageRepository;
+ private final Base64EncoderContentProcessor base64EncoderContentProcessor;
+ private final Base64DecoderContentProcessor base64DecoderContentProcessor;
+ private final DataUrlEncoderContentProcessor dataUrlEncoderContentProcessor;
+ private final DataUrlDecoderContentProcessor dataUrlDecoderContentProcessor;
+ private final SizeContentProcessor sizeContentProcessor;
+
+ @Transactional
+ @Override
+ public ImageCreateResponse createImage(final ImageCreateRequest request) {
+ 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())) {
+ //
+ request.setFileName(UUID.randomUUID().toString());
+ }
+
+ // TODO: make "prefix" configurable?
+ var path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), pathRandomizer.randomize(), request.getFileName(),
+ storeService.getDelimiter());
+
+ final var ctx = dataUrlDecoderContentProcessor.then(base64DecoderContentProcessor).then(sizeContentProcessor)
+ .process(new ContentProcessorContext(request.getStream()));
+
+ final var imagePath = storeService.upload(path, ctx.getInputStream());
+
+ // TODO: after we uploaded the file we can extract the size and type information
+ // final Long size = ctx.getResult(SIZE_RESULT_VALUE, -1L);
+ // final String type = ctx.getResult(DATA_URL_DECODE_RESULT_CONTENT_TYPE);
+
+ // TODO: refactor later then we can get rid of the image ID adapters
+ // .entitId(request.getEntityId())
+ // .entityType(request.getEntityType())
+
+ final var result = imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(request.getEntityType())).findFirst()
+ .flatMap(imageIdAdapter -> imageIdAdapter.get(request.getEntityId()))
+ .flatMap(imageIdResult -> imageRepository.findById(imageIdResult.getId())).or(() -> Optional.of(new Image()))
+ .map(image -> image.setLocation(imagePath).setStorageType(storeService.getType().getValue()))
+ .map(imageRepository::saveAndFlush).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));
+ }
+
+ @Transactional
+ @Override
+ public ImageDeleteResponse deleteImage(final ImageDeleteRequest request) {
+ return delete(request.getEntityType(), request.getEntityId())
+ .orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), -1L));
+ }
+
+ private Optional delete(final String entityType, final Long entityId) {
+ return imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(entityType)).findFirst().flatMap(imageIdAdapter -> {
+ final var result = imageIdAdapter.get(entityId);
+
+ // first we have to delete the constraint
+ imageIdAdapter.set(entityId);
+
+ return result;
+ }).filter(imageIdResult -> nonNull(imageIdResult.getId())).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 path, final String fileName,
+ String delimiter) {
+ requireNonNull(prefix);
+ requireNonNull(entityType);
+ requireNonNull(entityId);
+ requireNonNull(path);
+ requireNonNull(fileName);
+ requireNonNull(delimiter);
+
+ return String.join(delimiter, prefix, entityType, String.valueOf(entityId), path, 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 dc0aca7d434..0993476dcc3 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 b323857e7ae..1c9c5bb9bcd 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 14184b59e20..fb2d4c47e32 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/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 00000000000..48fb916f7e4
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/TestConfiguration.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;
+
+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" })
+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 00000000000..1c683082831
--- /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 00000000000..0c7bfe6338b
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/detector/TikaContentDetectorTest.java
@@ -0,0 +1,83 @@
+/**
+ * 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 java.io.FileOutputStream;
+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 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")).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")).build());
+
+ assertEquals("jpg", ctx.getFormat());
+ assertEquals(".jpg", ctx.getExtension());
+ assertEquals("image/jpeg", ctx.getMimeType());
+
+ write(ctx);
+ }
+
+ 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/processor/ContentProcessorTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorTest.java
new file mode 100644
index 00000000000..f42151712fc
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/contentstore/processor/ContentProcessorTest.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 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 dataUrlEncode() {
+ final var ctx = base64EncoderContentProcessor.then(dataUrlEncoderContentProcessor).then(sizeContentProcessor)
+ .process(new ContentProcessorContext(ContentProcessorTest.class.getClassLoader().getResourceAsStream("test.png"),
+ Map.of(DATA_URL_ENCODE_PARAM_CONTENT_TYPE, "image/png", DATA_URL_ENCODE_PARAM_ENCODING, "base64")));
+
+ write(ctx, "data-url-encode.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/resources/application-test.properties b/fineract-document/src/test/resources/application-test.properties
new file mode 100644
index 00000000000..075fd5a536e
--- /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.jpg b/fineract-document/src/test/resources/test.jpg
new file mode 100644
index 00000000000..1c61cde77f2
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 00000000000..227c547ac51
Binary files /dev/null and b/fineract-document/src/test/resources/test.png differ
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 d0c5bebff83..35c4744108c 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.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.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 @@
@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 String retrieveImportDocuments(@Context final UriInfo uriInfo, @QueryPara
@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 95f7e9c0cca..1d115148d39 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 259aac1c3b7..48e75cd00d6 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.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 static ImportDocument instance(final Document document, final LocalDateTi
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 void update(final LocalDateTime endTime, final Integer successCount, fina
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 00000000000..6c79d076e4a
--- /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 2597c7f7836..b289b2a6080 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 void onApplicationEvent(final BulkImportEvent event) {
throw new GeneralPlatformDomainRuleException("error.msg.unable.to.find.resource", "Unable to find requested resource");
};
- final Workbook workbook = event.getWorkbook();
- final Count count = importHandler.process(workbook, event.getLocale(), event.getDateFormat());
- importDocument.update(DateUtils.getLocalDateTimeOfTenant(), count.getSuccessCount(), count.getErrorCount());
- this.importRepository.saveAndFlush(importDocument);
+ final var count = importHandler.process(event.getWorkbook(), event.getLocale(), event.getDateFormat());
+
+ event.getImportDocument().update(DateUtils.getLocalDateTimeOfTenant(), count.getSuccessCount(), count.getErrorCount());
+
+ final var pipedInputStream = pipe.pipe(output -> {
+ event.getWorkbook().write(output);
+ });
- final Set modifiedParams = new HashSet<>();
- modifiedParams.add("fileName");
- modifiedParams.add("size");
- modifiedParams.add("type");
- modifiedParams.add("location");
- Document document = importDocument.getDocument();
+ final var command = new DocumentUpdateCommand();
- DocumentCommand documentCommand = new DocumentCommand(modifiedParams, document.getId(), entityType.name(), null,
- document.getName(), document.getFileName(), document.getSize(),
- URLConnection.guessContentTypeFromName(document.getFileName()), null, null);
+ command.setPayload(DocumentUpdateRequest.builder().id(event.getImportDocument().getDocumentId()).entityId(event.getEntityId())
+ .entityType("IMPORT").stream(pipedInputStream).build());
- 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 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 7aea2ed0f3a..78cf8d35cd0 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 Long importWorkbook(String entity, InputStream inputStream, FormDataConte
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 var command = new DocumentCreateCommand();
+
+ // 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);
- final String fileName = fileDetail.getFileName();
+ importDocument.setDocumentId(response.getResourceId());
- 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: 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 Collection getImports(GlobalEntityType type) {
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 ImportData mapRow(final ResultSet rs, @SuppressWarnings("unused") final i
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 438923e6f42..cea6f6922cd 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
@@ -201,6 +201,92 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.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 50bdb5975ea..0bb82fa9506 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 @@ protected void configure() {
@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 e164e5ef0c1..00000000000
--- 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 09f75bcac71..00000000000
--- 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 a2b68d1cfb7..3326b571ba5 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 boolean canSerialize(BusinessEvent event) {
@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 00000000000..6e3fa763b8b
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/adapter/StaffImageIdAdapter.java
@@ -0,0 +1,62 @@
+/**
+ * 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 void set(Long entityId, Long imageId) {
+ if (imageId == null) {
+ repository.removeImageId(entityId);
+ } else {
+ repository.updateByIdAndImageId(entityId, imageId);
+ }
+ }
+}
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 2f8c20410e9..3c6b10fdd8b 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 @@
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 56df5843537..f8849cfaf32 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 @@
@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 6bcd28663c1..7fb76cd4466 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.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.TikaContentDetector;
+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.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 TikaContentDetector tikaContentDetector;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@@ -149,7 +186,20 @@ public Response retrieveImage(@PathParam("clientId") @Parameter(description = "c
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 = tikaContentDetector.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 @@ private void validateAppuserClientsMapping(final Long clientId) {
@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 93c249625cd..1aac22a798f 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:}
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 8bf5bf31d6f..5ab60d471cd 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 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 @@
@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 @@ void documentStorageTypeIsPatchedIntoAvro() {
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 @@ void documentStorageTypeIsPatchedIntoAvro() {
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/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 00000000000..bda35b8faac
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/LocalContentStorageUtil.java
@@ -0,0 +1,83 @@
+/**
+ * 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 (!path.startsWith("/")) {
+ path = "/" + path;
+ }
+
+ if (Files.exists(Path.of(path))) {
+ return path;
+ }
+
+ // github actions
+ if (Files.exists(Path.of("/home/runner/.fineract/DefaultDemoTenant" + path))) {
+ return "/home/runner/.fineract/DefaultDemoTenant" + path;
+ }
+
+ // local docker volumes
+ path = Path.of("").toAbsolutePath().getParent() + "/build/fineract/tmp/DefaultDemoTenant" + path;
+
+ if (Files.exists(Path.of(path))) {
+ return path;
+ }
+
+ 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 236854a00c3..c1160fc23cb 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 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 void testClientImport() throws InterruptedException, IOException, ParseEx
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 309301a8429..fe51ff16948 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.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 void testLoanImport() throws InterruptedException, IOException, ParseExce
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 d8fb36acf65..664af14acac 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
@@ -33,6 +33,7 @@
import java.util.Locale;
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;
@@ -96,7 +97,7 @@ public void testOfficeImport() throws IOException, InterruptedException, NoSuchF
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);
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 a1f35aad230..13ea0d18fbc 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.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 void testSavingsImport() throws InterruptedException, IOException, ParseE
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/FeignDocumentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java
index b5a322d2d5f..9819ebeca6d 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 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 @@ void testCreateDocument() throws IOException {
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();
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 a0a1e3593ee..7b3bbea7135 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 static String performServerTemplatePost(final RequestSpecification reques
@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)