XmlUtils.java 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. package com.telerobot.fs.utils;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.w3c.dom.Document;
  5. import org.w3c.dom.Element;
  6. import org.w3c.dom.Node;
  7. import org.w3c.dom.NodeList;
  8. import javax.xml.parsers.DocumentBuilder;
  9. import javax.xml.parsers.DocumentBuilderFactory;
  10. import java.io.ByteArrayInputStream;
  11. import java.nio.charset.StandardCharsets;
  12. import java.util.regex.Matcher;
  13. import java.util.regex.Pattern;
  14. public class XmlUtils {
  15. protected final static Logger logger = LoggerFactory.getLogger(XmlUtils.class);
  16. /** 从 contact 标签中提取 sip:user@host:port 的 host:port */
  17. private static final Pattern CONTACT_HOST_PORT = Pattern.compile(
  18. "sip:[^@;>\\s]+@([^:;>\\s]+):(\\d+)", Pattern.CASE_INSENSITIVE);
  19. /**
  20. * 从 sofia xmlstatus profile X reg 的 XML 中解析已注册分机的 Contact(ip:port)。
  21. * 匹配规则与 GUI FsConfController.convertProfileRegExtnumXml 对齐:
  22. * 1) sip-auth-user 等于目标分机;或 2) user(@ 前部分)等于目标分机。
  23. * 地址优先取 network-ip:network-port(与 GUI 展示一致,为 NAT 后公网地址);
  24. * 若缺失再从 contact 的 sip URI 回退解析。
  25. * 注意:反向注册场景 ESL 事件常见 username=unknown,sip-auth-user 可能为空/unknown,
  26. * 仅匹配 sip-auth-user 会导致漏找(GUI 能显示而 callcenter 解析不到)。
  27. *
  28. * @param xml sofia xmlstatus 响应全文
  29. * @param extensionNumber 网关 authUsername / 分机号
  30. * @return ip:port,未找到返回空串
  31. */
  32. public static String parseFsOnlineUserListXml(String xml, String extensionNumber) {
  33. if (StringUtils.isNullOrEmpty(xml) || StringUtils.isNullOrEmpty(extensionNumber)) {
  34. return "";
  35. }
  36. String target = extensionNumber.trim();
  37. try {
  38. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  39. DocumentBuilder builder = factory.newDocumentBuilder();
  40. Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
  41. NodeList registrations = doc.getElementsByTagName("registration");
  42. int regCount = registrations.getLength();
  43. logger.info("parseFsOnlineUserListXml: registration条数={}, 目标分机={}", regCount, target);
  44. for (int i = 0; i < regCount; i++) {
  45. Element registration = (Element) registrations.item(i);
  46. String sipAuthUser = getChildText(registration, "sip-auth-user");
  47. String userRaw = getChildText(registration, "user");
  48. String userPart = extractUserPart(userRaw);
  49. boolean matchedByAuth = target.equals(sipAuthUser);
  50. boolean matchedByUser = target.equals(userPart);
  51. if (!matchedByAuth && !matchedByUser) {
  52. continue;
  53. }
  54. String matchBy = matchedByAuth ? "sip-auth-user" : "user";
  55. String networkIp = getChildText(registration, "network-ip");
  56. String networkPort = getChildText(registration, "network-port");
  57. String contactTag = getChildText(registration, "contact");
  58. String contact;
  59. if (!StringUtils.isNullOrEmpty(networkIp) && !StringUtils.isNullOrEmpty(networkPort)) {
  60. contact = networkIp + ":" + networkPort;
  61. } else {
  62. contact = extractHostPortFromContact(contactTag);
  63. logger.info("parseFsOnlineUserListXml: network-ip/port 缺失,回退解析 contact 标签,matchBy={}, contact={}", matchBy, contact);
  64. }
  65. if (StringUtils.isNullOrEmpty(contact)) {
  66. logger.warn("parseFsOnlineUserListXml: 已匹配 registration 但 Contact 为空,matchBy={}, user={}, sip-auth-user={}, contactTag={}", matchBy, userRaw, sipAuthUser, contactTag);
  67. continue;
  68. }
  69. logger.info("parseFsOnlineUserListXml: 匹配成功 index={}, matchBy={}, user={}, sip-auth-user={}, contact={}", i, matchBy, userRaw, sipAuthUser, contact);
  70. return contact;
  71. }
  72. // 未命中时输出少量候选,便于对比 GUI「查看状态」
  73. int sample = Math.min(regCount, 5);
  74. for (int i = 0; i < sample; i++) {
  75. Element registration = (Element) registrations.item(i);
  76. logger.info("parseFsOnlineUserListXml: 未命中样例[{}] user={}, sip-auth-user={}, network={}:{}",
  77. i,
  78. getChildText(registration, "user"),
  79. getChildText(registration, "sip-auth-user"),
  80. getChildText(registration, "network-ip"),
  81. getChildText(registration, "network-port"));
  82. }
  83. } catch (Throwable e) {
  84. logger.error("parseFsOnlineUserListXml error! {} {}", e.toString(),
  85. CommonUtils.getStackTraceString(e.getStackTrace()));
  86. // DOM 失败时(contact 含未转义 <> 等)用正则兜底,对齐 GUI 的 user + network-ip
  87. String fallback = parseFsOnlineUserListXmlByRegex(xml, target);
  88. if (!StringUtils.isNullOrEmpty(fallback)) {
  89. return fallback;
  90. }
  91. }
  92. return "";
  93. }
  94. /**
  95. * DOM 解析失败时的正则兜底:按 <user>分机@...</user> 定位 registration 片段,再取 network-ip/port。
  96. */
  97. private static String parseFsOnlineUserListXmlByRegex(String xml, String target) {
  98. try {
  99. Pattern regBlock = Pattern.compile("<registration>([\\s\\S]*?)</registration>", Pattern.CASE_INSENSITIVE);
  100. Matcher m = regBlock.matcher(xml);
  101. int idx = 0;
  102. while (m.find()) {
  103. String block = m.group(1);
  104. String sipAuthUser = extractXmlTag(block, "sip-auth-user");
  105. String userRaw = extractXmlTag(block, "user");
  106. String userPart = extractUserPart(userRaw);
  107. boolean matched = target.equals(sipAuthUser) || target.equals(userPart);
  108. if (!matched) {
  109. idx++;
  110. continue;
  111. }
  112. String networkIp = extractXmlTag(block, "network-ip");
  113. String networkPort = extractXmlTag(block, "network-port");
  114. String contact;
  115. if (!StringUtils.isNullOrEmpty(networkIp) && !StringUtils.isNullOrEmpty(networkPort)) {
  116. contact = networkIp + ":" + networkPort;
  117. } else {
  118. contact = extractHostPortFromContact(extractXmlTag(block, "contact"));
  119. }
  120. if (!StringUtils.isNullOrEmpty(contact)) {
  121. logger.info("parseFsOnlineUserListXml: 正则兜底匹配成功 index={}, user={}, sip-auth-user={}, contact={}", idx, userRaw, sipAuthUser, contact);
  122. return contact;
  123. }
  124. idx++;
  125. }
  126. } catch (Throwable e) {
  127. logger.error("parseFsOnlineUserListXmlByRegex error! {}", e.toString());
  128. }
  129. return "";
  130. }
  131. private static String getChildText(Element parent, String tagName) {
  132. NodeList list = parent.getElementsByTagName(tagName);
  133. if (list == null || list.getLength() == 0) {
  134. return "";
  135. }
  136. Node node = list.item(0);
  137. if (node == null || node.getTextContent() == null) {
  138. return "";
  139. }
  140. return node.getTextContent().trim();
  141. }
  142. private static String extractUserPart(String userRaw) {
  143. if (StringUtils.isNullOrEmpty(userRaw)) {
  144. return "";
  145. }
  146. int at = userRaw.indexOf('@');
  147. if (at > 0) {
  148. return userRaw.substring(0, at).trim();
  149. }
  150. return userRaw.trim();
  151. }
  152. private static String extractHostPortFromContact(String contactTag) {
  153. if (StringUtils.isNullOrEmpty(contactTag)) {
  154. return "";
  155. }
  156. Matcher matcher = CONTACT_HOST_PORT.matcher(contactTag);
  157. if (matcher.find()) {
  158. return matcher.group(1) + ":" + matcher.group(2);
  159. }
  160. return "";
  161. }
  162. private static String extractXmlTag(String block, String tagName) {
  163. Pattern p = Pattern.compile("<" + tagName + ">\\s*([^<]*?)\\s*</" + tagName + ">",
  164. Pattern.CASE_INSENSITIVE);
  165. Matcher m = p.matcher(block);
  166. if (m.find()) {
  167. return m.group(1).trim();
  168. }
  169. return "";
  170. }
  171. }