package com.telerobot.fs.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; public class XmlUtils { protected final static Logger logger = LoggerFactory.getLogger(XmlUtils.class); /** 从 contact 标签中提取 sip:user@host:port 的 host:port */ private static final Pattern CONTACT_HOST_PORT = Pattern.compile( "sip:[^@;>\\s]+@([^:;>\\s]+):(\\d+)", Pattern.CASE_INSENSITIVE); /** * 从 sofia xmlstatus profile X reg 的 XML 中解析已注册分机的 Contact(ip:port)。 * 匹配规则与 GUI FsConfController.convertProfileRegExtnumXml 对齐: * 1) sip-auth-user 等于目标分机;或 2) user(@ 前部分)等于目标分机。 * 地址优先取 network-ip:network-port(与 GUI 展示一致,为 NAT 后公网地址); * 若缺失再从 contact 的 sip URI 回退解析。 * 注意:反向注册场景 ESL 事件常见 username=unknown,sip-auth-user 可能为空/unknown, * 仅匹配 sip-auth-user 会导致漏找(GUI 能显示而 callcenter 解析不到)。 * * @param xml sofia xmlstatus 响应全文 * @param extensionNumber 网关 authUsername / 分机号 * @return ip:port,未找到返回空串 */ public static String parseFsOnlineUserListXml(String xml, String extensionNumber) { if (StringUtils.isNullOrEmpty(xml) || StringUtils.isNullOrEmpty(extensionNumber)) { return ""; } String target = extensionNumber.trim(); try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); NodeList registrations = doc.getElementsByTagName("registration"); int regCount = registrations.getLength(); logger.info("parseFsOnlineUserListXml: registration条数={}, 目标分机={}", regCount, target); for (int i = 0; i < regCount; i++) { Element registration = (Element) registrations.item(i); String sipAuthUser = getChildText(registration, "sip-auth-user"); String userRaw = getChildText(registration, "user"); String userPart = extractUserPart(userRaw); boolean matchedByAuth = target.equals(sipAuthUser); boolean matchedByUser = target.equals(userPart); if (!matchedByAuth && !matchedByUser) { continue; } String matchBy = matchedByAuth ? "sip-auth-user" : "user"; String networkIp = getChildText(registration, "network-ip"); String networkPort = getChildText(registration, "network-port"); String contactTag = getChildText(registration, "contact"); String contact; if (!StringUtils.isNullOrEmpty(networkIp) && !StringUtils.isNullOrEmpty(networkPort)) { contact = networkIp + ":" + networkPort; } else { contact = extractHostPortFromContact(contactTag); logger.info("parseFsOnlineUserListXml: network-ip/port 缺失,回退解析 contact 标签,matchBy={}, contact={}", matchBy, contact); } if (StringUtils.isNullOrEmpty(contact)) { logger.warn("parseFsOnlineUserListXml: 已匹配 registration 但 Contact 为空,matchBy={}, user={}, sip-auth-user={}, contactTag={}", matchBy, userRaw, sipAuthUser, contactTag); continue; } logger.info("parseFsOnlineUserListXml: 匹配成功 index={}, matchBy={}, user={}, sip-auth-user={}, contact={}", i, matchBy, userRaw, sipAuthUser, contact); return contact; } // 未命中时输出少量候选,便于对比 GUI「查看状态」 int sample = Math.min(regCount, 5); for (int i = 0; i < sample; i++) { Element registration = (Element) registrations.item(i); logger.info("parseFsOnlineUserListXml: 未命中样例[{}] user={}, sip-auth-user={}, network={}:{}", i, getChildText(registration, "user"), getChildText(registration, "sip-auth-user"), getChildText(registration, "network-ip"), getChildText(registration, "network-port")); } } catch (Throwable e) { logger.error("parseFsOnlineUserListXml error! {} {}", e.toString(), CommonUtils.getStackTraceString(e.getStackTrace())); // DOM 失败时(contact 含未转义 <> 等)用正则兜底,对齐 GUI 的 user + network-ip String fallback = parseFsOnlineUserListXmlByRegex(xml, target); if (!StringUtils.isNullOrEmpty(fallback)) { return fallback; } } return ""; } /** * DOM 解析失败时的正则兜底:按 分机@... 定位 registration 片段,再取 network-ip/port。 */ private static String parseFsOnlineUserListXmlByRegex(String xml, String target) { try { Pattern regBlock = Pattern.compile("([\\s\\S]*?)", Pattern.CASE_INSENSITIVE); Matcher m = regBlock.matcher(xml); int idx = 0; while (m.find()) { String block = m.group(1); String sipAuthUser = extractXmlTag(block, "sip-auth-user"); String userRaw = extractXmlTag(block, "user"); String userPart = extractUserPart(userRaw); boolean matched = target.equals(sipAuthUser) || target.equals(userPart); if (!matched) { idx++; continue; } String networkIp = extractXmlTag(block, "network-ip"); String networkPort = extractXmlTag(block, "network-port"); String contact; if (!StringUtils.isNullOrEmpty(networkIp) && !StringUtils.isNullOrEmpty(networkPort)) { contact = networkIp + ":" + networkPort; } else { contact = extractHostPortFromContact(extractXmlTag(block, "contact")); } if (!StringUtils.isNullOrEmpty(contact)) { logger.info("parseFsOnlineUserListXml: 正则兜底匹配成功 index={}, user={}, sip-auth-user={}, contact={}", idx, userRaw, sipAuthUser, contact); return contact; } idx++; } } catch (Throwable e) { logger.error("parseFsOnlineUserListXmlByRegex error! {}", e.toString()); } return ""; } private static String getChildText(Element parent, String tagName) { NodeList list = parent.getElementsByTagName(tagName); if (list == null || list.getLength() == 0) { return ""; } Node node = list.item(0); if (node == null || node.getTextContent() == null) { return ""; } return node.getTextContent().trim(); } private static String extractUserPart(String userRaw) { if (StringUtils.isNullOrEmpty(userRaw)) { return ""; } int at = userRaw.indexOf('@'); if (at > 0) { return userRaw.substring(0, at).trim(); } return userRaw.trim(); } private static String extractHostPortFromContact(String contactTag) { if (StringUtils.isNullOrEmpty(contactTag)) { return ""; } Matcher matcher = CONTACT_HOST_PORT.matcher(contactTag); if (matcher.find()) { return matcher.group(1) + ":" + matcher.group(2); } return ""; } private static String extractXmlTag(String block, String tagName) { Pattern p = Pattern.compile("<" + tagName + ">\\s*([^<]*?)\\s*", Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(block); if (m.find()) { return m.group(1).trim(); } return ""; } }