刘光辉
4 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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());
    }
}