|
|
@@ -10,46 +10,180 @@ 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);
|
|
|
+
|
|
|
/**
|
|
|
- * parse freeSWITCH online registration user list info.
|
|
|
- * @param xml
|
|
|
- * @return
|
|
|
+ * 从 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)){
|
|
|
+ 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()));
|
|
|
+ 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 < registrations.getLength(); i++) {
|
|
|
+ for (int i = 0; i < regCount; i++) {
|
|
|
Element registration = (Element) registrations.item(i);
|
|
|
|
|
|
- // get sip-auth-user element
|
|
|
- Node sipAuthUserNode = registration.getElementsByTagName("sip-auth-user").item(0);
|
|
|
- if (sipAuthUserNode != null && extensionNumber.equals(sipAuthUserNode.getTextContent())) {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
|
|
|
- // get network-ip和network-port
|
|
|
- String networkIp = registration.getElementsByTagName("network-ip")
|
|
|
- .item(0).getTextContent();
|
|
|
- String networkPort = registration.getElementsByTagName("network-port")
|
|
|
- .item(0).getTextContent();
|
|
|
+ if (StringUtils.isNullOrEmpty(contact)) {
|
|
|
+ logger.warn("parseFsOnlineUserListXml: 已匹配 registration 但 Contact 为空,matchBy={}, user={}, sip-auth-user={}, contactTag={}", matchBy, userRaw, sipAuthUser, contactTag);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- return networkIp + ":" + networkPort;
|
|
|
+ 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 解析失败时的正则兜底:按 <user>分机@...</user> 定位 registration 片段,再取 network-ip/port。
|
|
|
+ */
|
|
|
+ private static String parseFsOnlineUserListXmlByRegex(String xml, String target) {
|
|
|
+ try {
|
|
|
+ Pattern regBlock = Pattern.compile("<registration>([\\s\\S]*?)</registration>", 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("parseFsOnlineUserListXml error! {} {}", e.toString(), CommonUtils.getStackTraceString(e.getStackTrace()) );
|
|
|
+ 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 "";
|
|
|
+ }
|
|
|
}
|