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.device;
16 import java.io.FileInputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.util.HashMap;
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.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.insteon.internal.device.DeviceType.FeatureGroup;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.w3c.dom.Document;
32 import org.w3c.dom.Element;
33 import org.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35 import org.xml.sax.SAXException;
38 * Reads the device types from an xml file.
40 * @author Daniel Pfrommer - Initial contribution
41 * @author Bernd Pfrommer - openHAB 1 insteonplm binding
42 * @author Rob Nielsen - Port to openHAB 2 insteon binding
45 public class DeviceTypeLoader {
46 private static final Logger logger = LoggerFactory.getLogger(DeviceTypeLoader.class);
47 private Map<String, DeviceType> deviceTypes = new HashMap<>();
48 private static DeviceTypeLoader deviceTypeLoader = new DeviceTypeLoader();
50 private DeviceTypeLoader() {
51 } // private so nobody can call it
54 * Finds the device type for a given product key
56 * @param aProdKey product key to search for
57 * @return the device type, or null if not found
59 public @Nullable DeviceType getDeviceType(String aProdKey) {
60 return (deviceTypes.get(aProdKey));
64 * Must call loadDeviceTypesXML() before calling this function!
66 * @return currently known device types
68 public Map<String, DeviceType> getDeviceTypes() {
73 * Reads the device types from input stream and stores them in memory for
76 * @param in the input stream from which to read
78 public void loadDeviceTypesXML(InputStream in) throws ParserConfigurationException, SAXException, IOException {
79 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
80 // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
81 dbFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
82 dbFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
83 dbFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
84 dbFactory.setXIncludeAware(false);
85 dbFactory.setExpandEntityReferences(false);
86 DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
87 Document doc = dBuilder.parse(in);
88 doc.getDocumentElement().normalize();
89 Node root = doc.getDocumentElement();
90 NodeList nodes = root.getChildNodes();
91 for (int i = 0; i < nodes.getLength(); i++) {
92 Node node = nodes.item(i);
93 if (node.getNodeType() == Node.ELEMENT_NODE && "device".equals(node.getNodeName())) {
94 processDevice((Element) node);
100 * Reads the device types from file and stores them in memory for later access.
102 * @param aFileName The name of the file to read from
103 * @throws ParserConfigurationException
104 * @throws SAXException
105 * @throws IOException
107 public void loadDeviceTypesXML(String aFileName) throws ParserConfigurationException, SAXException, IOException {
108 File file = new File(aFileName);
109 InputStream in = new FileInputStream(file);
110 loadDeviceTypesXML(in);
114 * Process device node
116 * @param e name of the element to process
117 * @throws SAXException
119 private void processDevice(Element e) throws SAXException {
120 String productKey = e.getAttribute("productKey");
121 if ("".equals(productKey)) {
122 throw new SAXException("device in device_types file has no product key!");
124 if (deviceTypes.containsKey(productKey)) {
125 logger.warn("overwriting previous definition of device {}", productKey);
126 deviceTypes.remove(productKey);
128 DeviceType devType = new DeviceType(productKey);
130 NodeList nodes = e.getChildNodes();
131 for (int i = 0; i < nodes.getLength(); i++) {
132 Node node = nodes.item(i);
133 if (node.getNodeType() != Node.ELEMENT_NODE) {
136 Element subElement = (Element) node;
137 String nodeName = subElement.getNodeName();
138 if ("model".equals(nodeName)) {
139 devType.setModel(subElement.getTextContent());
140 } else if ("description".equals(nodeName)) {
141 devType.setDescription(subElement.getTextContent());
142 } else if ("feature".equals(nodeName)) {
143 processFeature(devType, subElement);
144 } else if ("feature_group".equals(nodeName)) {
145 processFeatureGroup(devType, subElement);
147 deviceTypes.put(productKey, devType);
151 private String processFeature(DeviceType devType, Element e) throws SAXException {
152 String name = e.getAttribute("name");
153 if ("".equals(name)) {
154 throw new SAXException("feature " + e.getNodeName() + " has feature without name!");
156 if (!name.equals(name.toLowerCase())) {
157 throw new SAXException("feature name '" + name + "' must be lower case");
159 if (!devType.addFeature(name, e.getTextContent())) {
160 throw new SAXException("duplicate feature: " + name);
165 private String processFeatureGroup(DeviceType devType, Element e) throws SAXException {
166 String name = e.getAttribute("name");
167 if ("".equals(name)) {
168 throw new SAXException("feature group " + e.getNodeName() + " has no name attr!");
170 String type = e.getAttribute("type");
171 if ("".equals(type)) {
172 throw new SAXException("feature group " + e.getNodeName() + " has no type attr!");
174 FeatureGroup fg = new FeatureGroup(name, type);
175 NodeList nodes = e.getChildNodes();
176 for (int i = 0; i < nodes.getLength(); i++) {
177 Node node = nodes.item(i);
178 if (node.getNodeType() != Node.ELEMENT_NODE) {
181 Element subElement = (Element) node;
182 String nodeName = subElement.getNodeName();
183 if ("feature".equals(nodeName)) {
184 fg.addFeature(processFeature(devType, subElement));
185 } else if ("feature_group".equals(nodeName)) {
186 fg.addFeature(processFeatureGroup(devType, subElement));
189 if (!devType.addFeatureGroup(name, fg)) {
190 throw new SAXException("duplicate feature group " + name);
196 * Singleton instance function, creates DeviceTypeLoader
198 * @return DeviceTypeLoader singleton reference
201 public static synchronized DeviceTypeLoader instance() {
202 if (deviceTypeLoader.getDeviceTypes().isEmpty()) {
203 InputStream input = DeviceTypeLoader.class.getResourceAsStream("/device_types.xml");
206 deviceTypeLoader.loadDeviceTypesXML(input);
208 logger.warn("Resource stream is null, cannot read xml file.");
210 } catch (ParserConfigurationException e) {
211 logger.warn("parser config error when reading device types xml file: ", e);
212 } catch (SAXException e) {
213 logger.warn("SAX exception when reading device types xml file: ", e);
214 } catch (IOException e) {
215 logger.warn("I/O exception when reading device types xml file: ", e);
218 return deviceTypeLoader;