FINERACT-2246: New command processing - document management
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
8a61e30a45
commit
2ff0c62f40
@@ -114,6 +114,23 @@ dependencyManagement {
|
||||
exclude 'org.apache.commons:commons-compress'
|
||||
exclude 'xml-apis:xml-apis'
|
||||
}
|
||||
dependency ('org.apache.tika:tika-parser-image-module:3.2.3') {
|
||||
exclude 'org.bouncycastle:bcprov-jdk15on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk15on'
|
||||
exclude 'org.bouncycastle:bcprov-jdk18on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk18on'
|
||||
exclude 'commons-logging:commons-logging'
|
||||
exclude 'org.apache.logging.log4j:log4j-api'
|
||||
exclude 'org.slf4j:slf4j-api'
|
||||
exclude 'commons-io:commons-io'
|
||||
exclude 'commons-codec:commons-codec'
|
||||
exclude 'org.apache.commons:commons-lang3'
|
||||
exclude 'org.apache.poi:poi'
|
||||
exclude 'org.apache.poi:poi-scratchpad'
|
||||
exclude 'org.glassfish.jaxb:jaxb-runtime'
|
||||
exclude 'org.apache.commons:commons-compress'
|
||||
exclude 'xml-apis:xml-apis'
|
||||
}
|
||||
dependency ('org.apache.httpcomponents:httpclient:4.5.14') {
|
||||
exclude 'commons-logging:commons-logging:'
|
||||
}
|
||||
@@ -265,6 +282,12 @@ dependencyManagement {
|
||||
|
||||
dependency 'com.lmax:disruptor:3.4.4'
|
||||
|
||||
dependency ('com.github.romankh3:image-comparison:4.4.0') {
|
||||
exclude 'org.junit.jupiter:junit-jupiter-api'
|
||||
exclude 'org.junit.jupiter:junit-jupiter-engine'
|
||||
exclude 'org.mockito:mockito-core'
|
||||
}
|
||||
|
||||
dependency 'com.ibm.icu:icu4j:76.1'
|
||||
dependency 'org.yakworks:spring-icu4j:0.4.2'
|
||||
dependency 'org.apache.commons:commons-lang3:3.18.0'
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-1
@@ -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"
|
||||
|
||||
-1
@@ -34,7 +34,6 @@
|
||||
<!-- Core Module Entities -->
|
||||
<class>org.apache.fineract.useradministration.domain.Role</class>
|
||||
<class>org.apache.fineract.portfolio.fund.domain.Fund</class>
|
||||
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Image</class>
|
||||
<class>org.apache.fineract.organisation.workingdays.domain.WorkingDays</class>
|
||||
<class>org.apache.fineract.useradministration.domain.Permission</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUserClientMapping</class>
|
||||
|
||||
-1
@@ -34,7 +34,6 @@
|
||||
<!-- Core Module Entities -->
|
||||
<class>org.apache.fineract.useradministration.domain.Role</class>
|
||||
<class>org.apache.fineract.portfolio.fund.domain.Fund</class>
|
||||
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Image</class>
|
||||
<class>org.apache.fineract.organisation.workingdays.domain.WorkingDays</class>
|
||||
<class>org.apache.fineract.useradministration.domain.Permission</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUserClientMapping</class>
|
||||
|
||||
-1
@@ -34,7 +34,6 @@
|
||||
<!-- Core Module Entities -->
|
||||
<class>org.apache.fineract.useradministration.domain.Role</class>
|
||||
<class>org.apache.fineract.portfolio.fund.domain.Fund</class>
|
||||
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Image</class>
|
||||
<class>org.apache.fineract.organisation.workingdays.domain.WorkingDays</class>
|
||||
<class>org.apache.fineract.useradministration.domain.Permission</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUserClientMapping</class>
|
||||
|
||||
+6
-6
@@ -22,10 +22,10 @@ import feign.Param;
|
||||
import feign.RequestLine;
|
||||
import feign.Response;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentCreateResponse;
|
||||
import org.apache.fineract.client.models.DocumentData;
|
||||
import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentDeleteResponse;
|
||||
import org.apache.fineract.client.models.DocumentUpdateResponse;
|
||||
|
||||
/**
|
||||
* This class was originally generated by <a href="https://openapi-generator.tech">OpenAPI Generator</a>, but then had
|
||||
@@ -44,7 +44,7 @@ public interface DocumentsApiFixed {
|
||||
* @return PostEntityTypeEntityIdDocumentsResponse
|
||||
*/
|
||||
@RequestLine("POST /v1/{entityType}/{entityId}/documents")
|
||||
PostEntityTypeEntityIdDocumentsResponse createDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
DocumentCreateResponse createDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
|
||||
|
||||
/**
|
||||
@@ -59,7 +59,7 @@ public interface DocumentsApiFixed {
|
||||
* @return DeleteEntityTypeEntityIdDocumentsResponse
|
||||
*/
|
||||
@RequestLine("DELETE /v1/{entityType}/{entityId}/documents/{documentId}")
|
||||
DeleteEntityTypeEntityIdDocumentsResponse deleteDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
DocumentDeleteResponse deleteDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
@Param("documentId") Long documentId);
|
||||
|
||||
/**
|
||||
@@ -114,6 +114,6 @@ public interface DocumentsApiFixed {
|
||||
* @return PutEntityTypeEntityIdDocumentsResponse
|
||||
*/
|
||||
@RequestLine("PUT /v1/{entityType}/{entityId}/documents/{documentId}")
|
||||
PutEntityTypeEntityIdDocumentsResponse updateDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
DocumentUpdateResponse updateDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
@Param("documentId") Long documentId, org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
|
||||
}
|
||||
|
||||
+2
-3
@@ -35,7 +35,6 @@ import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.client.feign.services.DocumentsApiFixed;
|
||||
import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -149,7 +148,7 @@ class DocumentsApiFixedIntegrationTest {
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("clients", 123L, 456L);
|
||||
var response = api.deleteDocument("clients", 123L, 456L);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isEqualTo(456L);
|
||||
@@ -162,7 +161,7 @@ class DocumentsApiFixedIntegrationTest {
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("loans", 789L, 999L);
|
||||
var response = api.deleteDocument("loans", 789L, 999L);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isEqualTo(999L);
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-6
@@ -20,10 +20,10 @@ package org.apache.fineract.client.services;
|
||||
|
||||
import java.util.List;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentCreateResponse;
|
||||
import org.apache.fineract.client.models.DocumentData;
|
||||
import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentDeleteResponse;
|
||||
import org.apache.fineract.client.models.DocumentUpdateResponse;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
@@ -54,7 +54,7 @@ public interface DocumentsApiFixed {
|
||||
*/
|
||||
@retrofit2.http.Multipart
|
||||
@POST("v1/{entityType}/{entityId}/documents")
|
||||
Call<PostEntityTypeEntityIdDocumentsResponse> createDocument(@retrofit2.http.Path("entityType") String entityType,
|
||||
Call<DocumentCreateResponse> createDocument(@retrofit2.http.Path("entityType") String entityType,
|
||||
@retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Part okhttp3.MultipartBody.Part file,
|
||||
@retrofit2.http.Part("name") String name, @retrofit2.http.Part("description") String description);
|
||||
|
||||
@@ -70,7 +70,7 @@ public interface DocumentsApiFixed {
|
||||
* @return Call<DeleteEntityTypeEntityIdDocumentsResponse>
|
||||
*/
|
||||
@DELETE("v1/{entityType}/{entityId}/documents/{documentId}")
|
||||
Call<DeleteEntityTypeEntityIdDocumentsResponse> deleteDocument(@retrofit2.http.Path("entityType") String entityType,
|
||||
Call<DocumentDeleteResponse> deleteDocument(@retrofit2.http.Path("entityType") String entityType,
|
||||
@retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Path("documentId") Long documentId);
|
||||
|
||||
/**
|
||||
@@ -139,7 +139,7 @@ public interface DocumentsApiFixed {
|
||||
*/
|
||||
@retrofit2.http.Multipart
|
||||
@PUT("v1/{entityType}/{entityId}/documents/{documentId}")
|
||||
Call<PutEntityTypeEntityIdDocumentsResponse> updateDocument(@retrofit2.http.Path("entityType") String entityType,
|
||||
Call<DocumentUpdateResponse> 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);
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
<!-- Core Module Entities -->
|
||||
<class>org.apache.fineract.useradministration.domain.Role</class>
|
||||
<class>org.apache.fineract.portfolio.fund.domain.Fund</class>
|
||||
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Image</class>
|
||||
<class>org.apache.fineract.organisation.workingdays.domain.WorkingDays</class>
|
||||
<class>org.apache.fineract.useradministration.domain.Permission</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUserClientMapping</class>
|
||||
|
||||
+18
-2
@@ -114,6 +114,7 @@ public class DefaultCommandAuditor implements CommandAuditor {
|
||||
final var now = Instant.now();
|
||||
final var commandEntity = mapper.map(command);
|
||||
|
||||
commandEntity.setExecutedAt(now);
|
||||
commandEntity.setUpdatedAt(now);
|
||||
commandEntity.setState(ERROR);
|
||||
|
||||
@@ -138,7 +139,17 @@ public class DefaultCommandAuditor implements CommandAuditor {
|
||||
|
||||
@Retry(name = "commandAuditFallback", fallbackMethod = "fatal")
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private void fallback(Command<?> command, Throwable t) throws Exception {
|
||||
void fallback(Command<?> command, Throwable t) throws Exception {
|
||||
if (Boolean.TRUE.equals(properties.getFileDeadLetterQueueEnabled())) {
|
||||
fallback(command, null, t);
|
||||
} else {
|
||||
fatal(command, t);
|
||||
}
|
||||
}
|
||||
|
||||
@Retry(name = "commandAuditFallback", fallbackMethod = "fatal")
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
void fallback(Command<?> command, Object response, Throwable t) throws Exception {
|
||||
if (Boolean.TRUE.equals(properties.getFileDeadLetterQueueEnabled())) {
|
||||
setError(command, t);
|
||||
write(command);
|
||||
@@ -148,7 +159,12 @@ public class DefaultCommandAuditor implements CommandAuditor {
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private void fatal(Command<?> command, Throwable t) {
|
||||
void fatal(Command<?> command, Throwable t) {
|
||||
fatal(command, null, t);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
void fatal(Command<?> command, Object response, Throwable t) {
|
||||
// note: last line of defense; if this fails then all is lost
|
||||
setError(command, t);
|
||||
log.error("Command audit error: {}", command, t);
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
|
||||
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
|
||||
|
||||
@Configuration
|
||||
@EnableJdbcRepositories(basePackages = { "org.apache.fineract.command.persistence" })
|
||||
@EnableJdbcRepositories(basePackages = { "org.apache.fineract.**.domain", "org.apache.fineract.**.persistence" })
|
||||
@ComponentScan("org.apache.fineract.command.persistence")
|
||||
class CommandPersistenceConfiguration {
|
||||
|
||||
|
||||
+8
@@ -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;
|
||||
|
||||
+1
-6
@@ -18,12 +18,10 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.bulkimport.service;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
|
||||
import org.apache.fineract.infrastructure.bulkimport.data.ImportData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
|
||||
public interface BulkImportWorkbookService {
|
||||
@@ -33,8 +31,5 @@ public interface BulkImportWorkbookService {
|
||||
|
||||
Collection<ImportData> getImports(GlobalEntityType type);
|
||||
|
||||
DocumentData getOutputTemplateLocation(String importDocumentId);
|
||||
|
||||
Response getOutputTemplate(String importDocumentId);
|
||||
|
||||
ImportData getImport(Long id);
|
||||
}
|
||||
|
||||
+1
@@ -364,6 +364,7 @@ public class FineractProperties {
|
||||
private List<String> regexWhitelist;
|
||||
private boolean mimeWhitelistEnabled;
|
||||
private List<String> mimeWhitelist;
|
||||
private Integer defaultBufferSize;
|
||||
private FineractContentFilesystemProperties filesystem;
|
||||
private FineractContentS3Properties s3;
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.adapter;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
// NOTE: this is a trick to decouple the entity image IDs from the image service
|
||||
@Deprecated
|
||||
public interface EntityImageIdAdapter {
|
||||
|
||||
boolean accept(String entityType);
|
||||
|
||||
Optional<ImageIdResult> get(Long entityId);
|
||||
|
||||
Optional<ImageIdResult> 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;
|
||||
}
|
||||
}
|
||||
-1
@@ -40,7 +40,6 @@ public class CurrencyUpdateCommandHandler implements CommandHandler<CurrencyUpda
|
||||
@Transactional
|
||||
@Override
|
||||
public CurrencyUpdateResponse handle(final Command<CurrencyUpdateRequest> command) {
|
||||
// NOTE: fallback method needs to be in the same class
|
||||
return writePlatformService.updateAllowedCurrencies(command.getPayload());
|
||||
}
|
||||
|
||||
|
||||
+2
-9
@@ -22,7 +22,6 @@ import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.time.LocalDate;
|
||||
@@ -33,7 +32,6 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
|
||||
@Getter
|
||||
@@ -81,9 +79,8 @@ public class Staff extends AbstractPersistableCustom<Long> {
|
||||
@JoinColumn(name = "organisational_role_parent_staff_id")
|
||||
private Staff organisationalRoleParentStaff;
|
||||
|
||||
@OneToOne(optional = true)
|
||||
@JoinColumn(name = "image_id")
|
||||
private Image image;
|
||||
@Column(name = "image_id")
|
||||
private Long imageId;
|
||||
|
||||
public static Staff fromJson(final Office staffOffice, final JsonCommand command) {
|
||||
|
||||
@@ -262,8 +259,4 @@ public class Staff extends AbstractPersistableCustom<Long> {
|
||||
return this.office;
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.adapter;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Deprecated
|
||||
class ClientImageIdAdapter implements EntityImageIdAdapter {
|
||||
|
||||
private static final String ENTITY_TYPE = "clients";
|
||||
|
||||
private final ClientRepository repository;
|
||||
|
||||
@Override
|
||||
public boolean accept(String entityType) {
|
||||
return ENTITY_TYPE.equalsIgnoreCase(entityType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<ImageIdResult> get(Long entityId) {
|
||||
return repository.findById(entityId).filter(client -> nonNull(client.getImageId()))
|
||||
.map(client -> ImageIdResult.builder().id(client.getImageId()).displayName(client.getDisplayName()).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Optional<ImageIdResult> set(Long entityId, Long imageId) {
|
||||
final var result = get(entityId);
|
||||
|
||||
if (imageId == null) {
|
||||
repository.removeImageId(entityId);
|
||||
} else {
|
||||
repository.updateByIdAndImageId(entityId, imageId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+2
-16
@@ -27,7 +27,6 @@ import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.time.LocalDate;
|
||||
@@ -46,7 +45,6 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
|
||||
import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.apache.fineract.organisation.staff.domain.Staff;
|
||||
@@ -72,13 +70,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@JoinColumn(name = "transfer_to_office_id")
|
||||
private Office transferToOffice;
|
||||
|
||||
@OneToOne(optional = true)
|
||||
@JoinColumn(name = "image_id")
|
||||
private Image image;
|
||||
@Column(name = "image_id")
|
||||
private Long imageId;
|
||||
|
||||
/**
|
||||
* A value from {@link ClientStatus}.
|
||||
*/
|
||||
@Column(name = "status_enum", nullable = false)
|
||||
private Integer status;
|
||||
|
||||
@@ -494,10 +488,6 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.office.getId();
|
||||
}
|
||||
|
||||
public void setImage(final Image image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public String mobileNo() {
|
||||
return this.mobileNo;
|
||||
}
|
||||
@@ -510,10 +500,6 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.mobileNo = mobileNo;
|
||||
}
|
||||
|
||||
public boolean isNotStaff() {
|
||||
return !isStaff();
|
||||
}
|
||||
|
||||
public boolean isStaff() {
|
||||
return this.isStaff;
|
||||
}
|
||||
|
||||
+8
@@ -22,6 +22,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.portfolio.client.domain.search.SearchingClientRepository;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
@@ -46,4 +47,11 @@ public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecif
|
||||
@Query("SELECT c.id FROM Client c WHERE c.externalId = :externalId")
|
||||
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Client client SET client.imageId = :imageId WHERE client.id = :clientId")
|
||||
void updateByIdAndImageId(@Param("clientId") Long staffId, @Param("imageId") Long imageId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId")
|
||||
void removeImageId(@Param("clientId") Long clientId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.util;
|
||||
|
||||
import jakarta.ws.rs.container.AsyncResponse;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.StreamingOutput;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@Slf4j
|
||||
public final class StreamResponseUtil {
|
||||
|
||||
public static String DISPOSITION_TYPE_ATTACHMENT = "attachment";
|
||||
public static String DISPOSITION_TYPE_INLINE = "inline";
|
||||
|
||||
private static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
private StreamResponseUtil() {}
|
||||
|
||||
public static Response ok(final StreamResponseData content) {
|
||||
final var stream = new StreamingOutput() {
|
||||
|
||||
@Override
|
||||
public void write(OutputStream out) throws IOException {
|
||||
IOUtils.copy(content.getStream(), out);
|
||||
}
|
||||
};
|
||||
|
||||
if (StringUtils.isEmpty(content.getDispositionType())) {
|
||||
return Response.ok(stream, content.getType()).build();
|
||||
} else {
|
||||
return Response.ok(stream, content.getType()).header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
String.format("%s; filename=\"%s\"", content.getDispositionType(), content.getFileName())).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static Future<?> ok(final AsyncResponse asyncResponse, final StreamResponseData content) {
|
||||
return executor.submit(() -> {
|
||||
if (StringUtils.isEmpty(content.getDispositionType())) {
|
||||
asyncResponse.resume(Response.ok(content.getStream(), content.getType()).build());
|
||||
} else {
|
||||
asyncResponse.resume(Response.ok(content.getStream(), content.getType()).header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
String.format("%s; filename=\"%s\"", content.getDispositionType(), content.getFileName())).build());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static final class StreamResponseData {
|
||||
|
||||
private InputStream stream;
|
||||
private String type;
|
||||
private String fileName;
|
||||
private String dispositionType;
|
||||
private Long size;
|
||||
}
|
||||
}
|
||||
-1
@@ -34,7 +34,6 @@
|
||||
<!-- Core Module Entities -->
|
||||
<class>org.apache.fineract.useradministration.domain.Role</class>
|
||||
<class>org.apache.fineract.portfolio.fund.domain.Fund</class>
|
||||
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Image</class>
|
||||
<class>org.apache.fineract.organisation.workingdays.domain.WorkingDays</class>
|
||||
<class>org.apache.fineract.useradministration.domain.Permission</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUserClientMapping</class>
|
||||
|
||||
@@ -24,27 +24,25 @@ dependencies {
|
||||
|
||||
// implementation dependencies are directly used (compiled against) in src/main (and src/test)
|
||||
//
|
||||
implementation(project(path: ':fineract-core'))
|
||||
implementation(
|
||||
project(path: ':fineract-core'),
|
||||
project(path: ':fineract-command'),
|
||||
)
|
||||
|
||||
implementation(
|
||||
'org.springframework.boot:spring-boot-starter-web',
|
||||
'org.springframework.boot:spring-boot-starter-validation',
|
||||
'org.springframework.boot:spring-boot-starter-security',
|
||||
'org.springframework.boot:spring-boot-starter-data-jdbc',
|
||||
'jakarta.ws.rs:jakarta.ws.rs-api',
|
||||
'org.glassfish.jersey.media:jersey-media-multipart',
|
||||
|
||||
'com.google.guava:guava',
|
||||
'com.google.code.gson:gson',
|
||||
'com.fasterxml.jackson.core:jackson-databind',
|
||||
|
||||
'org.apache.commons:commons-lang3',
|
||||
|
||||
'com.jayway.jsonpath:json-path',
|
||||
|
||||
'com.github.spotbugs:spotbugs-annotations',
|
||||
'io.swagger.core.v3:swagger-annotations-jakarta',
|
||||
|
||||
'com.squareup.retrofit2:converter-gson',
|
||||
|
||||
'org.springdoc:springdoc-openapi-starter-webmvc-ui',
|
||||
'org.mapstruct:mapstruct',
|
||||
|
||||
'io.github.resilience4j:resilience4j-spring-boot3',
|
||||
@@ -53,6 +51,7 @@ dependencies {
|
||||
'org.apache.tika:tika-core',
|
||||
'org.apache.tika:tika-parser-microsoft-module',
|
||||
'org.apache.tika:tika-parser-miscoffice-module',
|
||||
'org.apache.tika:tika-parser-image-module',
|
||||
)
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
@@ -62,12 +61,6 @@ dependencies {
|
||||
}
|
||||
implementation ('software.amazon.awssdk:auth') {
|
||||
}
|
||||
implementation ('org.springframework.boot:spring-boot-starter-data-jpa') {
|
||||
exclude group: 'org.hibernate'
|
||||
}
|
||||
implementation('org.eclipse.persistence:org.eclipse.persistence.jpa') {
|
||||
exclude group: 'org.eclipse.persistence', module: 'jakarta.persistence'
|
||||
}
|
||||
// testCompile dependencies are ONLY used in src/test, not src/main.
|
||||
// Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly!
|
||||
//
|
||||
@@ -80,4 +73,5 @@ dependencies {
|
||||
exclude group: 'org.skyscreamer'
|
||||
}
|
||||
testImplementation ('org.mockito:mockito-inline')
|
||||
testImplementation 'com.github.romankh3:image-comparison'
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.config;
|
||||
|
||||
import static org.apache.fineract.infrastructure.contentstore.processor.ContentProcessor.BEAN_NAME_EXECUTOR;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
class ContentStoreConfig {
|
||||
|
||||
@Bean(BEAN_NAME_EXECUTOR)
|
||||
ExecutorService contentProcessorExecutor() {
|
||||
// TODO: make this more configurable?
|
||||
return Executors.newCachedThreadPool();
|
||||
}
|
||||
}
|
||||
+3
-19
@@ -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<Integer, StorageType> 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;
|
||||
}
|
||||
}
|
||||
+3
-10
@@ -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);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.detector;
|
||||
|
||||
import java.io.InputStream;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class ContentDetectorContext {
|
||||
|
||||
private InputStream inputStream;
|
||||
@Builder.Default
|
||||
private boolean inputStreamEnabled = false;
|
||||
private String fileName;
|
||||
private String mimeType;
|
||||
private String extension;
|
||||
private String format;
|
||||
|
||||
public ContentDetectorContext clone(String mimeType, String extension, String format) {
|
||||
return new ContentDetectorContext(this.inputStream, this.inputStreamEnabled, this.fileName, mimeType, extension, format);
|
||||
}
|
||||
|
||||
public ContentDetectorContext clone(InputStream inputStream, String mimeType, String extension, String format) {
|
||||
return new ContentDetectorContext(inputStream, this.inputStreamEnabled, this.fileName, mimeType, extension, format);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.detector;
|
||||
|
||||
public interface ContentDetectorManager {
|
||||
|
||||
ContentDetectorContext detect(ContentDetectorContext ctx);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.detector;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public final class DefaultContentDetectorManager implements ContentDetectorManager {
|
||||
|
||||
private final TikaContentDetector tikaContentDetector;
|
||||
|
||||
@Override
|
||||
public ContentDetectorContext detect(ContentDetectorContext ctx) {
|
||||
return tikaContentDetector.detect(ctx);
|
||||
}
|
||||
}
|
||||
+45
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.detector;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentDetectorException;
|
||||
import org.apache.tika.Tika;
|
||||
import org.apache.tika.io.TikaInputStream;
|
||||
import org.apache.tika.metadata.Metadata;
|
||||
import org.apache.tika.metadata.TikaCoreProperties;
|
||||
import org.apache.tika.mime.MimeTypeException;
|
||||
import org.apache.tika.mime.MimeTypes;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public final class TikaContentDetector implements ContentDetector {
|
||||
|
||||
private final Tika tika = new Tika();
|
||||
|
||||
@Override
|
||||
public ContentDetectorContext detect(ContentDetectorContext ctx) {
|
||||
if (ctx.getInputStream() != null && !ctx.isInputStreamEnabled()) {
|
||||
log.warn(
|
||||
"Input stream provided, but not explicitly enabled for detection. This operation is potentially making the input stream unusable, especially with HTTP multipart streams. Input stream will be ignored!");
|
||||
}
|
||||
|
||||
try {
|
||||
if (ctx.getInputStream() != null && ctx.isInputStreamEnabled()) {
|
||||
final var stream = TikaInputStream.get(ctx.getInputStream());
|
||||
final var metadata = new Metadata();
|
||||
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, ctx.getFileName());
|
||||
|
||||
final var mimeType = tika.detect(stream, metadata);
|
||||
final var extension = getExtension(mimeType);
|
||||
|
||||
stream.reset();
|
||||
|
||||
// note: original stream should be still usable if "mark" is supported
|
||||
return ctx.clone(mimeType, extension, extension.substring(1));
|
||||
} else if (StringUtils.isNotEmpty(ctx.getFileName())) {
|
||||
final var mimeType = tika.detect(ctx.getFileName());
|
||||
final var extension = getExtension(mimeType);
|
||||
|
||||
return ctx.clone(mimeType, extension, extension.substring(1));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ContentDetectorException(e);
|
||||
}
|
||||
|
||||
throw new ContentDetectorException(
|
||||
new IllegalArgumentException("Could not run detection, because required arguments were missing."));
|
||||
}
|
||||
|
||||
private String getExtension(String mimeType) {
|
||||
try {
|
||||
return Optional.ofNullable(MimeTypes.getDefaultMimeTypes().forName(mimeType)).orElseGet(() -> {
|
||||
try {
|
||||
return MimeTypes.getDefaultMimeTypes().forName(MimeTypes.OCTET_STREAM);
|
||||
} catch (MimeTypeException e) {
|
||||
throw new ContentDetectorException(e);
|
||||
}
|
||||
}).getExtension();
|
||||
} catch (Exception e) {
|
||||
throw new ContentDetectorException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformException;
|
||||
|
||||
public class ContentPolicyException extends AbstractPlatformException {
|
||||
|
||||
public ContentPolicyException(String message) {
|
||||
super("error.msg.content.policy", message);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DefaultDeleteContentPolicy implements ContentPolicy {
|
||||
|
||||
private final TraversalContentPolicy traversalContentPolicy;
|
||||
private final FineractProperties properties;
|
||||
|
||||
@Override
|
||||
public void check(ContentPolicyContext ctx) {
|
||||
traversalContentPolicy.check(ctx);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DefaultDownloadContentPolicy implements ContentPolicy {
|
||||
|
||||
private final TraversalContentPolicy traversalContentPolicy;
|
||||
private final FineractProperties properties;
|
||||
|
||||
@Override
|
||||
public void check(ContentPolicyContext ctx) {
|
||||
traversalContentPolicy.check(ctx);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DefaultPostUploadContentPolicy implements ContentPolicy {
|
||||
|
||||
private final MimeContentPolicy mimeContentPolicy;
|
||||
|
||||
@Override
|
||||
public void check(ContentPolicyContext ctx) {
|
||||
mimeContentPolicy.check(ctx);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DefaultPreUploadContentPolicy implements ContentPolicy {
|
||||
|
||||
private final WhitelistContentPolicy whitelistContentPolicy;
|
||||
private final TraversalContentPolicy traversalContentPolicy;
|
||||
|
||||
@Override
|
||||
public void check(ContentPolicyContext ctx) {
|
||||
traversalContentPolicy.check(ctx);
|
||||
whitelistContentPolicy.check(ctx);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class MimeContentPolicy implements ContentPolicy {
|
||||
|
||||
private final ContentDetectorManager contentDetectorManager;
|
||||
|
||||
@Override
|
||||
public void check(ContentPolicyContext ctx) {
|
||||
if (ctx.getInputStream() != null) {
|
||||
var result = contentDetectorManager
|
||||
.detect(ContentDetectorContext.builder().inputStream(ctx.getInputStream()).inputStreamEnabled(true).build());
|
||||
|
||||
if (!Strings.CI.equals(result.getMimeType(), ctx.getMimeType())) {
|
||||
throw new ContentPolicyException(String.format("Detected file type (%s), but mime type (%s) was provided. Mismatch!",
|
||||
result.getMimeType(), ctx.getMimeType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class TraversalContentPolicy implements ContentPolicy {
|
||||
|
||||
private final Pattern overwriteSiblingsPattern = Pattern.compile(".*((\\.)?\\./+)+(/+)?.*");
|
||||
|
||||
private final FineractProperties properties;
|
||||
|
||||
@Override
|
||||
public void check(ContentPolicyContext ctx) {
|
||||
if (overwriteSiblingsPattern.matcher(ctx.getPath()).matches()) {
|
||||
throw new ContentPolicyException(String.format("Trying to overwrite a sibling file: %s", ctx.getPath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class WhitelistContentPolicy implements ContentPolicy {
|
||||
|
||||
private final FineractProperties properties;
|
||||
|
||||
private List<Pattern> regexWhitelist;
|
||||
|
||||
@EventListener(ApplicationStartedEvent.class)
|
||||
void onStartup() {
|
||||
regexWhitelist = properties.getContent().getRegexWhitelist().stream().map(Pattern::compile).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(ContentPolicyContext ctx) {
|
||||
if (properties.getContent().isRegexWhitelistEnabled()) {
|
||||
final var fileName = FilenameUtils.getName(ctx.getPath());
|
||||
|
||||
boolean matches = regexWhitelist.stream().anyMatch(p -> p.matcher(fileName).matches());
|
||||
|
||||
if (!matches) {
|
||||
throw new ContentPolicyException(String.format("File name not allowed: %s", fileName));
|
||||
}
|
||||
}
|
||||
|
||||
if (properties.getContent().isMimeWhitelistEnabled()) {
|
||||
final var fileName = FilenameUtils.getName(ctx.getPath());
|
||||
|
||||
if (StringUtils.isEmpty(ctx.getMimeType())) {
|
||||
throw new ContentPolicyException(String.format("Could not detect mime type for filename %s!", fileName));
|
||||
}
|
||||
|
||||
if (!properties.getContent().getMimeWhitelist().contains(ctx.getMimeType())) {
|
||||
throw new ContentPolicyException(
|
||||
String.format("Detected mime type %s for filename %s not allowed!", ctx.getMimeType(), fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+59
@@ -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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
+76
@@ -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<String, Object> parameters;
|
||||
private final Map<String, Object> results;
|
||||
|
||||
public ContentProcessorContext(InputStream inputStream) {
|
||||
this(inputStream, Map.of(), new HashMap<>());
|
||||
}
|
||||
|
||||
public ContentProcessorContext(InputStream inputStream, Map<String, Object> parameters) {
|
||||
this(inputStream, parameters, new HashMap<>());
|
||||
}
|
||||
|
||||
ContentProcessorContext(InputStream inputStream, Map<String, Object> parameters, Map<String, Object> results) {
|
||||
this.inputStream = inputStream;
|
||||
this.parameters = parameters;
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
public <R> R getParameter(String key) {
|
||||
return getParameter(key, null);
|
||||
}
|
||||
|
||||
public <R> 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> R getResult(String key) {
|
||||
return getResult(key, null);
|
||||
}
|
||||
|
||||
public <R> 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);
|
||||
}
|
||||
}
|
||||
+153
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.processor;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.imageio.ImageIO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentProcessorException;
|
||||
import org.apache.fineract.infrastructure.contentstore.util.ContentPipe;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class ImageResizeContentProcessor implements ContentProcessor {
|
||||
|
||||
private static final String IMAGE_RESIZE_PREFIX = "image.resize.";
|
||||
|
||||
public static final String IMAGE_RESIZE_PARAM_MAX_WIDTH = IMAGE_RESIZE_PREFIX + "max-width";
|
||||
public static final String IMAGE_RESIZE_PARAM_MAX_HEIGHT = IMAGE_RESIZE_PREFIX + "max-height";
|
||||
public static final String IMAGE_RESIZE_PARAM_FORMAT = IMAGE_RESIZE_PREFIX + "format";
|
||||
|
||||
private final ContentPipe pipe;
|
||||
|
||||
private static final Pattern VALID_IMAGE_FORMATS = Pattern.compile("^(gif|jpg|jpeg|png)$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Override
|
||||
public ContentProcessorContext process(final ContentProcessorContext ctx) {
|
||||
final Integer maxWidth = ctx.getParameter(IMAGE_RESIZE_PARAM_MAX_WIDTH);
|
||||
final Integer maxHeight = ctx.getParameter(IMAGE_RESIZE_PARAM_MAX_HEIGHT);
|
||||
final String format = ctx.getParameter(IMAGE_RESIZE_PARAM_FORMAT, "jpg");
|
||||
|
||||
requireNonNull(maxWidth, "Missing max width parameter");
|
||||
requireNonNull(maxHeight, "Missing max height parameter");
|
||||
|
||||
if (!VALID_IMAGE_FORMATS.matcher(format).matches()) {
|
||||
throw new ContentProcessorException(String.format("Wrong image format parameter: %s", format));
|
||||
}
|
||||
|
||||
final var pipedInputStream = pipe.pipe(ctx.getInputStream(), (in, out) -> {
|
||||
final var image = resize(in, maxWidth, maxHeight);
|
||||
|
||||
// compresses the image into the specified format (e.g., "gif", "jpeg", "png")
|
||||
ImageIO.write(image, format, out);
|
||||
});
|
||||
|
||||
return ctx.clone(pipedInputStream);
|
||||
}
|
||||
|
||||
private BufferedImage resize(InputStream is, int targetWidth, int targetHeight) throws IOException {
|
||||
final var imageIs = ImageIO.createImageInputStream(StreamUtils.nonClosing(is));
|
||||
|
||||
final var readers = ImageIO.getImageReaders(imageIs);
|
||||
|
||||
if (!readers.hasNext()) {
|
||||
throw new ContentProcessorException("No image reader found for format");
|
||||
}
|
||||
|
||||
final var reader = readers.next();
|
||||
|
||||
try {
|
||||
reader.setInput(imageIs, true, true);
|
||||
|
||||
// get original dimensions to calculate subsampling ratio
|
||||
int originalWidth = reader.getWidth(0);
|
||||
int originalHeight = reader.getHeight(0);
|
||||
|
||||
// calculate subsampling ratio
|
||||
// subsampling is an integer ratio of source size to output size
|
||||
int subsampleX = Math.max(1, originalWidth / targetWidth);
|
||||
int subsampleY = Math.max(1, originalHeight / targetHeight);
|
||||
int subsampling = Math.max(subsampleX, subsampleY);
|
||||
|
||||
final var param = reader.getDefaultReadParam();
|
||||
param.setSourceSubsampling(subsampling, subsampling, 0, 0);
|
||||
|
||||
// image will be close to the target size, but not exact,
|
||||
// as subsampling only works with integer ratios. Further quality scaling
|
||||
// can be done on the smaller BufferedImage if exact dimensions are needed.
|
||||
|
||||
// read the image using the parameters
|
||||
return reader.read(0, param);
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
|
||||
|
||||
public interface ContentStoreService {
|
||||
|
||||
String EXTENSION_REGEX = "^.+\\..+$";
|
||||
String DELIMITER = "/";
|
||||
|
||||
InputStream download(String path);
|
||||
|
||||
String upload(String path, InputStream is, String mimeType);
|
||||
|
||||
void delete(String path);
|
||||
|
||||
ContentStoreType getType();
|
||||
|
||||
default String getDelimiter() {
|
||||
return DELIMITER;
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.service;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentStoreException;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.ContentPolicyContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultDeleteContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultDownloadContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultPostUploadContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultPreUploadContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.util.ContentPathRandomizer;
|
||||
import org.apache.fineract.infrastructure.contentstore.util.ContentPathSanitizer;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@ConditionalOnProperty(name = "fineract.content.filesystem.enabled", havingValue = "true")
|
||||
public class FileContentStoreService implements ContentStoreService {
|
||||
|
||||
private final ContentPathSanitizer pathSanitizer;
|
||||
private final ContentPathRandomizer pathRandomizer;
|
||||
private final DefaultDownloadContentPolicy downloadContentPolicy;
|
||||
private final DefaultPreUploadContentPolicy preUploadContentPolicy;
|
||||
private final DefaultPostUploadContentPolicy postUploadContentPolicy;
|
||||
private final DefaultDeleteContentPolicy deleteContentPolicy;
|
||||
private final ContentDetectorManager contentDetectorManager;
|
||||
private final FineractProperties properties;
|
||||
|
||||
@Override
|
||||
public InputStream download(String path) {
|
||||
downloadContentPolicy.check(ContentPolicyContext.builder().path(path).build());
|
||||
|
||||
final var safePath = pathSanitizer.sanitize(path);
|
||||
|
||||
try {
|
||||
final var target = getPath(safePath, false);
|
||||
|
||||
return Files.newInputStream(target);
|
||||
} catch (IOException e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(String path, InputStream is, String mimeType) {
|
||||
requireNonNull(path, "Path missing");
|
||||
requireNonNull(is, "Data missing");
|
||||
|
||||
if (StringUtils.isEmpty(mimeType)) {
|
||||
mimeType = Optional.ofNullable(contentDetectorManager.detect(ContentDetectorContext.builder().fileName(path).build()))
|
||||
.map(ContentDetectorContext::getMimeType).orElse(APPLICATION_OCTET_STREAM_VALUE);
|
||||
}
|
||||
|
||||
preUploadContentPolicy.check(ContentPolicyContext.builder().path(path).mimeType(mimeType).build());
|
||||
|
||||
var safePath = pathSanitizer.sanitize(path);
|
||||
|
||||
final var target = Files.exists(getPath(safePath, false)) ? getPath(safePath, false) : getPath(safePath, true);
|
||||
|
||||
try (var in = is) {
|
||||
IOUtils.copy(in, new FileOutputStream(target.toString()));
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
|
||||
final var relativePath = getRelativePath(target);
|
||||
|
||||
try {
|
||||
postUploadContentPolicy
|
||||
.check(ContentPolicyContext.builder().path(path).inputStream(getInputStream(target)).mimeType(mimeType).build());
|
||||
} catch (ContentPolicyException e) {
|
||||
log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", target, e.getMessage());
|
||||
|
||||
delete(relativePath.toString());
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return relativePath.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
deleteContentPolicy.check(ContentPolicyContext.builder().path(path).build());
|
||||
|
||||
final var safePath = pathSanitizer.sanitize(path);
|
||||
|
||||
try {
|
||||
final var target = getPath(safePath, false);
|
||||
|
||||
final boolean deleted = Files.deleteIfExists(target);
|
||||
|
||||
if (!hasFiles(target.getParent())) {
|
||||
Files.deleteIfExists(target.getParent());
|
||||
}
|
||||
|
||||
if (!deleted) {
|
||||
// no need to throw an Error, what's a caller going to do about it, so simply log a warning
|
||||
log.warn("Unable to delete file {}", safePath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentStoreType getType() {
|
||||
return ContentStoreType.FILE_SYSTEM;
|
||||
}
|
||||
|
||||
private Path getPath(String path, boolean randomize) {
|
||||
Path p;
|
||||
|
||||
if (randomize) {
|
||||
var tail = Path.of(path);
|
||||
var file = tail.getFileName();
|
||||
|
||||
p = getRootPath().resolve(tail.getParent()).resolve(pathRandomizer.randomize()).resolve(file);
|
||||
|
||||
try {
|
||||
if (Files.notExists(p.getParent())) {
|
||||
Files.createDirectories(p.getParent());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
} else {
|
||||
p = getRootPath().resolve(path);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private Path getRelativePath(Path fullPath) {
|
||||
return getRootPath().relativize(fullPath);
|
||||
}
|
||||
|
||||
private Path getRootPath() {
|
||||
return Path.of(properties.getContent().getFilesystem().getRootFolder(),
|
||||
ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim());
|
||||
}
|
||||
|
||||
private InputStream getInputStream(Path path) {
|
||||
try {
|
||||
return Files.newInputStream(path);
|
||||
} catch (IOException ioe) {
|
||||
throw new ContentStoreException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasFiles(Path folder) throws IOException {
|
||||
if (Files.exists(folder) && Files.isDirectory(folder)) {
|
||||
try (Stream<Path> entries = Files.list(folder)) {
|
||||
return entries.anyMatch(Files::isRegularFile);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.contentstore.service;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentPolicyException;
|
||||
import org.apache.fineract.infrastructure.contentstore.exception.ContentStoreException;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.ContentPolicyContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultDeleteContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultDownloadContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultPostUploadContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.policy.DefaultPreUploadContentPolicy;
|
||||
import org.apache.fineract.infrastructure.contentstore.util.ContentPathSanitizer;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.core.sync.ResponseTransformer;
|
||||
import software.amazon.awssdk.http.ContentStreamProvider;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@ConditionalOnProperty(name = "fineract.content.s3.enabled", havingValue = "true")
|
||||
public class S3ContentStoreService implements ContentStoreService {
|
||||
|
||||
private final S3Client s3Client;
|
||||
private final ContentPathSanitizer pathSanitizer;
|
||||
private final DefaultDownloadContentPolicy downloadContentPolicy;
|
||||
private final DefaultPreUploadContentPolicy preUploadContentPolicy;
|
||||
private final DefaultPostUploadContentPolicy postUploadContentPolicy;
|
||||
private final DefaultDeleteContentPolicy deleteContentPolicy;
|
||||
private final ContentDetectorManager contentDetectorManager;
|
||||
private final FineractProperties properties;
|
||||
|
||||
@Override
|
||||
public InputStream download(String path) {
|
||||
downloadContentPolicy.check(ContentPolicyContext.builder().path(path).build());
|
||||
|
||||
final var safePath = pathSanitizer.sanitize(path);
|
||||
|
||||
try {
|
||||
return s3Client
|
||||
.getObject(GetObjectRequest.builder().bucket(properties.getContent().getS3().getBucketName()).key(safePath).build(),
|
||||
ResponseTransformer.toBytes())
|
||||
.asInputStream();
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(String path, InputStream is, String mimeType) {
|
||||
requireNonNull(path, "Path missing");
|
||||
requireNonNull(is, "Data missing");
|
||||
|
||||
preUploadContentPolicy.check(ContentPolicyContext.builder().path(path).mimeType(mimeType).build());
|
||||
|
||||
var safePath = pathSanitizer.sanitize(path);
|
||||
|
||||
try {
|
||||
final var p = safePath;
|
||||
|
||||
s3Client.putObject(builder -> builder.bucket(properties.getContent().getS3().getBucketName()).key(p),
|
||||
RequestBody.fromContentProvider(ContentStreamProvider.fromInputStream(is), mimeType));
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: verify if this is working; maybe there is a better way (e.g. temporary storage on file system)
|
||||
postUploadContentPolicy
|
||||
.check(ContentPolicyContext.builder().path(path).inputStream(download(safePath)).mimeType(mimeType).build());
|
||||
} catch (ContentPolicyException e) {
|
||||
log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", safePath, e.getMessage());
|
||||
|
||||
delete(safePath);
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return safePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
deleteContentPolicy.check(ContentPolicyContext.builder().path(path).build());
|
||||
|
||||
final var safePath = pathSanitizer.sanitize(path);
|
||||
|
||||
try {
|
||||
s3Client.deleteObject(builder -> builder.bucket(properties.getContent().getS3().getBucketName()).key(safePath));
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentStoreType getType() {
|
||||
return ContentStoreType.S3;
|
||||
}
|
||||
}
|
||||
+24
@@ -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();
|
||||
}
|
||||
+1
-5
@@ -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);
|
||||
}
|
||||
+169
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.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);
|
||||
}
|
||||
}
|
||||
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
-61
@@ -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);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.api;
|
||||
|
||||
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";
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.api;
|
||||
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_DESCRIPTION;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_DOCUMENT_ID;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_ID;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_TYPE;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_FILE;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_NAME;
|
||||
import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_ATTACHMENT;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
|
||||
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.HeaderParam;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCreateCommand;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentDeleteCommand;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentUpdateCommand;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
|
||||
import org.apache.fineract.util.StreamResponseUtil;
|
||||
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@Path("/v1/{entityType}/{entityId}/documents")
|
||||
@Tag(name = "Documents", description = """
|
||||
Multiple Documents (a combination of a name, description and a file) may be attached to different entities like clients, groups, staff, loans, savings and client identifiers in the system
|
||||
|
||||
Note: the currently allowed entities are
|
||||
|
||||
- clients: URL pattern as clients
|
||||
- staff: URL pattern as staff
|
||||
- loans: URL pattern as loans
|
||||
- savings: URL pattern as savings
|
||||
- client identifiers: URL pattern as client_identifiers
|
||||
- groups: URL pattern as groups
|
||||
""")
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentApiResource {
|
||||
|
||||
private final DocumentReadPlatformService documentReadPlatformService;
|
||||
private final FileUploadValidator fileUploadValidator;
|
||||
private final ContentDetectorManager contentDetectorManager;
|
||||
private final CommandPipeline commandPipeline;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List documents", description = """
|
||||
Example Requests:
|
||||
|
||||
- clients/1/documents
|
||||
- client_identifiers/1/documents
|
||||
- loans/1/documents?fields=name,description
|
||||
""")
|
||||
public List<DocumentData> retrieveAllDocuments(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId) {
|
||||
|
||||
return documentReadPlatformService.retrieveAllDocuments(entityType, entityId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{documentId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve a Document", description = """
|
||||
Example Requests:
|
||||
|
||||
- clients/1/documents/1
|
||||
- loans/1/documents/1
|
||||
- client_identifiers/1/documents/1?fields=name,description
|
||||
""")
|
||||
public DocumentData getDocument(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId,
|
||||
@PathParam(DOCUMENT_API_PARAM_DOCUMENT_ID) final Long documentId) {
|
||||
|
||||
return documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{documentId}/attachment")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
|
||||
@Operation(summary = "Retrieve Binary File associated with Document", description = """
|
||||
Request used to download the file associated with the document
|
||||
|
||||
Example Requests:
|
||||
|
||||
- clients/1/documents/1/attachment
|
||||
- loans/1/documents/1/inline
|
||||
""")
|
||||
@ApiResponse(responseCode = "200", description = "Not Shown: The corresponding Binary file")
|
||||
public Response downloadFile(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId,
|
||||
@PathParam(DOCUMENT_API_PARAM_DOCUMENT_ID) final Long documentId) {
|
||||
|
||||
final var content = documentReadPlatformService.retrieveDocumentContent(entityType, entityId, documentId);
|
||||
|
||||
return StreamResponseUtil
|
||||
.ok(StreamResponseUtil.StreamResponseData.builder().type(content.getContentType()).fileName(content.getFileName())
|
||||
.dispositionType(DISPOSITION_TYPE_ATTACHMENT).stream(content.getStream()).size(content.getSize()).build());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.MULTIPART_FORM_DATA })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create a Document", description = """
|
||||
Note: A document is created using a Multi-part form upload
|
||||
|
||||
Body parts
|
||||
|
||||
- name : name or summary of the document
|
||||
- description : description of the document
|
||||
- file : the file to be uploaded
|
||||
|
||||
Mandatory fields :
|
||||
|
||||
- file
|
||||
- description
|
||||
""")
|
||||
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)")
|
||||
public DocumentCreateResponse createDocument(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @HeaderParam(CONTENT_LENGTH) final Long fileSize,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream is,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart filePart,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_NAME) final String name,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_DESCRIPTION) final String description) {
|
||||
|
||||
fileUploadValidator.validate(fileSize, is, fileDetails, filePart);
|
||||
|
||||
final var command = new DocumentCreateCommand();
|
||||
|
||||
var type = Optional.ofNullable(filePart.getMediaType()).map(MediaType::toString)
|
||||
.or(() -> Optional.of(contentDetectorManager
|
||||
.detect(ContentDetectorContext.builder().fileName(fileDetails.getFileName()).build()).getMimeType()))
|
||||
.orElse(APPLICATION_OCTET_STREAM_VALUE);
|
||||
|
||||
command.setPayload(DocumentCreateRequest.builder().entityId(entityId).entityType(entityType).name(name).description(description)
|
||||
.fileName(fileDetails.getFileName()).size(fileSize).type(type).stream(is).build());
|
||||
|
||||
final Supplier<DocumentCreateResponse> 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<DocumentUpdateResponse> 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<DocumentDeleteResponse> response = commandPipeline.send(command);
|
||||
|
||||
return response.get();
|
||||
}
|
||||
}
|
||||
-249
@@ -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<DocumentData> 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<String> modifiedParams = new HashSet<>();
|
||||
modifiedParams.add("name");
|
||||
modifiedParams.add("description");
|
||||
|
||||
/***
|
||||
* Populate Document command based on whether a file has also been passed in as a part of the update
|
||||
***/
|
||||
DocumentCommand documentCommand;
|
||||
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);
|
||||
}
|
||||
}
|
||||
-111
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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";
|
||||
}
|
||||
+151
-151
@@ -18,9 +18,30 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.api;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_CONTENT_TYPE;
|
||||
import static org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor.DATA_URL_ENCODE_PARAM_ENCODING;
|
||||
import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_FORMAT;
|
||||
import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_HEIGHT;
|
||||
import static org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor.IMAGE_RESIZE_PARAM_MAX_WIDTH;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_ID;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_ENTITY_TYPE;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_FILE;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_PARAM_MAX_HEIGHT;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_PARAM_MAX_WIDTH;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_PARAM_OUTPUT;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_VALUE_ENCODING_BASE64;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.ImageApiConstants.IMAGE_API_VALUE_OUTPUT_INLINE_OCTET;
|
||||
import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_ATTACHMENT;
|
||||
import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_INLINE;
|
||||
import static org.springframework.http.HttpHeaders.ACCEPT;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
|
||||
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -33,196 +54,175 @@ import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.UploadRequest;
|
||||
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils.ImageFileExtension;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageResizer;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.InvalidEntityTypeForImageManagementException;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.apache.fineract.infrastructure.contentstore.processor.Base64DecoderContentProcessor;
|
||||
import org.apache.fineract.infrastructure.contentstore.processor.Base64EncoderContentProcessor;
|
||||
import org.apache.fineract.infrastructure.contentstore.processor.ContentProcessorContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.processor.DataUrlDecoderContentProcessor;
|
||||
import org.apache.fineract.infrastructure.contentstore.processor.DataUrlEncoderContentProcessor;
|
||||
import org.apache.fineract.infrastructure.contentstore.processor.ImageResizeContentProcessor;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.ImageCreateCommand;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.ImageDeleteCommand;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.ImageReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.ImageWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.util.StreamResponseUtil;
|
||||
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
// NOTE: left for backward compatibility only, could be unified with documents
|
||||
@Deprecated
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Path("/v1/{entity}/{entityId}/images")
|
||||
@Path("/v1/{entityType}/{entityId}/images")
|
||||
public class ImagesApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ImageReadPlatformService imageReadPlatformService;
|
||||
private final ImageWritePlatformService imageWritePlatformService;
|
||||
private final CommandPipeline commandPipeline;
|
||||
private final ImageResizeContentProcessor imageResizeContentProcessor;
|
||||
private final Base64EncoderContentProcessor base64EncoderContentProcessor;
|
||||
private final DataUrlEncoderContentProcessor dataUrlEncoderContentProcessor;
|
||||
private final Base64DecoderContentProcessor base64DecoderContentProcessor;
|
||||
private final DataUrlDecoderContentProcessor dataUrlDecoderContentProcessor;
|
||||
private final FileUploadValidator fileUploadValidator;
|
||||
private final ImageResizer imageResizer;
|
||||
|
||||
/**
|
||||
* Upload images through multi-part form upload
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@RequestBody(description = "Upload new client image", content = {
|
||||
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
|
||||
public CommandProcessingResult addNewClientImage(@PathParam("entity") final String entityName,
|
||||
@PathParam("entityId") final Long entityId, @HeaderParam("Content-Length") final Long fileSize,
|
||||
@FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails,
|
||||
@FormDataParam("file") final FormDataBodyPart bodyPart) {
|
||||
validateEntityTypeforImage(entityName);
|
||||
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
|
||||
// TODO: vishwas might need more advances validation (like reading magic
|
||||
// number) for handling malicious clients
|
||||
// and clients not setting mime type
|
||||
ContentRepositoryUtils.validateClientImageNotEmpty(fileDetails.getFileName());
|
||||
ContentRepositoryUtils.validateImageMimeType(bodyPart.getMediaType().toString());
|
||||
|
||||
return imageWritePlatformService.saveOrUpdateImage(entityName, entityId, fileDetails.getFileName(), inputStream, fileSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload image as a Data URL (essentially a base64 encoded stream)
|
||||
*/
|
||||
@POST
|
||||
@Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CommandProcessingResult addNewClientImage(@PathParam("entity") final String entityName,
|
||||
@PathParam("entityId") final Long entityId, final String jsonRequestBody) {
|
||||
validateEntityTypeforImage(entityName);
|
||||
|
||||
final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody);
|
||||
|
||||
return imageWritePlatformService.saveOrUpdateImage(entityName, entityId, base64EncodedImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a images, either as Base64 encoded text/plain or as inline or attachment with image MIME type as
|
||||
* Content-Type.
|
||||
*/
|
||||
@GET
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
// FINERACT-1265: Do NOT specify @Produces(TEXT_PLAIN) here - it may actually not (if it calls the next methods it's
|
||||
// octet-stream)
|
||||
public Response retrieveImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId,
|
||||
@QueryParam("maxWidth") final Integer maxWidth, @QueryParam("maxHeight") final Integer maxHeight,
|
||||
@QueryParam("output") final String output, @HeaderParam("Accept") String acceptHeader) {
|
||||
validateEntityTypeforImage(entityName);
|
||||
if (EntityTypeForImages.CLIENTS.toString().equalsIgnoreCase(entityName)) {
|
||||
context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE");
|
||||
} else if (EntityTypeForImages.STAFF.toString().equalsIgnoreCase(entityName)) {
|
||||
context.authenticatedUser().validateHasReadPermission("STAFFIMAGE");
|
||||
public Response retrieveImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityName,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @QueryParam(IMAGE_API_PARAM_MAX_WIDTH) final Integer maxWidth,
|
||||
@QueryParam(IMAGE_API_PARAM_MAX_HEIGHT) final Integer maxHeight, @QueryParam(IMAGE_API_PARAM_OUTPUT) String output,
|
||||
@HeaderParam(ACCEPT) String acceptHeader) {
|
||||
|
||||
// TODO: pass resize information here and do all the processing in the service
|
||||
final var content = imageReadPlatformService.retrieveImage(entityName, entityId);
|
||||
|
||||
String dispositionType = null;
|
||||
ContentProcessorContext ctx = new ContentProcessorContext(content.getStream());
|
||||
String type;
|
||||
|
||||
// prevent clients sending non-sense
|
||||
if (!"octet".equalsIgnoreCase(output) && !"inline_octet".equalsIgnoreCase(output)) {
|
||||
output = null;
|
||||
}
|
||||
|
||||
final FileData imageData = imageReadPlatformService.retrieveImage(entityName, entityId);
|
||||
final FileData resizedImage = imageResizer.resize(imageData, maxWidth, maxHeight);
|
||||
|
||||
// If client wants (Accept header) octet-stream, or output="octet" or "inline_octet", then send that instead of
|
||||
// text
|
||||
if (MediaType.APPLICATION_OCTET_STREAM.equalsIgnoreCase(acceptHeader)
|
||||
|| (output != null && (output.equals("octet") || output.equals("inline_octet")))) {
|
||||
return ContentResources.fileDataToResponse(resizedImage, resizedImage.name() + ImageFileExtension.JPEG,
|
||||
"inline_octet".equals(output) ? "inline" : "attachment");
|
||||
}
|
||||
|
||||
// Else return response with Base64 encoded
|
||||
// TODO: Need a better way of determining image type
|
||||
String imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.JPEG.getValue();
|
||||
if (StringUtils.endsWith(imageData.name(), ContentRepositoryUtils.ImageFileExtension.GIF.getValue())) {
|
||||
imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.GIF.getValue();
|
||||
} else if (StringUtils.endsWith(imageData.name(), ContentRepositoryUtils.ImageFileExtension.PNG.getValue())) {
|
||||
imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.PNG.getValue();
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] resizedImageBytes = resizedImage.getByteSource().read();
|
||||
if (resizedImageBytes != null) {
|
||||
final String clientImageAsBase64Text = imageDataURISuffix + Base64.getMimeEncoder().encodeToString(resizedImageBytes);
|
||||
return Response.ok(clientImageAsBase64Text, MediaType.TEXT_PLAIN_TYPE).build();
|
||||
} else {
|
||||
log.error("resizedImageBytes is null for entityName={}, entityId={}, maxWidth={}, maxHeight={}", entityName, entityId,
|
||||
maxWidth, maxHeight);
|
||||
return Response.serverError().build();
|
||||
if ((StringUtils.isNotEmpty(output) && output.contains("octet")) || APPLICATION_OCTET_STREAM_VALUE.equalsIgnoreCase(acceptHeader)) {
|
||||
if (maxWidth != null && maxHeight != null) {
|
||||
ctx = imageResizeContentProcessor.process(content.getStream(), Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, maxWidth,
|
||||
IMAGE_RESIZE_PARAM_MAX_HEIGHT, maxHeight, IMAGE_RESIZE_PARAM_FORMAT, content.getFormat()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ContentManagementException(imageData.name(), e.getMessage(), e);
|
||||
|
||||
if (IMAGE_API_VALUE_OUTPUT_INLINE_OCTET.equalsIgnoreCase(output)) {
|
||||
dispositionType = DISPOSITION_TYPE_INLINE;
|
||||
} else {
|
||||
dispositionType = DISPOSITION_TYPE_ATTACHMENT;
|
||||
}
|
||||
|
||||
type = content.getContentType();
|
||||
} else {
|
||||
// else stream base64 encoded original format
|
||||
if (maxWidth != null && maxHeight != null) {
|
||||
ctx = imageResizeContentProcessor.then(base64EncoderContentProcessor).then(dataUrlEncoderContentProcessor).process(
|
||||
content.getStream(),
|
||||
Map.of(IMAGE_RESIZE_PARAM_MAX_WIDTH, maxWidth, IMAGE_RESIZE_PARAM_MAX_HEIGHT, maxHeight, IMAGE_RESIZE_PARAM_FORMAT,
|
||||
content.getFormat(), DATA_URL_ENCODE_PARAM_CONTENT_TYPE, content.getContentType(),
|
||||
DATA_URL_ENCODE_PARAM_ENCODING, IMAGE_API_VALUE_ENCODING_BASE64));
|
||||
} else {
|
||||
ctx = base64EncoderContentProcessor.then(dataUrlEncoderContentProcessor).process(content.getStream(),
|
||||
Map.of(DATA_URL_ENCODE_PARAM_CONTENT_TYPE, content.getContentType(), DATA_URL_ENCODE_PARAM_ENCODING,
|
||||
IMAGE_API_VALUE_ENCODING_BASE64));
|
||||
}
|
||||
|
||||
type = TEXT_PLAIN_VALUE;
|
||||
}
|
||||
|
||||
// make sure we use the transformed input stream ("ctx.getInputStream()")
|
||||
return StreamResponseUtil.ok(StreamResponseUtil.StreamResponseData.builder().fileName(content.getDisplayName()).type(type)
|
||||
.stream(ctx.getInputStream()).dispositionType(dispositionType).build());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.MULTIPART_FORM_DATA })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)")
|
||||
public ImageCreateResponse createImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @HeaderParam(CONTENT_LENGTH) final Long fileSize,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream is,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart filePart) {
|
||||
|
||||
fileUploadValidator.validate(fileSize, is, fileDetails, filePart);
|
||||
|
||||
final var command = new ImageCreateCommand();
|
||||
|
||||
command.setPayload(
|
||||
ImageCreateRequest.builder().entityId(entityId).entityType(entityType).fileName(fileDetails.getFileName()).size(fileSize)
|
||||
.type(Optional.ofNullable(filePart.getMediaType()).map(MediaType::toString).orElse(null)).stream(is).build());
|
||||
|
||||
final Supplier<ImageCreateResponse> response = commandPipeline.send(command);
|
||||
|
||||
return response.get();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
|
||||
public ImageCreateResponse createImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, final InputStream body) {
|
||||
|
||||
requireNonNull(body, "Missing input stream");
|
||||
|
||||
final var command = new ImageCreateCommand();
|
||||
|
||||
var ctx = dataUrlDecoderContentProcessor.then(base64DecoderContentProcessor).process(new ContentProcessorContext(body));
|
||||
|
||||
command.setPayload(ImageCreateRequest.builder().entityId(entityId).entityType(entityType).stream(ctx.getInputStream()).build());
|
||||
|
||||
final Supplier<ImageCreateResponse> response = commandPipeline.send(command);
|
||||
|
||||
return response.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is added only for consistency with other URL patterns and for maintaining consistency of usage of the
|
||||
* HTTP "verb" at the client side
|
||||
*/
|
||||
@PUT
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@RequestBody(description = "Update client image", content = {
|
||||
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
|
||||
public CommandProcessingResult updateClientImage(@PathParam("entity") final String entityName,
|
||||
@PathParam("entityId") final Long entityId, @HeaderParam("Content-Length") final Long fileSize,
|
||||
@FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails,
|
||||
@FormDataParam("file") final FormDataBodyPart bodyPart) {
|
||||
return addNewClientImage(entityName, entityId, fileSize, inputStream, fileDetails, bodyPart);
|
||||
@RequestBody(description = "Update image", content = { @Content(mediaType = MediaType.MULTIPART_FORM_DATA) })
|
||||
public ImageCreateResponse updateImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityName,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @HeaderParam(CONTENT_LENGTH) final Long fileSize,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream inputStream,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart bodyPart) {
|
||||
return createImage(entityName, entityId, fileSize, inputStream, fileDetails, bodyPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is added only for consistency with other URL patterns and for maintaining consistency of usage of the
|
||||
* HTTP "verb" at the client side
|
||||
*
|
||||
* Upload image as a Data URL (essentially a base64 encoded stream)
|
||||
*/
|
||||
@PUT
|
||||
@Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CommandProcessingResult updateClientImage(@PathParam("entity") final String entityName,
|
||||
@PathParam("entityId") final Long entityId, final String jsonRequestBody) {
|
||||
return addNewClientImage(entityName, entityId, jsonRequestBody);
|
||||
public ImageCreateResponse updateImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityName,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, final InputStream body) {
|
||||
return createImage(entityName, entityId, body);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CommandProcessingResult deleteClientImage(@PathParam("entity") final String entityName,
|
||||
@PathParam("entityId") final Long entityId) {
|
||||
validateEntityTypeforImage(entityName);
|
||||
imageWritePlatformService.deleteImage(entityName, entityId);
|
||||
return CommandProcessingResult.resourceResult(entityId);
|
||||
}
|
||||
public ImageDeleteResponse deleteImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityType,
|
||||
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId) {
|
||||
|
||||
/*** Entities for document Management **/
|
||||
public enum EntityTypeForImages {
|
||||
final var command = new ImageDeleteCommand();
|
||||
|
||||
STAFF, //
|
||||
CLIENTS; //
|
||||
command.setPayload(ImageDeleteRequest.builder().entityId(entityId).entityType(entityType).build());
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().toLowerCase();
|
||||
}
|
||||
}
|
||||
final Supplier<ImageDeleteResponse> response = commandPipeline.send(command);
|
||||
|
||||
private void validateEntityTypeforImage(final String entityName) {
|
||||
if (!checkValidEntityType(entityName)) {
|
||||
throw new InvalidEntityTypeForImageManagementException(entityName);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkValidEntityType(final String entityType) {
|
||||
for (final EntityTypeForImages entities : EntityTypeForImages.values()) {
|
||||
if (entities.name().equalsIgnoreCase(entityType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return response.get();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-44
@@ -20,9 +20,7 @@ package org.apache.fineract.infrastructure.documentmanagement.command;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Immutable command for creating or updating details of a client identifier.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public class DocumentCommand {
|
||||
|
||||
private final Long id;
|
||||
@@ -35,13 +33,9 @@ public class DocumentCommand {
|
||||
private Long size;
|
||||
private String type;
|
||||
private String location;
|
||||
private Integer storageType;
|
||||
|
||||
private final Set<String> modifiedParameters;
|
||||
|
||||
public DocumentCommand(final Set<String> modifiedParameters, final Long id, final String parentEntityType, final Long parentEntityId,
|
||||
final String name, final String fileName, final Long size, final String type, final String description, final String location) {
|
||||
this.modifiedParameters = modifiedParameters;
|
||||
this.id = id;
|
||||
this.parentEntityType = parentEntityType;
|
||||
this.parentEntityId = parentEntityId;
|
||||
@@ -89,10 +83,6 @@ public class DocumentCommand {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
public Set<String> getModifiedParameters() {
|
||||
return this.modifiedParameters;
|
||||
}
|
||||
|
||||
public void setFileName(final String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
@@ -108,37 +98,4 @@ public class DocumentCommand {
|
||||
public void setLocation(final String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public Integer getStorageType() {
|
||||
return this.storageType;
|
||||
}
|
||||
|
||||
public void setStorageType(final Integer storageType) {
|
||||
this.storageType = storageType;
|
||||
}
|
||||
|
||||
public boolean isNameChanged() {
|
||||
return this.modifiedParameters.contains("name");
|
||||
}
|
||||
|
||||
public boolean isFileNameChanged() {
|
||||
return this.modifiedParameters.contains("fileName");
|
||||
}
|
||||
|
||||
public boolean isSizeChanged() {
|
||||
return this.modifiedParameters.contains("size");
|
||||
}
|
||||
|
||||
public boolean isFileTypeChanged() {
|
||||
return this.modifiedParameters.contains("type");
|
||||
}
|
||||
|
||||
public boolean isDescriptionChanged() {
|
||||
return this.modifiedParameters.contains("description");
|
||||
}
|
||||
|
||||
public boolean isLocationChanged() {
|
||||
return this.modifiedParameters.contains("location");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-73
@@ -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<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("document");
|
||||
|
||||
baseDataValidator.reset().parameter("name").value(this.command.getName()).ignoreIfNull().notBlank();
|
||||
baseDataValidator.reset().parameter("size").value(this.command.getSize()).ignoreIfNull().integerGreaterThanZero();
|
||||
baseDataValidator.reset().parameter("fileName").value(this.command.getFileName()).ignoreIfNull().notBlank()
|
||||
.notExceedingLengthOf(250);
|
||||
baseDataValidator.reset().parameter("location").value(this.command.getLocation()).ignoreIfNull().notBlank();
|
||||
baseDataValidator.reset().parameter("description").value(this.command.getName()).ignoreIfNull().notExceedingLengthOf(250);
|
||||
|
||||
baseDataValidator.reset().anyOfNotNull(this.command.getName(), this.command.getFileName(), this.command.getDescription(),
|
||||
this.command.getLocation(), this.command.getSize());
|
||||
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateForCreate() {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("document");
|
||||
baseDataValidator.reset().parameter("parentEntityType").value(this.command.getParentEntityType()).notBlank()
|
||||
.notExceedingLengthOf(50);
|
||||
baseDataValidator.reset().parameter("parentEntityId").value(this.command.getParentEntityId()).integerGreaterThanZero();
|
||||
baseDataValidator.reset().parameter("name").value(this.command.getName()).notBlank().notExceedingLengthOf(250);
|
||||
baseDataValidator.reset().parameter("size").value(this.command.getSize()).integerGreaterThanZero();
|
||||
baseDataValidator.reset().parameter("fileName").value(this.command.getFileName()).notBlank().notExceedingLengthOf(250);
|
||||
baseDataValidator.reset().parameter("description").value(this.command.getName()).notExceedingLengthOf(250);
|
||||
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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<DocumentCreateRequest> {}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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<DocumentDeleteRequest> {}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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<DocumentUpdateRequest> {}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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<ImageCreateRequest> {}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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<ImageDeleteRequest> {}
|
||||
-56
@@ -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();
|
||||
|
||||
}
|
||||
-53
@@ -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<ContentRepository> contentRepositories;
|
||||
|
||||
public ContentRepository getRepository() {
|
||||
if (fineractProperties.getContent() != null) {
|
||||
if (fineractProperties.getContent().getFilesystem() != null
|
||||
&& Boolean.TRUE.equals(fineractProperties.getContent().getFilesystem().getEnabled())) {
|
||||
return getRepository(StorageType.FILE_SYSTEM);
|
||||
} else if (fineractProperties.getContent().getS3() != null
|
||||
&& Boolean.TRUE.equals(fineractProperties.getContent().getS3().getEnabled())) {
|
||||
return getRepository(StorageType.S3);
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
"No content repository enabled. Please set either 'fineract.content.filesystem.enabled=true' or 'fineract.content.s3.enabled=true' in your application.properties.");
|
||||
}
|
||||
|
||||
public ContentRepository getRepository(StorageType storageType) {
|
||||
return contentRepositories.stream().filter(cr -> cr.getStorageType().equals(storageType)).findFirst().orElseThrow(
|
||||
() -> new RuntimeException(String.format("No content repository implementation found for storage type: %s", storageType)));
|
||||
}
|
||||
}
|
||||
-210
@@ -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<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
if (imageFileName == null) {
|
||||
final StringBuilder validationErrorCode = new StringBuilder("validation.msg.clientImage.cannot.be.blank");
|
||||
final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter image cannot be blank.");
|
||||
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
|
||||
defaultEnglishMessage.toString(), "image");
|
||||
dataValidationErrors.add(error);
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random String.
|
||||
*/
|
||||
|
||||
@SuppressFBWarnings(value = {
|
||||
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
||||
public static String generateRandomString() {
|
||||
final String characters = "abcdefghijklmnopqrstuvwxyz123456789";
|
||||
// length is a random number between 5 to 16
|
||||
final int length = random.nextInt(11) + 5;
|
||||
final char[] text = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
text[i] = characters.charAt(random.nextInt(characters.length()));
|
||||
}
|
||||
return new String(text);
|
||||
}
|
||||
}
|
||||
-138
@@ -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<Pattern> regexWhitelist;
|
||||
|
||||
private static Pattern OVERWRITE_SIBLING_IMAGE = Pattern.compile(".*\\.\\./+[0-9]+/+.*");
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
regexWhitelist = fineractProperties.getContent().getRegexWhitelist().stream().map(Pattern::compile).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sanitize(String path) {
|
||||
return sanitize(path, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sanitize(String path, BufferedInputStream is) {
|
||||
try {
|
||||
if (OVERWRITE_SIBLING_IMAGE.matcher(path).matches()) {
|
||||
throw new RuntimeException(String.format("Trying to overwrite another resource's image: %s", path));
|
||||
}
|
||||
|
||||
String sanitizedPath = Path.of(path).normalize().toString();
|
||||
|
||||
String fileName = FilenameUtils.getName(sanitizedPath).toLowerCase();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Path: {} -> {} ({})", path, sanitizedPath, fileName);
|
||||
}
|
||||
|
||||
if (fineractProperties.getContent().isRegexWhitelistEnabled()) {
|
||||
boolean matches = regexWhitelist.stream().anyMatch(p -> p.matcher(fileName).matches());
|
||||
|
||||
if (!matches) {
|
||||
throw new RuntimeException(String.format("File name not allowed: %s", fileName));
|
||||
}
|
||||
}
|
||||
|
||||
if (is != null && fineractProperties.getContent().isMimeWhitelistEnabled()) {
|
||||
Tika tika = new Tika();
|
||||
String extensionMimeType = tika.detect(fileName);
|
||||
|
||||
if (StringUtils.isEmpty(extensionMimeType)) {
|
||||
throw new RuntimeException(String.format("Could not detect mime type for filename %s!", fileName));
|
||||
}
|
||||
|
||||
if (!fineractProperties.getContent().getMimeWhitelist().contains(extensionMimeType)) {
|
||||
throw new RuntimeException(
|
||||
String.format("Detected mime type %s for filename %s not allowed!", extensionMimeType, fileName));
|
||||
}
|
||||
|
||||
String contentMimeType = detectContentMimeType(is);
|
||||
|
||||
if (StringUtils.isEmpty(contentMimeType)) {
|
||||
throw new RuntimeException(String.format("Could not detect content mime type for %s!", fileName));
|
||||
}
|
||||
|
||||
if (!fineractProperties.getContent().getMimeWhitelist().contains(contentMimeType)) {
|
||||
throw new RuntimeException(
|
||||
String.format("Detected content mime type %s for %s not allowed!", contentMimeType, fileName));
|
||||
}
|
||||
|
||||
if (!contentMimeType.equalsIgnoreCase(extensionMimeType)) {
|
||||
throw new RuntimeException(String.format("Detected filename (%s) and content (%s) mime type do not match!",
|
||||
extensionMimeType, contentMimeType));
|
||||
}
|
||||
}
|
||||
|
||||
Path target = Path.of(sanitizedPath);
|
||||
Path rootFolder = Path.of(fineractProperties.getContent().getFilesystem().getRootFolder(),
|
||||
ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim());
|
||||
|
||||
if (!target.startsWith(rootFolder)) {
|
||||
throw new RuntimeException(String.format("Path traversal attempt: %s (%s)", target, rootFolder));
|
||||
}
|
||||
|
||||
return sanitizedPath;
|
||||
} catch (Exception e) {
|
||||
throw new ContentManagementException(path, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String detectContentMimeType(BufferedInputStream bis) throws Exception {
|
||||
TikaInputStream tis = TikaInputStream.get(bis);
|
||||
AutoDetectParser parser = new AutoDetectParser();
|
||||
// NOTE: turn off write limit with "-1"
|
||||
BodyContentHandler handler = new BodyContentHandler(-1);
|
||||
Metadata metadata = new Metadata();
|
||||
parser.parse(tis, handler, metadata);
|
||||
|
||||
return metadata.get("Content-Type");
|
||||
}
|
||||
}
|
||||
-164
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-177
@@ -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<GetObjectResponse> getObject(String key) {
|
||||
try {
|
||||
log.debug("Downloading an object from Amazon S3 Bucket: {}, location: {}",
|
||||
fineractProperties.getContent().getS3().getBucketName(), key);
|
||||
return this.s3Client.getObject(builder -> builder.bucket(fineractProperties.getContent().getS3().getBucketName()).key(key),
|
||||
ResponseTransformer.toBytes());
|
||||
} catch (SdkException ase) {
|
||||
throw new ContentManagementException(key, ase.getMessage(), ase);
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DocumentContent implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String displayName;
|
||||
private String fileName;
|
||||
private String contentType;
|
||||
private String format;
|
||||
private Long size;
|
||||
@Hidden
|
||||
@JsonIgnore
|
||||
private InputStream stream;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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;
|
||||
}
|
||||
+15
-22
@@ -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;
|
||||
}
|
||||
+18
-22
@@ -20,32 +20,28 @@ package org.apache.fineract.infrastructure.documentmanagement.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Immutable data object representing a user document being managed on the platform.
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DocumentData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final String parentEntityType;
|
||||
private final Long parentEntityId;
|
||||
private final String name;
|
||||
private final String fileName;
|
||||
private final Long size;
|
||||
private final String type;
|
||||
private final String location;
|
||||
private final String description;
|
||||
private final Integer storageType;
|
||||
|
||||
public StorageType storageType() {
|
||||
return StorageType.fromInt(this.storageType);
|
||||
}
|
||||
private Long id;
|
||||
private String parentEntityType;
|
||||
private Long parentEntityId;
|
||||
private String name;
|
||||
private String fileName;
|
||||
private Long size;
|
||||
private String type;
|
||||
private String location;
|
||||
private String description;
|
||||
private Integer storageType;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.data;
|
||||
|
||||
import 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;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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;
|
||||
}
|
||||
+63
@@ -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;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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;
|
||||
}
|
||||
-55
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.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;
|
||||
}
|
||||
-93
@@ -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<InputStream> optResizedIS = resizeImage(ContentRepositoryUtils.imageExtensionFromFileName(fileData.name()), is,
|
||||
maxWidth != null ? maxWidth : Integer.MAX_VALUE, maxHeight != null ? maxHeight : Integer.MAX_VALUE);
|
||||
if (optResizedIS.isPresent()) {
|
||||
FileData resizedImage = new FileData(new ByteSource() {
|
||||
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
return optResizedIS.get(); // NOSONAR
|
||||
}
|
||||
}, fileData.name(), fileData.contentType());
|
||||
return resizedImage;
|
||||
}
|
||||
return fileData;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("resize() failed, returning original image: {}", e.getMessage(), e);
|
||||
return fileData;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<InputStream> resizeImage(ImageFileExtension fileExtension, InputStream in, int maxWidth, int maxHeight)
|
||||
throws IOException {
|
||||
BufferedImage src = ImageIO.read(in);
|
||||
if (src.getWidth() <= maxWidth && src.getHeight() <= maxHeight) {
|
||||
return Optional.empty();
|
||||
}
|
||||
float widthRatio = (float) src.getWidth() / maxWidth;
|
||||
float heightRatio = (float) src.getHeight() / maxHeight;
|
||||
float scaleRatio = widthRatio > heightRatio ? widthRatio : heightRatio;
|
||||
|
||||
// TODO(lindahl): Improve compressed image quality (perhaps quality ratio)
|
||||
|
||||
int newWidth = (int) (src.getWidth() / scaleRatio);
|
||||
int newHeight = (int) (src.getHeight() / scaleRatio);
|
||||
int colorModel = fileExtension == ContentRepositoryUtils.ImageFileExtension.JPEG ? BufferedImage.TYPE_INT_RGB
|
||||
: BufferedImage.TYPE_INT_ARGB;
|
||||
BufferedImage target = new BufferedImage(newWidth, newHeight, colorModel);
|
||||
Graphics2D g = target.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(src, 0, 0, newWidth, newHeight, Color.BLACK, null);
|
||||
g.dispose();
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
ImageIO.write(target, fileExtension != null ? fileExtension.getValueWithoutDot() : "jpeg", os);
|
||||
return Optional.of(new ByteArrayInputStream(os.toByteArray()));
|
||||
}
|
||||
}
|
||||
+35
-127
@@ -18,150 +18,58 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_document")
|
||||
public class Document extends AbstractPersistableCustom<Long> {
|
||||
@Table("m_document")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@FieldNameConstants
|
||||
public final class Document implements Serializable {
|
||||
|
||||
@Column(name = "parent_entity_type", length = 50)
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column("id")
|
||||
private Long id;
|
||||
|
||||
@Column("parent_entity_type")
|
||||
private String parentEntityType;
|
||||
|
||||
@Column(name = "parent_entity_id", length = 1000)
|
||||
@Column("parent_entity_id")
|
||||
private Long parentEntityId;
|
||||
|
||||
@Column(name = "name", length = 250)
|
||||
@Column("name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "file_name", length = 250)
|
||||
@Column("file_name")
|
||||
private String fileName;
|
||||
|
||||
@Column(name = "size")
|
||||
@Column("size")
|
||||
private Long size;
|
||||
|
||||
@Column(name = "type", length = 50)
|
||||
@Column("type")
|
||||
private String type;
|
||||
|
||||
@Column(name = "description", length = 1000)
|
||||
@Column("description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "location", length = 500)
|
||||
@Column("location")
|
||||
private String location;
|
||||
|
||||
@Column(name = "storage_type_enum")
|
||||
@Column("storage_type_enum")
|
||||
private Integer storageType;
|
||||
|
||||
public Document() {}
|
||||
|
||||
public static Document createNew(final String parentEntityType, final Long parentEntityId, final String name, final String fileName,
|
||||
final Long size, final String type, final String description, final String location, final StorageType storageType) {
|
||||
return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location, storageType);
|
||||
}
|
||||
|
||||
private Document(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size,
|
||||
final String type, final String description, final String location, final StorageType storageType) {
|
||||
this.parentEntityType = StringUtils.defaultIfEmpty(parentEntityType, null);
|
||||
this.parentEntityId = parentEntityId;
|
||||
this.name = StringUtils.defaultIfEmpty(name, null);
|
||||
this.fileName = StringUtils.defaultIfEmpty(fileName, null);
|
||||
this.size = size;
|
||||
this.type = StringUtils.defaultIfEmpty(type, null);
|
||||
this.description = StringUtils.defaultIfEmpty(description, null);
|
||||
this.location = StringUtils.defaultIfEmpty(location, null);
|
||||
this.storageType = storageType.getValue();
|
||||
}
|
||||
|
||||
public void update(final DocumentCommand command) {
|
||||
if (command.isDescriptionChanged()) {
|
||||
this.description = command.getDescription();
|
||||
}
|
||||
if (command.isFileNameChanged()) {
|
||||
this.fileName = command.getFileName();
|
||||
}
|
||||
if (command.isFileTypeChanged()) {
|
||||
this.type = command.getType();
|
||||
}
|
||||
if (command.isLocationChanged()) {
|
||||
this.location = command.getLocation();
|
||||
}
|
||||
if (command.isNameChanged()) {
|
||||
this.name = command.getName();
|
||||
}
|
||||
if (command.isSizeChanged()) {
|
||||
this.size = command.getSize();
|
||||
}
|
||||
}
|
||||
|
||||
public String getParentEntityType() {
|
||||
return this.parentEntityType;
|
||||
}
|
||||
|
||||
public void setParentEntityType(final String parentEntityType) {
|
||||
this.parentEntityType = parentEntityType;
|
||||
}
|
||||
|
||||
public Long getParentEntityId() {
|
||||
return this.parentEntityId;
|
||||
}
|
||||
|
||||
public void setParentEntityId(final Long parentEntityId) {
|
||||
this.parentEntityId = parentEntityId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return this.fileName;
|
||||
}
|
||||
|
||||
public void setFileName(final String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
public void setSize(final Long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(final String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
public void setLocation(final String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public StorageType storageType() {
|
||||
return StorageType.fromInt(this.storageType);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user