2 * Copyright (c) 2010-2024 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.insteon.internal.message;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.HashMap;
18 import java.util.LinkedHashMap;
20 import java.util.Map.Entry;
22 import javax.xml.parsers.DocumentBuilder;
23 import javax.xml.parsers.DocumentBuilderFactory;
24 import javax.xml.parsers.ParserConfigurationException;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.openhab.binding.insteon.internal.utils.Pair;
28 import org.openhab.binding.insteon.internal.utils.Utils.DataTypeParser;
29 import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
30 import org.w3c.dom.Document;
31 import org.w3c.dom.Element;
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
34 import org.xml.sax.SAXException;
37 * Reads the Msg definitions from an XML file
39 * @author Daniel Pfrommer - Initial contribution
40 * @author Rob Nielsen - Port to openHAB 2 insteon binding
43 public class XMLMessageReader {
45 * Reads the message definitions from an xml file
47 * @param input input stream from which to read
48 * @return what was read from file: the map between clear text string and Msg objects
49 * @throws IOException couldn't read file etc
50 * @throws ParsingException something wrong with the file format
51 * @throws FieldException something wrong with the field definition
53 public static Map<String, Msg> readMessageDefinitions(InputStream input)
54 throws IOException, ParsingException, FieldException {
55 Map<String, Msg> messageMap = new HashMap<>();
57 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
58 // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
59 dbFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
60 dbFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
61 dbFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
62 dbFactory.setXIncludeAware(false);
63 dbFactory.setExpandEntityReferences(false);
64 DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
66 Document doc = dBuilder.parse(input);
67 doc.getDocumentElement().normalize();
69 Node root = doc.getDocumentElement();
71 NodeList nodes = root.getChildNodes();
73 for (int i = 0; i < nodes.getLength(); i++) {
74 Node node = nodes.item(i);
75 if (node.getNodeType() == Node.ELEMENT_NODE) {
76 if ("msg".equals(node.getNodeName())) {
77 Pair<String, Msg> msgDef = readMessageDefinition((Element) node);
78 messageMap.put(msgDef.getKey(), msgDef.getValue());
82 } catch (SAXException e) {
83 throw new ParsingException("Failed to parse XML!", e);
84 } catch (ParserConfigurationException e) {
85 throw new ParsingException("Got parser config exception! ", e);
90 private static Pair<String, Msg> readMessageDefinition(Element msg) throws FieldException, ParsingException {
93 LinkedHashMap<Field, Object> fieldMap = new LinkedHashMap<>();
94 String dir = msg.getAttribute("direction");
95 String name = msg.getAttribute("name");
96 Msg.Direction direction = Msg.Direction.getDirectionFromString(dir);
98 if (msg.hasAttribute("length")) {
99 length = Integer.parseInt(msg.getAttribute("length"));
102 NodeList nodes = msg.getChildNodes();
106 for (int i = 0; i < nodes.getLength(); i++) {
107 Node node = nodes.item(i);
108 if (node.getNodeType() == Node.ELEMENT_NODE) {
109 if ("header".equals(node.getNodeName())) {
110 int o = readHeaderElement((Element) node, fieldMap);
112 // Increment the offset by the header length
115 Pair<Field, Object> field = readField((Element) node, offset);
116 fieldMap.put(field.getKey(), field.getValue());
117 // Increment the offset
118 offset += field.getKey().getType().getSize();
122 if (offset != length) {
123 throw new ParsingException(
124 "Actual msg length " + offset + " differs from given msg length " + length + "!");
130 return new Pair<>(name, createMsg(fieldMap, length, hlength, direction));
133 private static int readHeaderElement(Element header, LinkedHashMap<Field, Object> fields) throws ParsingException {
135 int headerLen = Integer.parseInt(header.getAttribute("length"));
137 NodeList nodes = header.getChildNodes();
138 for (int i = 0; i < nodes.getLength(); i++) {
139 Node node = nodes.item(i);
140 if (node.getNodeType() == Node.ELEMENT_NODE) {
141 Pair<Field, Object> definition = readField((Element) node, offset);
142 offset += definition.getKey().getType().getSize();
143 fields.put(definition.getKey(), definition.getValue());
146 if (headerLen != offset) {
147 throw new ParsingException(
148 "Actual header length " + offset + " differs from given length " + headerLen + "!");
153 private static Pair<Field, Object> readField(Element field, int offset) {
154 DataType dType = DataType.getDataType(field.getTagName());
155 // Will return blank if no name attribute
156 String name = field.getAttribute("name");
157 Field f = new Field(name, dType, offset);
158 // Now we have field, only need value
159 String sVal = field.getTextContent();
160 Object val = DataTypeParser.parseDataType(dType, sVal);
161 return new Pair<>(f, val);
164 private static Msg createMsg(HashMap<Field, Object> values, int length, int headerLength, Msg.Direction dir)
165 throws FieldException {
166 Msg msg = new Msg(headerLength, new byte[length], length, dir);
167 for (Entry<Field, Object> e : values.entrySet()) {
168 Field f = e.getKey();
169 byte[] data = msg.getData();
171 f.set(data, e.getValue());
173 throw new FieldException("data is null");
175 if (!"".equals(f.getName())) {