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 order(String flowXml, List nodes) { if (nodes.stream().noneMatch(DmsQianpiyeNodeOrder::isStep)) return new ArrayList<>(nodes); if (!StringUtils.hasText(flowXml)) throw new DataException("签批页流程缺少连线配置,无法确定步骤顺序"); Map> outgoing = new LinkedHashMap<>(); List 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 codes = new ArrayList<>(); Set visited = new HashSet<>(); for (int index = starts.size() - 1; index >= 0; index--) { visit(starts.get(index), outgoing, visited, codes); } Collections.reverse(codes); Map 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 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> outgoing, Set visited, List ordered) { if (!visited.add(code)) return; List 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()); } }