2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.yamahareceiver.internal.protocol.xml;
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.util.stream.IntStream;
18 import java.util.stream.Stream;
20 import javax.xml.parsers.DocumentBuilderFactory;
21 import javax.xml.parsers.ParserConfigurationException;
23 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
24 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27 import org.w3c.dom.Document;
28 import org.w3c.dom.Element;
29 import org.w3c.dom.Node;
30 import org.w3c.dom.NodeList;
31 import org.xml.sax.InputSource;
32 import org.xml.sax.SAXException;
35 * Utility methods for XML handling
37 * @author David Graeff - Initial contribution
38 * @author Tomasz Maruszak - DAB support, Spotify support, refactoring, input name conversion fix, Input mapping fix
40 public class XMLUtils {
42 private static final Logger LOG = LoggerFactory.getLogger(XMLUtils.class);
44 // We need a lot of xml parsing. Create a document builder beforehand.
45 static final DocumentBuilderFactory DBF = DocumentBuilderFactory.newInstance();
47 static Node getNode(Node parent, String[] nodePath, int offset) {
51 if (offset < nodePath.length - 1) {
52 return getNode(((Element) parent).getElementsByTagName(nodePath[offset]).item(0), nodePath, offset + 1);
54 return ((Element) parent).getElementsByTagName(nodePath[offset]).item(0);
58 static Node getNode(Node root, String nodePath) {
59 String[] nodePathArr = nodePath.split("/");
60 return getNode(root, nodePathArr, 0);
63 static Stream<Element> getChildElements(Node node) {
65 return Stream.empty();
67 return toStream(node.getChildNodes()).filter(x -> x.getNodeType() == Node.ELEMENT_NODE).map(x -> (Element) x);
70 static Stream<Node> toStream(NodeList nodeList) {
71 return IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item);
75 * Retrieves the child node according to the xpath expression.
80 * @throws ReceivedMessageParseException when the child node does not exist throws
81 * {@link ReceivedMessageParseException}.
83 static Node getNodeOrFail(Node root, String nodePath) throws ReceivedMessageParseException {
84 Node node = getNode(root, nodePath);
86 throw new ReceivedMessageParseException(nodePath + " child in parent node missing!");
92 * Finds the node starting with the root and following the path. If the node is found it's inner text is returned,
93 * otherwise the default provided value.
100 public static String getNodeContentOrDefault(Node root, String nodePath, String defaultValue) {
101 Node node = getNode(root, nodePath);
103 return node.getTextContent();
109 * Finds the node starting with the root and following the path.
110 * If the node is found it's inner text is returned, otherwise the default provided value.
111 * The first path that exists is returned.
115 * @param defaultValue
118 public static String getAnyNodeContentOrDefault(Node root, String defaultValue, String... nodePaths) {
119 for (String nodePath : nodePaths) {
120 String value = getNodeContentOrDefault(root, nodePath, (String) null);
129 * Finds the node starting with the root and following the path. If the node is found it's inner text is returned,
130 * otherwise the default provided value.
136 public static String getNodeContentOrEmpty(Node root, String nodePath) {
137 return getNodeContentOrDefault(root, nodePath, "");
141 * Finds the node starting with the root and following the path. If the node is found it's inner text is returned,
142 * otherwise the default provided value.
146 * @param defaultValue
149 public static Integer getNodeContentOrDefault(Node root, String nodePath, Integer defaultValue) {
150 Node node = getNode(root, nodePath);
153 return Integer.valueOf(node.getTextContent());
154 } catch (NumberFormatException e) {
156 "The value '{}' of node with path {} could not been parsed to an integer. Applying default of {}",
157 node.getTextContent(), nodePath, defaultValue);
164 * Parse the given xml message into a xml document node.
166 * @param message XML formatted message.
167 * @return Return the response as xml node or throws an exception if response is not xml.
168 * @throws IOException
170 public static Document xml(String message) throws IOException, ReceivedMessageParseException {
171 // Ensure the message contains XML declaration
172 String response = message.startsWith("<?xml") ? message
173 : "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + message;
176 // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
177 DBF.setFeature("http://xml.org/sax/features/external-general-entities", false);
178 DBF.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
179 DBF.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
180 DBF.setXIncludeAware(false);
181 DBF.setExpandEntityReferences(false);
182 return DBF.newDocumentBuilder().parse(new InputSource(new StringReader(response)));
183 } catch (SAXException | ParserConfigurationException e) {
184 throw new ReceivedMessageParseException(e);
189 * Wraps the XML message with the zone tags. Example with zone=Main_Zone:
190 * <Main_Zone>message</Main_Zone>.
192 * @param message XML message
195 public static String wrZone(Zone zone, String message) {
196 return "<" + zone.name() + ">" + message + "</" + zone.name() + ">";