FINERACT-2246: New command processing - document management
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
8a61e30a45
commit
2ff0c62f40
+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();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* 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.data;
|
||||
|
||||
public enum ContentStoreType {
|
||||
|
||||
FILE_SYSTEM(1), //
|
||||
S3(2); //
|
||||
|
||||
private final Integer value;
|
||||
|
||||
ContentStoreType(final Integer value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Integer getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
+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.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 DocumentData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -18,9 +18,14 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.repository.ListCrudRepository;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
|
||||
public interface DocumentRepository extends JpaRepository<Document, Long>, JpaSpecificationExecutor<Document> {
|
||||
// no added behaviour
|
||||
public interface DocumentRepository extends ListCrudRepository<Document, Long>, QueryByExampleExecutor<Document> {
|
||||
|
||||
List<Document> findAllByParentEntityTypeAndParentEntityId(String parentEntityType, Long parentEntityId);
|
||||
|
||||
Optional<Document> findByIdAndParentEntityTypeAndParentEntityId(Long id, String entityType, Long entityId);
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.domain;
|
||||
|
||||
import 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;
|
||||
|
||||
@Table("m_image")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@FieldNameConstants
|
||||
public final class Image implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column("id")
|
||||
private Long id;
|
||||
|
||||
@Column("location")
|
||||
private String location;
|
||||
|
||||
@Column("storage_type_enum")
|
||||
private Integer storageType;
|
||||
}
|
||||
+3
-4
@@ -18,8 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.domain;
|
||||
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.repository.ListCrudRepository;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
|
||||
public interface ImageRepository extends JpaRepository<Image, Long>, JpaSpecificationExecutor<Client> {}
|
||||
public interface ImageRepository extends ListCrudRepository<Image, Long>, QueryByExampleExecutor<Image> {}
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ContentManagementException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ContentManagementException(final String filename, final String message) {
|
||||
super("error.msg.document.save", "Error while manipulating file " + filename + " due to a ContentRepository issue " + message,
|
||||
filename, message);
|
||||
}
|
||||
|
||||
public ContentManagementException(final String name, final Long fileSize, final int maxFileSize) {
|
||||
super("error.msg.document.file.too.big", "Unable to save the document with name" + name + " since its file Size of "
|
||||
+ fileSize / (1024 * 1024) + " MB exceeds the max permissable file size of " + maxFileSize + " MB", name, fileSize);
|
||||
}
|
||||
|
||||
public ContentManagementException(String filename, String message, Exception exception) {
|
||||
super("error.msg.document.save", "Error while manipulating file " + filename + " due to a ContentRepository issue " + message,
|
||||
filename, message, exception);
|
||||
}
|
||||
}
|
||||
+4
-7
@@ -18,14 +18,11 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Runtime exception for invalid image types
|
||||
*/
|
||||
public class InvalidEntityTypeForImageManagementException extends AbstractPlatformResourceNotFoundException {
|
||||
public class DocumentInvalidRequestException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public InvalidEntityTypeForImageManagementException(String imageType) {
|
||||
super("error.imagemanagement.entitytype.invalid", "Image Management is not support for the Entity Type: " + imageType);
|
||||
public DocumentInvalidRequestException(Exception e) {
|
||||
super("error.msg.document.request.invalid", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
+6
-14
@@ -18,24 +18,16 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import software.amazon.awssdk.core.exception.SdkException;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class DocumentNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
public class DocumentNotFoundException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public DocumentNotFoundException(final Long id) {
|
||||
super("error.msg.document.id.invalid", "Document with identifier " + id + " does not exist", id);
|
||||
}
|
||||
|
||||
public DocumentNotFoundException(final String entityType, final Long entityId, final Long id) {
|
||||
super("error.msg.document.id.invalid",
|
||||
"Document with identifier " + id + " does not exist for the " + entityType + " with Identifier " + entityId, id);
|
||||
}
|
||||
|
||||
public DocumentNotFoundException(String entityType, Long entityId, Long id, EmptyResultDataAccessException e) {
|
||||
super("error.msg.document.id.invalid",
|
||||
"Document with identifier " + id + " does not exist for the " + entityType + " with Identifier " + entityId, id, e);
|
||||
}
|
||||
|
||||
public DocumentNotFoundException(String entityType, Long entityId, Long id, SdkException ace) {
|
||||
super("error.msg.document.id.invalid",
|
||||
"Document with identifier " + id + " does not exist for the " + entityType + " with Identifier " + entityId, id, ace);
|
||||
}
|
||||
}
|
||||
|
||||
+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.handler;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentCreateCommandHandler implements CommandHandler<DocumentCreateRequest, DocumentCreateResponse> {
|
||||
|
||||
private final DocumentWritePlatformService writePlatformService;
|
||||
|
||||
@Retry(name = "commandDocumentCreate", fallbackMethod = "fallback")
|
||||
@Transactional
|
||||
@Override
|
||||
public DocumentCreateResponse handle(final Command<DocumentCreateRequest> command) {
|
||||
return writePlatformService.createDocument(command.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentCreateResponse fallback(Command<DocumentCreateRequest> command, Throwable t) {
|
||||
// NOTE: fallback method needs to be in the same class
|
||||
return CommandHandler.super.fallback(command, t);
|
||||
}
|
||||
}
|
||||
+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.handler;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentDeleteCommandHandler implements CommandHandler<DocumentDeleteRequest, DocumentDeleteResponse> {
|
||||
|
||||
private final DocumentWritePlatformService writePlatformService;
|
||||
|
||||
@Retry(name = "commandDocumentDelete", fallbackMethod = "fallback")
|
||||
@Transactional
|
||||
@Override
|
||||
public DocumentDeleteResponse handle(final Command<DocumentDeleteRequest> command) {
|
||||
return writePlatformService.deleteDocument(command.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentDeleteResponse fallback(Command<DocumentDeleteRequest> command, Throwable t) {
|
||||
// NOTE: fallback method needs to be in the same class
|
||||
return CommandHandler.super.fallback(command, t);
|
||||
}
|
||||
}
|
||||
+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.handler;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentUpdateCommandHandler implements CommandHandler<DocumentUpdateRequest, DocumentUpdateResponse> {
|
||||
|
||||
private final DocumentWritePlatformService writePlatformService;
|
||||
|
||||
@Retry(name = "commandDocumentUpdate", fallbackMethod = "fallback")
|
||||
@Transactional
|
||||
@Override
|
||||
public DocumentUpdateResponse handle(final Command<DocumentUpdateRequest> command) {
|
||||
return writePlatformService.updateDocument(command.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentUpdateResponse fallback(Command<DocumentUpdateRequest> command, Throwable t) {
|
||||
// NOTE: fallback method needs to be in the same class
|
||||
return CommandHandler.super.fallback(command, t);
|
||||
}
|
||||
}
|
||||
+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.handler;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.ImageWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ImageCreateCommandHandler implements CommandHandler<ImageCreateRequest, ImageCreateResponse> {
|
||||
|
||||
private final ImageWritePlatformService writePlatformService;
|
||||
|
||||
@Retry(name = "commandImageCreate", fallbackMethod = "fallback")
|
||||
@Transactional
|
||||
@Override
|
||||
public ImageCreateResponse handle(final Command<ImageCreateRequest> command) {
|
||||
return writePlatformService.createImage(command.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCreateResponse fallback(Command<ImageCreateRequest> command, Throwable t) {
|
||||
// NOTE: fallback method needs to be in the same class
|
||||
return CommandHandler.super.fallback(command, t);
|
||||
}
|
||||
}
|
||||
+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.handler;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.service.ImageWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ImageDeleteCommandHandler implements CommandHandler<ImageDeleteRequest, ImageDeleteResponse> {
|
||||
|
||||
private final ImageWritePlatformService writePlatformService;
|
||||
|
||||
@Retry(name = "commandImageDelete", fallbackMethod = "fallback")
|
||||
@Transactional
|
||||
@Override
|
||||
public ImageDeleteResponse handle(final Command<ImageDeleteRequest> command) {
|
||||
return writePlatformService.deleteImage(command.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageDeleteResponse fallback(Command<ImageDeleteRequest> command, Throwable t) {
|
||||
// NOTE: fallback method needs to be in the same class
|
||||
return CommandHandler.super.fallback(command, t);
|
||||
}
|
||||
}
|
||||
+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.documentmanagement.mapping;
|
||||
|
||||
import static org.mapstruct.InjectionStrategy.CONSTRUCTOR;
|
||||
import static org.mapstruct.MappingConstants.ComponentModel.SPRING;
|
||||
|
||||
import java.io.InputStream;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
@Mapper(componentModel = SPRING, injectionStrategy = CONSTRUCTOR)
|
||||
public interface DocumentMapper {
|
||||
|
||||
DocumentData map(Document source);
|
||||
|
||||
@Mapping(source = "source.type", target = "contentType")
|
||||
@Mapping(source = "source.name", target = "displayName")
|
||||
@Mapping(source = "source.fileName", target = "fileName")
|
||||
@Mapping(source = "source.size", target = "size")
|
||||
@Mapping(ignore = true, target = "format")
|
||||
@Mapping(source = "stream", target = "stream")
|
||||
DocumentContent map(Document source, InputStream stream);
|
||||
}
|
||||
+4
-2
@@ -19,14 +19,16 @@
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
|
||||
|
||||
public interface DocumentReadPlatformService {
|
||||
|
||||
List<DocumentData> retrieveAllDocuments(String entityType, Long entityId);
|
||||
|
||||
FileData retrieveFileData(String entityType, Long entityId, Long documentId);
|
||||
DocumentData retrieveDocument(Long id);
|
||||
|
||||
DocumentContent retrieveDocumentContent(String entityType, Long entityId, Long documentId);
|
||||
|
||||
DocumentData retrieveDocument(String entityType, Long entityId, Long documentId);
|
||||
|
||||
|
||||
+23
-81
@@ -18,108 +18,50 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepository;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.mapping.DocumentMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final PlatformSecurityContext context;
|
||||
private final ContentRepositoryFactory contentRepositoryFactory;
|
||||
private final ContentStoreService storeService;
|
||||
private final DocumentRepository documentRepository;
|
||||
private final DocumentMapper documentMapper;
|
||||
|
||||
@Override
|
||||
public List<DocumentData> retrieveAllDocuments(final String entityType, final Long entityId) {
|
||||
final var docs = documentRepository.findAllByParentEntityTypeAndParentEntityId(entityType, entityId);
|
||||
|
||||
this.context.authenticatedUser();
|
||||
|
||||
// TODO verify if the entities are valid and a user
|
||||
// has data
|
||||
// scope for the particular entities
|
||||
final DocumentMapper mapper = new DocumentMapper(true, true);
|
||||
final String sql = "select " + mapper.schema() + " order by d.id";
|
||||
return this.jdbcTemplate.query(sql, mapper, new Object[] { entityType, entityId }); // NOSONAR
|
||||
return docs.stream().map(documentMapper::map).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileData retrieveFileData(final String entityType, final Long entityId, final Long documentId) {
|
||||
try {
|
||||
final DocumentMapper mapper = new DocumentMapper(false, false);
|
||||
final DocumentData documentData = fetchDocumentDetails(entityType, entityId, documentId, mapper);
|
||||
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(documentData.storageType());
|
||||
return contentRepository.fetchFile(documentData);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new DocumentNotFoundException(entityType, entityId, documentId, e);
|
||||
}
|
||||
public DocumentData retrieveDocument(final Long id) {
|
||||
return documentRepository.findById(id).map(documentMapper::map).orElseThrow(() -> new DocumentNotFoundException(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentContent retrieveDocumentContent(final String entityType, final Long entityId, final Long documentId) {
|
||||
final var doc = documentRepository.findByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
|
||||
.orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
|
||||
final var is = storeService.download(doc.getLocation());
|
||||
|
||||
return documentMapper.map(doc, is);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentData retrieveDocument(final String entityType, final Long entityId, final Long documentId) {
|
||||
try {
|
||||
final DocumentMapper mapper = new DocumentMapper(true, true);
|
||||
return fetchDocumentDetails(entityType, entityId, documentId, mapper);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new DocumentNotFoundException(entityType, entityId, documentId, e);
|
||||
}
|
||||
}
|
||||
final var doc = documentRepository.findByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
|
||||
.orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
|
||||
|
||||
private DocumentData fetchDocumentDetails(final String entityType, final Long entityId, final Long documentId,
|
||||
final DocumentMapper mapper) {
|
||||
final String sql = "select " + mapper.schema() + " and d.id=? ";
|
||||
return this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { entityType, entityId, documentId }); // NOSONAR
|
||||
}
|
||||
|
||||
private static final class DocumentMapper implements RowMapper<DocumentData> {
|
||||
|
||||
private final boolean hideLocation;
|
||||
private final boolean hideStorageType;
|
||||
|
||||
DocumentMapper(final boolean hideLocation, final boolean hideStorageType) {
|
||||
this.hideLocation = hideLocation;
|
||||
this.hideStorageType = hideStorageType;
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return "d.id as id, d.parent_entity_type as parentEntityType, d.parent_entity_id as parentEntityId, d.name as name, "
|
||||
+ " d.file_name as fileName, d.size as fileSize, d.type as fileType, "
|
||||
+ " d.description as description, d.location as location," + " d.storage_type_enum as storageType"
|
||||
+ " from m_document d where d.parent_entity_type=? and d.parent_entity_id=?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
final Long id = JdbcSupport.getLong(rs, "id");
|
||||
final Long parentEntityId = JdbcSupport.getLong(rs, "parentEntityId");
|
||||
final Long fileSize = JdbcSupport.getLong(rs, "fileSize");
|
||||
final String parentEntityType = rs.getString("parentEntityType");
|
||||
final String name = rs.getString("name");
|
||||
final String fileName = rs.getString("fileName");
|
||||
final String fileType = rs.getString("fileType");
|
||||
final String description = rs.getString("description");
|
||||
String location = null;
|
||||
Integer storageType = null;
|
||||
if (!this.hideLocation) {
|
||||
location = rs.getString("location");
|
||||
}
|
||||
if (!this.hideStorageType) {
|
||||
storageType = rs.getInt("storageType");
|
||||
}
|
||||
return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, location, description,
|
||||
storageType);
|
||||
}
|
||||
return documentMapper.map(doc);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-15
@@ -18,24 +18,18 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
|
||||
|
||||
public interface DocumentWritePlatformService {
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_DOCUMENT')")
|
||||
Long createDocument(DocumentCommand documentCommand, InputStream inputStream);
|
||||
DocumentCreateResponse createDocument(DocumentCreateRequest request);
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'UPDATE_DOCUMENT')")
|
||||
CommandProcessingResult updateDocument(DocumentCommand documentCommand, InputStream inputStream);
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'DELETE_DOCUMENT')")
|
||||
CommandProcessingResult deleteDocument(DocumentCommand documentCommand);
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_DOCUMENT')")
|
||||
Long createInternalDocument(String entityType, Long entityId, Long fileSize, InputStream inputStream, String mimeType, String name,
|
||||
String description, String fileName);
|
||||
DocumentUpdateResponse updateDocument(DocumentUpdateRequest request);
|
||||
|
||||
DocumentDeleteResponse deleteDocument(DocumentDeleteRequest request);
|
||||
}
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentUpdateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.mapping.DocumentMapper;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentCreatedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentDeletedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformService {
|
||||
|
||||
private static final String STORE_PREFIX = "documents";
|
||||
|
||||
private final DocumentRepository documentRepository;
|
||||
private final DocumentMapper documentMapper;
|
||||
private final ContentStoreService storeService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final FineractProperties properties;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public DocumentCreateResponse createDocument(@Valid final DocumentCreateRequest request) {
|
||||
try {
|
||||
requireNonNull(request, "Request parameter required");
|
||||
requireNonNull(request.getFileName(), "File name parameter required");
|
||||
requireNonNull(request.getStream(), "Document content required");
|
||||
|
||||
// TODO: make "prefix" configurable
|
||||
var path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), request.getFileName(),
|
||||
storeService.getDelimiter());
|
||||
|
||||
path = storeService.upload(path, request.getStream(), request.getType());
|
||||
|
||||
final var doc = new Document().setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId())
|
||||
.setName(Optional.ofNullable(request.getName()).orElse(request.getFileName())).setFileName(request.getFileName())
|
||||
.setSize(request.getSize()).setType(request.getType()).setDescription(request.getDescription()).setLocation(path)
|
||||
.setStorageType(storeService.getType().getValue());
|
||||
|
||||
documentRepository.save(doc);
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(documentMapper.map(doc)));
|
||||
|
||||
return DocumentCreateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
|
||||
} catch (final Exception e) {
|
||||
throw ErrorHandler.getMappable(e, "error.msg.document.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource.");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public DocumentUpdateResponse updateDocument(@Valid final DocumentUpdateRequest request) {
|
||||
try {
|
||||
requireNonNull(request, "Request parameter required");
|
||||
|
||||
String path = null;
|
||||
|
||||
if (StringUtils.isNotEmpty(request.getFileName())) {
|
||||
// TODO: make "prefix" configurable
|
||||
path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), request.getFileName(),
|
||||
storeService.getDelimiter());
|
||||
}
|
||||
|
||||
final var doc = this.documentRepository.findById(request.getId())
|
||||
.orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), request.getId()));
|
||||
|
||||
doc.setStorageType(storeService.getType().getValue());
|
||||
|
||||
if (StringUtils.isNotEmpty(request.getFileName())) {
|
||||
// these two only make sense if we have an actual file
|
||||
|
||||
if (request.getStream() != null && StringUtils.isNotEmpty(path) && !Strings.CI.equals(doc.getLocation(), path)) {
|
||||
storeService.delete(doc.getLocation());
|
||||
storeService.upload(path, request.getStream(), request.getType());
|
||||
doc.setLocation(path);
|
||||
doc.setFileName(FilenameUtils.getName(path));
|
||||
}
|
||||
} else if (request.getStream() != null) {
|
||||
storeService.upload(doc.getLocation(), request.getStream(), request.getType());
|
||||
}
|
||||
if (StringUtils.isNotEmpty(request.getDescription()) && !Strings.CI.equals(doc.getDescription(), request.getDescription())) {
|
||||
doc.setDescription(request.getDescription());
|
||||
}
|
||||
if (StringUtils.isNotEmpty(request.getName()) && !Strings.CI.equals(doc.getName(), request.getName())) {
|
||||
doc.setName(request.getName());
|
||||
}
|
||||
if (Objects.requireNonNullElse(request.getSize(), 0L) > 1L && !Objects.equals(doc.getSize(), request.getSize())) {
|
||||
doc.setSize(request.getSize());
|
||||
}
|
||||
|
||||
documentRepository.save(doc);
|
||||
|
||||
// TODO: shouldn't we send a business event?
|
||||
// businessEventNotifierService.notifyPostBusinessEvent(new DocumentUpdateBusinessEvent(doc));
|
||||
|
||||
return DocumentUpdateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
|
||||
} catch (final Exception e) {
|
||||
throw ErrorHandler.getMappable(e, "error.msg.document.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource.");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public DocumentDeleteResponse deleteDocument(@Valid final DocumentDeleteRequest request) {
|
||||
try {
|
||||
final var doc = this.documentRepository.findById(request.getId())
|
||||
.orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), request.getId()));
|
||||
|
||||
storeService.delete(doc.getLocation());
|
||||
|
||||
documentRepository.deleteById(request.getId());
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new DocumentDeletedBusinessEvent(documentMapper.map(doc)));
|
||||
|
||||
return DocumentDeleteResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
|
||||
} catch (final Exception e) {
|
||||
throw ErrorHandler.getMappable(e, "error.msg.document.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource.");
|
||||
}
|
||||
}
|
||||
|
||||
private String getPath(final String prefix, final String entityType, final Long entityId, final String fileName, String delimiter) {
|
||||
requireNonNull(prefix);
|
||||
requireNonNull(entityType);
|
||||
requireNonNull(entityId);
|
||||
requireNonNull(fileName);
|
||||
requireNonNull(delimiter);
|
||||
|
||||
return String.join(delimiter, prefix, entityType, String.valueOf(entityId), fileName);
|
||||
}
|
||||
}
|
||||
-200
@@ -1,200 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommandValidator;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepository;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.InvalidEntityTypeForDocumentManagementException;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentCreatedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.document.DocumentDeletedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWritePlatformService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DocumentWritePlatformServiceJpaRepositoryImpl.class);
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final DocumentRepository documentRepository;
|
||||
private final ContentRepositoryFactory contentRepositoryFactory;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
|
||||
@Autowired
|
||||
public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DocumentRepository documentRepository,
|
||||
final ContentRepositoryFactory documentStoreFactory, BusinessEventNotifierService businessEventNotifierService) {
|
||||
this.context = context;
|
||||
this.documentRepository = documentRepository;
|
||||
this.contentRepositoryFactory = documentStoreFactory;
|
||||
this.businessEventNotifierService = businessEventNotifierService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Long createDocument(final DocumentCommand documentCommand, final InputStream inputStream) {
|
||||
try {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final DocumentCommandValidator validator = new DocumentCommandValidator(documentCommand);
|
||||
|
||||
validateParentEntityType(documentCommand);
|
||||
|
||||
validator.validateForCreate();
|
||||
|
||||
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository();
|
||||
|
||||
final String fileLocation = contentRepository.saveFile(inputStream, documentCommand);
|
||||
|
||||
final Document document = Document.createNew(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(),
|
||||
documentCommand.getName(), documentCommand.getFileName(), documentCommand.getSize(), documentCommand.getType(),
|
||||
documentCommand.getDescription(), fileLocation, contentRepository.getStorageType());
|
||||
|
||||
this.documentRepository.saveAndFlush(document);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(document));
|
||||
|
||||
return document.getId();
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
LOG.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, "error.msg.document.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource.");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Long createInternalDocument(final String entityType, final Long entityId, final Long fileSize, final InputStream inputStream,
|
||||
final String mimeType, final String name, final String description, final String fileName) {
|
||||
|
||||
final DocumentCommand documentCommand = new DocumentCommand(null, null, entityType, entityId, name, fileName, fileSize, mimeType,
|
||||
description, null);
|
||||
|
||||
return createDocument(documentCommand, inputStream);
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult updateDocument(final DocumentCommand documentCommand, final InputStream inputStream) {
|
||||
try {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
String oldLocation = null;
|
||||
final DocumentCommandValidator validator = new DocumentCommandValidator(documentCommand);
|
||||
validator.validateForUpdate();
|
||||
// TODO check if entity id is valid and within data scope for the
|
||||
// user
|
||||
final Document documentForUpdate = this.documentRepository.findById(documentCommand.getId())
|
||||
.orElseThrow(() -> new DocumentNotFoundException(documentCommand.getParentEntityType(),
|
||||
documentCommand.getParentEntityId(), documentCommand.getId()));
|
||||
|
||||
final StorageType documentStoreType = documentForUpdate.storageType();
|
||||
oldLocation = documentForUpdate.getLocation();
|
||||
if (inputStream != null && documentCommand.isFileNameChanged()) {
|
||||
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository();
|
||||
documentCommand.setLocation(contentRepository.saveFile(inputStream, documentCommand));
|
||||
documentCommand.setStorageType(contentRepository.getStorageType().getValue());
|
||||
}
|
||||
|
||||
documentForUpdate.update(documentCommand);
|
||||
|
||||
if (inputStream != null && documentCommand.isFileNameChanged()) {
|
||||
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(documentStoreType);
|
||||
contentRepository.deleteFile(oldLocation);
|
||||
}
|
||||
|
||||
this.documentRepository.saveAndFlush(documentForUpdate);
|
||||
|
||||
return CommandProcessingResult.resourceResult(documentForUpdate.getId());
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
LOG.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, "error.msg.document.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource.");
|
||||
} catch (final ContentManagementException cme) {
|
||||
LOG.error("Error occured.", cme);
|
||||
throw new ContentManagementException(documentCommand.getName(), cme.getMessage(), cme);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult deleteDocument(final DocumentCommand documentCommand) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
validateParentEntityType(documentCommand);
|
||||
// TODO: Check document is present under this entity Id
|
||||
final Document document = this.documentRepository.findById(documentCommand.getId())
|
||||
.orElseThrow(() -> new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(),
|
||||
documentCommand.getId()));
|
||||
this.documentRepository.delete(document);
|
||||
|
||||
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(document.storageType());
|
||||
contentRepository.deleteFile(document.getLocation());
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new DocumentDeletedBusinessEvent(document));
|
||||
return CommandProcessingResult.resourceResult(document.getId());
|
||||
}
|
||||
|
||||
private void validateParentEntityType(final DocumentCommand documentCommand) {
|
||||
if (!checkValidEntityType(documentCommand.getParentEntityType())) {
|
||||
throw new InvalidEntityTypeForDocumentManagementException(documentCommand.getParentEntityType());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkValidEntityType(final String entityType) {
|
||||
for (final DocumentManagementEntity entities : DocumentManagementEntity.values()) {
|
||||
if (entities.name().equalsIgnoreCase(entityType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*** Entities for document Management **/
|
||||
public enum DocumentManagementEntity {
|
||||
|
||||
CLIENTS, //
|
||||
CLIENT_IDENTIFIERS, //
|
||||
STAFF, //
|
||||
LOANS, //
|
||||
SAVINGS, //
|
||||
GROUPS, //
|
||||
IMPORT; //
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -18,10 +18,10 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
|
||||
|
||||
public interface ImageReadPlatformService {
|
||||
|
||||
FileData retrieveImage(String entityType, Long entityId);
|
||||
DocumentContent retrieveImage(String entityType, Long entityId);
|
||||
|
||||
}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentInvalidRequestException;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ImageReadPlatformServiceImpl implements ImageReadPlatformService {
|
||||
|
||||
@Deprecated
|
||||
private final List<EntityImageIdAdapter> imageIdAdapters;
|
||||
private final ContentStoreService storeService;
|
||||
private final ImageRepository imageRepository;
|
||||
private final ContentDetectorManager contentDetectorManager;
|
||||
|
||||
@Override
|
||||
public DocumentContent retrieveImage(final String entityType, final Long entityId) {
|
||||
try {
|
||||
return imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(entityType)).findFirst()
|
||||
.flatMap(imageIdAdapter -> imageIdAdapter.get(entityId))
|
||||
.flatMap(
|
||||
imageIdResult -> imageRepository.findById(imageIdResult.getId())
|
||||
.map(image -> DocumentContent
|
||||
.builder().fileName(
|
||||
FilenameUtils.getName(image.getLocation()))
|
||||
.format(FilenameUtils.getExtension(image.getLocation()))
|
||||
.displayName(imageIdResult.getDisplayName())
|
||||
.contentType(
|
||||
contentDetectorManager
|
||||
.detect(ContentDetectorContext.builder()
|
||||
.fileName(FilenameUtils.getName(image.getLocation())).build())
|
||||
.getMimeType())
|
||||
.stream(storeService.download(image.getLocation())).build()))
|
||||
.orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, -1L));
|
||||
} catch (final Exception e) {
|
||||
throw new DocumentInvalidRequestException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-11
@@ -18,19 +18,14 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.domain.Base64EncodedImage;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
|
||||
|
||||
public interface ImageWritePlatformService {
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE','CREATE_STAFFIMAGE')")
|
||||
CommandProcessingResult saveOrUpdateImage(String entityName, Long entityId, String imageName, InputStream inputStream, Long fileSize);
|
||||
ImageCreateResponse createImage(ImageCreateRequest request);
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE','CREATE_STAFFIMAGE')")
|
||||
CommandProcessingResult saveOrUpdateImage(String entityName, Long entityId, Base64EncodedImage encodedImage);
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'DELETE_CLIENTIMAGE','DELETE_STAFFIMAGE')")
|
||||
CommandProcessingResult deleteImage(String entityName, Long entityId);
|
||||
ImageDeleteResponse deleteImage(ImageDeleteRequest request);
|
||||
}
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
|
||||
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.ImageDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentInvalidRequestException;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ImageWritePlatformServiceImpl implements ImageWritePlatformService {
|
||||
|
||||
private static final String STORE_PREFIX = "images";
|
||||
private static final String DEFAULT_ENTITY_TYPE = "clients";
|
||||
private static final String DEFAULT_EXTENSION = ".jpg";
|
||||
|
||||
@Deprecated
|
||||
private final List<EntityImageIdAdapter> imageIdAdapters;
|
||||
private final ContentStoreService storeService;
|
||||
private final ImageRepository imageRepository;
|
||||
private final ContentDetectorManager contentDetectorManager;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public ImageCreateResponse createImage(final ImageCreateRequest request) {
|
||||
try {
|
||||
if (StringUtils.isEmpty(request.getEntityType())) {
|
||||
// TODO: keeping the path segment always "clients" not consistent how this works with documents
|
||||
request.setEntityType(DEFAULT_ENTITY_TYPE);
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(request.getFileName())) {
|
||||
// NOTE: defacto limiting the uploads to JPEG files, same behavior as before
|
||||
request.setFileName(UUID.randomUUID() + DEFAULT_EXTENSION);
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(request.getType())) {
|
||||
final var type = Optional
|
||||
.of(contentDetectorManager.detect(ContentDetectorContext.builder().fileName(request.getFileName()).build()))
|
||||
.filter(contentDetectorContext -> StringUtils.isNotEmpty(contentDetectorContext.getMimeType()))
|
||||
.filter(contentDetectorContext -> !APPLICATION_OCTET_STREAM_VALUE
|
||||
.equalsIgnoreCase(contentDetectorContext.getMimeType()))
|
||||
.map(ContentDetectorContext::getMimeType).orElse(APPLICATION_OCTET_STREAM_VALUE);
|
||||
|
||||
request.setType(type);
|
||||
}
|
||||
|
||||
// TODO: make "prefix" configurable?
|
||||
var path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), request.getFileName(),
|
||||
storeService.getDelimiter());
|
||||
|
||||
final var imagePath = storeService.upload(path, request.getStream(), request.getType());
|
||||
|
||||
final var result = imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(request.getEntityType())).findFirst()
|
||||
.flatMap(imageIdAdapter -> imageIdAdapter.get(request.getEntityId()))
|
||||
.flatMap(imageIdResult -> imageRepository.findById(imageIdResult.getId())).map(image -> {
|
||||
// delete old image
|
||||
storeService.delete(image.getLocation());
|
||||
|
||||
return image;
|
||||
}).or(() -> Optional.of(new Image()))
|
||||
.map(image -> image.setLocation(imagePath).setStorageType(storeService.getType().getValue())).map(imageRepository::save)
|
||||
.map(image -> ImageCreateResponse.builder().resourceId(image.getId()).build());
|
||||
|
||||
imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(request.getEntityType())).findFirst()
|
||||
.ifPresent(imageIdAdapter -> result.ifPresent(
|
||||
imageCreateResponse -> imageIdAdapter.set(request.getEntityId(), imageCreateResponse.getResourceId())));
|
||||
|
||||
return result.orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), -1L));
|
||||
} catch (final Exception e) {
|
||||
throw new DocumentInvalidRequestException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public ImageDeleteResponse deleteImage(final ImageDeleteRequest request) {
|
||||
try {
|
||||
return delete(request.getEntityType(), request.getEntityId())
|
||||
.orElseThrow(() -> new DocumentNotFoundException(request.getEntityType(), request.getEntityId(), -1L));
|
||||
} catch (final Exception e) {
|
||||
throw new DocumentInvalidRequestException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<ImageDeleteResponse> delete(final String entityType, final Long entityId) {
|
||||
return imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(entityType)).findFirst()
|
||||
.flatMap(imageIdAdapter -> imageIdAdapter.set(entityId, null))
|
||||
.flatMap(imageIdResult -> imageRepository.findById(imageIdResult.getId())).map(image -> {
|
||||
requireNonNull(image.getId(), "Image ID required");
|
||||
requireNonNull(image.getLocation(), "Image location required");
|
||||
|
||||
storeService.delete(image.getLocation());
|
||||
|
||||
imageRepository.deleteById(image.getId());
|
||||
|
||||
return ImageDeleteResponse.builder().resourceId(entityId).resourceIdentifier(entityType).build();
|
||||
});
|
||||
}
|
||||
|
||||
private String getPath(final String prefix, final String entityType, final Long entityId, final String fileName, String delimiter) {
|
||||
requireNonNull(prefix);
|
||||
requireNonNull(entityType);
|
||||
requireNonNull(entityId);
|
||||
requireNonNull(fileName);
|
||||
requireNonNull(delimiter);
|
||||
|
||||
return String.join(delimiter, prefix, entityType, String.valueOf(entityId), fileName);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -18,14 +18,14 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.event.business.domain.document;
|
||||
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent;
|
||||
|
||||
public abstract class DocumentBusinessEvent extends AbstractBusinessEvent<Document> {
|
||||
public abstract class DocumentBusinessEvent extends AbstractBusinessEvent<DocumentData> {
|
||||
|
||||
private static final String CATEGORY = "Document";
|
||||
|
||||
protected DocumentBusinessEvent(Document value) {
|
||||
protected DocumentBusinessEvent(DocumentData value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -18,13 +18,13 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.event.business.domain.document;
|
||||
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
|
||||
public class DocumentCreatedBusinessEvent extends DocumentBusinessEvent {
|
||||
|
||||
private static final String TYPE = "DocumentCreatedBusinessEvent";
|
||||
|
||||
public DocumentCreatedBusinessEvent(Document value) {
|
||||
public DocumentCreatedBusinessEvent(DocumentData value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -18,13 +18,13 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.event.business.domain.document;
|
||||
|
||||
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
|
||||
public class DocumentDeletedBusinessEvent extends DocumentBusinessEvent {
|
||||
|
||||
private static final String TYPE = "DocumentDeletedBusinessEvent";
|
||||
|
||||
public DocumentDeletedBusinessEvent(Document value) {
|
||||
public DocumentDeletedBusinessEvent(DocumentData value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
|
||||
|
||||
<persistence version="2.0"
|
||||
xmlns="http://java.sun.com/xml/ns/persistence"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
|
||||
|
||||
<!-- This file is only used for static weaving, nothing more. -->
|
||||
<!-- You can find the runtime configuration in the JPAConfig class -->
|
||||
<persistence-unit name="jpa-pu" transaction-type="RESOURCE_LOCAL">
|
||||
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
|
||||
|
||||
<!-- 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>
|
||||
<class>org.apache.fineract.commands.domain.CommandSource</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUser</class>
|
||||
<class>org.apache.fineract.accounting.glaccount.domain.GLAccount</class>
|
||||
<class>org.apache.fineract.organisation.monetary.domain.OrganisationCurrency</class>
|
||||
<class>org.apache.fineract.organisation.staff.domain.Staff</class>
|
||||
<class>org.apache.fineract.portfolio.rate.domain.Rate</class>
|
||||
<class>org.apache.fineract.organisation.monetary.domain.ApplicationCurrency</class>
|
||||
<class>org.apache.fineract.portfolio.calendar.domain.CalendarInstance</class>
|
||||
<class>org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail</class>
|
||||
<class>org.apache.fineract.portfolio.calendar.domain.Calendar</class>
|
||||
<class>org.apache.fineract.portfolio.calendar.domain.CalendarHistory</class>
|
||||
<class>org.apache.fineract.portfolio.client.domain.ClientIdentifier</class>
|
||||
<class>org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket</class>
|
||||
<class>org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
||||
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
||||
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
||||
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.GroupLevel</class>
|
||||
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEvent</class>
|
||||
<class>org.apache.fineract.organisation.office.domain.Office</class>
|
||||
<class>org.apache.fineract.organisation.holiday.domain.Holiday</class>
|
||||
<class>org.apache.fineract.infrastructure.cache.domain.PlatformCache</class>
|
||||
<class>org.apache.fineract.infrastructure.codes.domain.Code</class>
|
||||
<class>org.apache.fineract.infrastructure.businessdate.domain.BusinessDate</class>
|
||||
<class>org.apache.fineract.infrastructure.codes.domain.CodeValue</class>
|
||||
|
||||
<!-- Document Module Entities -->
|
||||
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Document</class>
|
||||
|
||||
<exclude-unlisted-classes>false</exclude-unlisted-classes>
|
||||
<properties>
|
||||
<property name="eclipselink.weaving" value="static" />
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
</persistence>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user