package jnpf.dmsService;
|
|
import jnpf.exception.DataException;
|
import jnpf.flowable.entity.TemplateNodeEntity;
|
import jnpf.flowable.enums.NodeEnum;
|
import org.springframework.util.StringUtils;
|
import org.w3c.dom.Element;
|
import org.w3c.dom.NodeList;
|
|
import javax.xml.XMLConstants;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
import java.io.ByteArrayInputStream;
|
import java.net.URLDecoder;
|
import java.nio.charset.StandardCharsets;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
|
/** Shared step order for approval-page annotation and value rendering. */
|
final class DmsQianpiyeNodeOrder {
|
private static final String BPMN_NAMESPACE = "http://www.omg.org/spec/BPMN/20100524/MODEL";
|
|
private DmsQianpiyeNodeOrder() {
|
}
|
|
static List<TemplateNodeEntity> order(String flowXml, List<TemplateNodeEntity> nodes) {
|
if (nodes.stream().noneMatch(DmsQianpiyeNodeOrder::isStep)) return new ArrayList<>(nodes);
|
if (!StringUtils.hasText(flowXml)) throw new DataException("签批页流程缺少连线配置,无法确定步骤顺序");
|
|
Map<String, List<String>> outgoing = new LinkedHashMap<>();
|
List<String> starts = new ArrayList<>();
|
try {
|
String xml = flowXml.trim();
|
if (!xml.startsWith("<")) xml = URLDecoder.decode(xml, StandardCharsets.UTF_8);
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newDefaultInstance();
|
factory.setNamespaceAware(true);
|
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
|
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
|
org.w3c.dom.Document document = factory.newDocumentBuilder().parse(
|
new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
|
NodeList events = document.getElementsByTagNameNS(BPMN_NAMESPACE, "startEvent");
|
for (int index = 0; index < events.getLength(); index++) {
|
starts.add(((Element) events.item(index)).getAttribute("id"));
|
}
|
NodeList connections = document.getElementsByTagNameNS(BPMN_NAMESPACE, "sequenceFlow");
|
for (int index = 0; index < connections.getLength(); index++) {
|
Element connection = (Element) connections.item(index);
|
String source = connection.getAttribute("sourceRef");
|
String target = connection.getAttribute("targetRef");
|
if (!StringUtils.hasText(source) || !StringUtils.hasText(target)) {
|
throw new IllegalArgumentException("流程连线缺少起点或终点");
|
}
|
outgoing.computeIfAbsent(source, key -> new ArrayList<>()).add(target);
|
}
|
} catch (Exception cause) {
|
throw new DataException("签批页流程连线配置无效,无法确定步骤顺序");
|
}
|
if (starts.isEmpty()) throw new DataException("签批页流程缺少开始节点,无法确定步骤顺序");
|
|
// Reverse postorder keeps predecessors before joins. Reverse traversal preserves the
|
// XML order of sibling branches; visited nodes also make loops terminate safely.
|
List<String> codes = new ArrayList<>();
|
Set<String> visited = new HashSet<>();
|
for (int index = starts.size() - 1; index >= 0; index--) {
|
visit(starts.get(index), outgoing, visited, codes);
|
}
|
Collections.reverse(codes);
|
Map<String, TemplateNodeEntity> byCode = new LinkedHashMap<>();
|
for (TemplateNodeEntity node : nodes) {
|
if (isStep(node) && !visited.contains(node.getNodeCode())) {
|
throw new DataException("签批页流程节点未连接到开始节点: " + node.getNodeCode());
|
}
|
if (byCode.putIfAbsent(node.getNodeCode(), node) != null && isStep(node)) {
|
throw new DataException("签批页流程节点编码重复: " + node.getNodeCode());
|
}
|
}
|
List<TemplateNodeEntity> ordered = new ArrayList<>();
|
for (String code : codes) {
|
TemplateNodeEntity node = byCode.remove(code);
|
if (node != null) ordered.add(node);
|
}
|
// Global settings and connection properties are not executable steps, but still carry
|
// form metadata needed by the existing catalog and rendering code.
|
ordered.addAll(byCode.values());
|
return ordered;
|
}
|
|
private static void visit(String code, Map<String, List<String>> outgoing, Set<String> visited,
|
List<String> ordered) {
|
if (!visited.add(code)) return;
|
List<String> targets = outgoing.getOrDefault(code, Collections.emptyList());
|
for (int index = targets.size() - 1; index >= 0; index--) {
|
visit(targets.get(index), outgoing, visited, ordered);
|
}
|
ordered.add(code);
|
}
|
|
private static boolean isStep(TemplateNodeEntity node) {
|
return NodeEnum.START.getType().equals(node.getNodeType())
|
|| NodeEnum.APPROVER.getType().equals(node.getNodeType());
|
}
|
}
|