package jnpf.fileStorage.dms;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import jnpf.exception.DataException;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.http.MediaType;
|
import org.springframework.stereotype.Component;
|
import org.springframework.web.util.UriUtils;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.io.File;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.io.UncheckedIOException;
|
import java.net.URI;
|
import java.net.URLConnection;
|
import java.nio.charset.StandardCharsets;
|
import java.nio.file.Files;
|
import java.nio.file.Path;
|
import java.nio.file.StandardCopyOption;
|
import java.util.Iterator;
|
import java.util.Locale;
|
import java.util.function.Function;
|
|
@Component
|
@RequiredArgsConstructor
|
public class DmsCommonFileReader {
|
private static final String FILE_URL_PREFIX = "/api/file/Image/";
|
private static final String FILE_DOWNLOAD_PATH = "/api/file/Download";
|
private static final String ANNEX_TYPE = "annex";
|
|
private final ObjectMapper objectMapper;
|
private final DmsCommonFileStorage commonFileStorage;
|
|
public <T> T withFile(String url, Object fileJson, String documentName,
|
Function<MultipartFile, T> operation) {
|
SourceFile source = resolve(url, fileJson);
|
if (!commonFileStorage.exists(source.type, source.fileName)) {
|
throw new DataException("file-common文件不存在:" + source.fileName);
|
}
|
|
Path temporaryFile = null;
|
try {
|
temporaryFile = Files.createTempFile("dms-workflow-file-", ".tmp");
|
Path target = temporaryFile;
|
commonFileStorage.download(source.type, source.fileName, input -> {
|
try {
|
Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING);
|
} catch (IOException exception) {
|
throw new UncheckedIOException(exception);
|
}
|
});
|
if (Files.size(temporaryFile) == 0L) {
|
throw new DataException("file-common文件内容为空:" + source.fileName);
|
}
|
String originalFilename = originalFilename(
|
fileJson, url, source.fileName, documentName);
|
return operation.apply(new PathMultipartFile(temporaryFile, originalFilename));
|
} catch (UncheckedIOException exception) {
|
throw new DataException("读取file-common文件失败:" + exception.getCause().getMessage());
|
} catch (IOException exception) {
|
throw new DataException("读取file-common文件失败:" + exception.getMessage());
|
} finally {
|
if (temporaryFile != null) {
|
try {
|
Files.deleteIfExists(temporaryFile);
|
} catch (IOException ignored) {
|
// Temporary-file cleanup failure must not hide the DMS result.
|
}
|
}
|
}
|
}
|
|
SourceFile parse(String value) {
|
if (value == null || value.trim().isEmpty()) {
|
throw new DataException("文件URL不能为空");
|
}
|
try {
|
URI uri = URI.create(value.trim());
|
if (uri.getScheme() != null && !"http".equalsIgnoreCase(uri.getScheme())
|
&& !"https".equalsIgnoreCase(uri.getScheme())) {
|
throw new DataException("文件URL仅支持file-common的HTTP路径");
|
}
|
String path = uri.getRawPath();
|
int prefixIndex = path == null ? -1 : path.indexOf(FILE_URL_PREFIX);
|
if (prefixIndex < 0) {
|
throw new DataException("文件URL不是file-common文件路径");
|
}
|
String remainder = path.substring(prefixIndex + FILE_URL_PREFIX.length());
|
String[] parts = remainder.split("/", -1);
|
if (parts.length != 2) {
|
throw new DataException("file-common文件URL格式不正确");
|
}
|
String type = decodeSegment(parts[0]);
|
String fileName = decodeSegment(parts[1]);
|
validateSegment(type, "文件类型");
|
validateSegment(fileName, "文件名");
|
return new SourceFile(type, fileName);
|
} catch (DataException exception) {
|
throw exception;
|
} catch (IllegalArgumentException exception) {
|
throw new DataException("file-common文件URL格式不正确");
|
}
|
}
|
|
private SourceFile resolve(String url, Object fileJson) {
|
try {
|
return parse(url);
|
} catch (DataException original) {
|
if (!isDownloadUrl(url)) {
|
throw original;
|
}
|
JsonNode root = jsonNode(fileJson);
|
String fileId = findFileId(root, url, true);
|
if (fileId == null) {
|
fileId = findFileId(root, url, false);
|
}
|
if (fileId == null) {
|
throw original;
|
}
|
validateSegment(fileId, "文件编号");
|
return new SourceFile(ANNEX_TYPE, fileId);
|
}
|
}
|
|
private boolean isDownloadUrl(String value) {
|
if (value == null || value.trim().isEmpty()) {
|
return false;
|
}
|
try {
|
URI uri = URI.create(value.trim());
|
if (uri.getScheme() != null && !"http".equalsIgnoreCase(uri.getScheme())
|
&& !"https".equalsIgnoreCase(uri.getScheme())) {
|
return false;
|
}
|
String path = uri.getRawPath();
|
return path != null && (FILE_DOWNLOAD_PATH.equals(path)
|
|| path.endsWith(FILE_DOWNLOAD_PATH));
|
} catch (IllegalArgumentException ignored) {
|
return false;
|
}
|
}
|
|
private String decodeSegment(String value) {
|
try {
|
return UriUtils.decode(value, StandardCharsets.UTF_8);
|
} catch (IllegalArgumentException exception) {
|
throw new DataException("file-common文件URL编码不正确");
|
}
|
}
|
|
private void validateSegment(String value, String label) {
|
if (value == null || value.trim().isEmpty() || value.contains("/")
|
|| value.contains("\\") || ".".equals(value) || "..".equals(value)) {
|
throw new DataException(label + "不合法");
|
}
|
}
|
|
private String originalFilename(Object fileJson, String url, String storedFilename,
|
String documentName) {
|
JsonNode root = jsonNode(fileJson);
|
String name = findName(root, url, storedFilename, true);
|
if (name == null) {
|
name = findName(root, url, storedFilename, false);
|
}
|
if (name != null && !name.trim().isEmpty()) {
|
return name.trim();
|
}
|
String extension = extension(storedFilename);
|
String fallback = documentName == null || documentName.trim().isEmpty()
|
? storedFilename : documentName.trim();
|
return extension.isEmpty()
|
|| fallback.toLowerCase(Locale.ROOT).endsWith(extension.toLowerCase(Locale.ROOT))
|
? fallback : fallback + extension;
|
}
|
|
private JsonNode jsonNode(Object fileJson) {
|
if (fileJson == null) {
|
return null;
|
}
|
try {
|
if (fileJson instanceof String) {
|
String text = ((String) fileJson).trim();
|
if (!text.startsWith("{") && !text.startsWith("[")) {
|
return null;
|
}
|
return objectMapper.readTree(text);
|
}
|
return objectMapper.valueToTree(fileJson);
|
} catch (RuntimeException | IOException ignored) {
|
return null;
|
}
|
}
|
|
private String findName(JsonNode node, String url, String storedFilename,
|
boolean requireMatch) {
|
if (node == null) {
|
return null;
|
}
|
if (node.isArray()) {
|
Iterator<JsonNode> items = node.elements();
|
while (items.hasNext()) {
|
String name = findName(items.next(), url, storedFilename, requireMatch);
|
if (name != null) {
|
return name;
|
}
|
}
|
return null;
|
}
|
if (!node.isObject()) {
|
return null;
|
}
|
boolean matches = textEquals(node.get("url"), url)
|
|| textEquals(node.get("fileId"), storedFilename);
|
if (!requireMatch || matches) {
|
String name = text(node.get("name"));
|
if (name == null) {
|
name = text(node.get("fileName"));
|
}
|
if (name != null) {
|
return name;
|
}
|
}
|
Iterator<JsonNode> children = node.elements();
|
while (children.hasNext()) {
|
String name = findName(children.next(), url, storedFilename, requireMatch);
|
if (name != null) {
|
return name;
|
}
|
}
|
return null;
|
}
|
|
private String findFileId(JsonNode node, String url, boolean requireMatch) {
|
if (node == null) {
|
return null;
|
}
|
if (node.isArray()) {
|
Iterator<JsonNode> items = node.elements();
|
while (items.hasNext()) {
|
String fileId = findFileId(items.next(), url, requireMatch);
|
if (fileId != null) {
|
return fileId;
|
}
|
}
|
return null;
|
}
|
if (!node.isObject()) {
|
return null;
|
}
|
boolean matches = textEquals(node.get("url"), url);
|
if (!requireMatch || matches) {
|
String fileId = text(node.get("fileId"));
|
if (fileId != null) {
|
return fileId;
|
}
|
}
|
Iterator<JsonNode> children = node.elements();
|
while (children.hasNext()) {
|
String fileId = findFileId(children.next(), url, requireMatch);
|
if (fileId != null) {
|
return fileId;
|
}
|
}
|
return null;
|
}
|
|
private boolean textEquals(JsonNode node, String expected) {
|
return node != null && node.isTextual() && node.asText().equals(expected);
|
}
|
|
private String text(JsonNode node) {
|
return node == null || !node.isTextual() || node.asText().trim().isEmpty()
|
? null : node.asText();
|
}
|
|
private String extension(String filename) {
|
int index = filename.lastIndexOf('.');
|
return index <= 0 || index == filename.length() - 1 ? "" : filename.substring(index);
|
}
|
|
static final class SourceFile {
|
private final String type;
|
private final String fileName;
|
|
private SourceFile(String type, String fileName) {
|
this.type = type;
|
this.fileName = fileName;
|
}
|
|
String getType() {
|
return type;
|
}
|
|
String getFileName() {
|
return fileName;
|
}
|
}
|
|
private static final class PathMultipartFile implements MultipartFile {
|
private final Path path;
|
private final String originalFilename;
|
|
private PathMultipartFile(Path path, String originalFilename) {
|
this.path = path;
|
this.originalFilename = originalFilename;
|
}
|
|
@Override
|
public String getName() {
|
return "file";
|
}
|
|
@Override
|
public String getOriginalFilename() {
|
return originalFilename;
|
}
|
|
@Override
|
public String getContentType() {
|
String guessed = URLConnection.guessContentTypeFromName(originalFilename);
|
return guessed == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : guessed;
|
}
|
|
@Override
|
public boolean isEmpty() {
|
return getSize() == 0L;
|
}
|
|
@Override
|
public long getSize() {
|
try {
|
return Files.size(path);
|
} catch (IOException exception) {
|
throw new UncheckedIOException(exception);
|
}
|
}
|
|
@Override
|
public byte[] getBytes() throws IOException {
|
return Files.readAllBytes(path);
|
}
|
|
@Override
|
public InputStream getInputStream() throws IOException {
|
return Files.newInputStream(path);
|
}
|
|
@Override
|
public void transferTo(File destination) throws IOException {
|
Files.copy(path, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
}
|
}
|
}
|