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*" + tagName + ">",
Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(block);
if (m.find()) {
return m.group(1).trim();
}
return "";
}
}