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 withFile(String url, Object fileJson, String documentName, Function 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 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 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 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 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); } } }