]> git.basschouten.com Git - openhab-addons.git/blob
41a8fc43f10a26d3a83acc9480e44c5b24cdf734
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.yamahareceiver.internal.protocol.xml;
14
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.util.stream.IntStream;
18 import java.util.stream.Stream;
19
20 import javax.xml.parsers.DocumentBuilderFactory;
21 import javax.xml.parsers.ParserConfigurationException;
22
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;
33
34 /**
35  * Utility methods for XML handling
36  *
37  * @author David Graeff - Initial contribution
38  * @author Tomasz Maruszak - DAB support, Spotify support, refactoring, input name conversion fix, Input mapping fix
39  */
40 public class XMLUtils {
41
42     private static final Logger LOG = LoggerFactory.getLogger(XMLUtils.class);
43
44     // We need a lot of xml parsing. Create a document builder beforehand.
45     static final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
46
47     static Node getNode(Node parent, String[] nodePath, int offset) {
48         if (parent == null) {
49             return null;
50         }
51         if (offset < nodePath.length - 1) {
52             return getNode(((Element) parent).getElementsByTagName(nodePath[offset]).item(0), nodePath, offset + 1);
53         } else {
54             return ((Element) parent).getElementsByTagName(nodePath[offset]).item(0);
55         }
56     }
57
58     static Node getNode(Node root, String nodePath) {
59         String[] nodePathArr = nodePath.split("/");
60         return getNode(root, nodePathArr, 0);
61     }
62
63     static Stream<Element> getChildElements(Node node) {
64         if (node == null) {
65             return Stream.empty();
66         }
67         return toStream(node.getChildNodes()).filter(x -> x.getNodeType() == Node.ELEMENT_NODE).map(x -> (Element) x);
68     }
69
70     static Stream<Node> toStream(NodeList nodeList) {
71         return IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item);
72     }
73
74     /**
75      * Retrieves the child node according to the xpath expression.
76      *
77      * @param root
78      * @param nodePath
79      * @return
80      * @throws ReceivedMessageParseException when the child node does not exist throws
81      *             {@link ReceivedMessageParseException}.
82      */
83     static Node getNodeOrFail(Node root, String nodePath) throws ReceivedMessageParseException {
84         Node node = getNode(root, nodePath);
85         if (node == null) {
86             throw new ReceivedMessageParseException(nodePath + " child in parent node missing!");
87         }
88         return node;
89     }
90
91     /**
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.
94      *
95      * @param root
96      * @param nodePath
97      * @param defaultValue
98      * @return
99      */
100     public static String getNodeContentOrDefault(Node root, String nodePath, String defaultValue) {
101         Node node = getNode(root, nodePath);
102         if (node != null) {
103             return node.getTextContent();
104         }
105         return defaultValue;
106     }
107
108     /**
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.
112      *
113      * @param root
114      * @param nodePaths
115      * @param defaultValue
116      * @return
117      */
118     public static String getAnyNodeContentOrDefault(Node root, String defaultValue, String... nodePaths) {
119         for (String nodePath : nodePaths) {
120             String value = getNodeContentOrDefault(root, nodePath, (String) null);
121             if (value != null) {
122                 return value;
123             }
124         }
125         return defaultValue;
126     }
127
128     /**
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.
131      *
132      * @param root
133      * @param nodePath
134      * @return
135      */
136     public static String getNodeContentOrEmpty(Node root, String nodePath) {
137         return getNodeContentOrDefault(root, nodePath, "");
138     }
139
140     /**
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.
143      *
144      * @param root
145      * @param nodePath
146      * @param defaultValue
147      * @return
148      */
149     public static Integer getNodeContentOrDefault(Node root, String nodePath, Integer defaultValue) {
150         Node node = getNode(root, nodePath);
151         if (node != null) {
152             try {
153                 return Integer.valueOf(node.getTextContent());
154             } catch (NumberFormatException e) {
155                 LOG.trace(
156                         "The value '{}' of node with path {} could not been parsed to an integer. Applying default of {}",
157                         node.getTextContent(), nodePath, defaultValue);
158             }
159         }
160         return defaultValue;
161     }
162
163     /**
164      * Parse the given xml message into a xml document node.
165      *
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
169      */
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;
174
175         try {
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);
185         }
186     }
187
188     /**
189      * Wraps the XML message with the zone tags. Example with zone=Main_Zone:
190      * <Main_Zone>message</Main_Zone>.
191      *
192      * @param message XML message
193      * @return
194      */
195     public static String wrZone(Zone zone, String message) {
196         return "<" + zone.name() + ">" + message + "</" + zone.name() + ">";
197     }
198 }