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.transport.message;
15 import java.util.LinkedHashMap;
17 import java.util.Map.Entry;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.insteon.internal.InsteonResourceLoader;
22 import org.openhab.binding.insteon.internal.device.InsteonAddress;
23 import org.openhab.binding.insteon.internal.transport.message.Msg.Direction;
24 import org.openhab.binding.insteon.internal.utils.HexUtils;
25 import org.w3c.dom.Element;
26 import org.w3c.dom.Node;
27 import org.w3c.dom.NodeList;
28 import org.xml.sax.SAXException;
31 * The {@link MsgDefinitionRegistry} represents the message definition registry
33 * @author Bernd Pfrommer - Initial contribution
34 * @author Rob Nielsen - Port to openHAB 2 insteon binding
35 * @author Jeremy Setton - Rewrite insteon binding
38 public class MsgDefinitionRegistry extends InsteonResourceLoader {
39 private static final MsgDefinitionRegistry MSG_DEFINITION_REGISTRY = new MsgDefinitionRegistry();
40 private static final String RESOURCE_NAME = "/msg-definitions.xml";
42 private Map<String, Msg> definitions = new LinkedHashMap<>();
44 private MsgDefinitionRegistry() {
49 * Returns message template for a given type
51 * @param type message type to match
52 * @return message template if found, otherwise null
54 public @Nullable Msg getTemplate(String type) {
55 return definitions.get(type);
59 * Returns message template for a given command and direction
61 * @param cmd message command to match
62 * @param direction message direction to match
63 * @return message template if found, otherwise null
65 public @Nullable Msg getTemplate(byte cmd, Direction direction) {
66 return getTemplate(cmd, null, direction);
70 * Returns message template for a given command, extended flag and direction
72 * @param cmd message command to match
73 * @param isExtended if message is extended
74 * @param direction message direction to match
75 * @return message template if found, otherwise null
77 public @Nullable Msg getTemplate(byte cmd, @Nullable Boolean isExtended, Direction direction) {
78 return definitions.values().stream().filter(msg -> msg.getCommand() == cmd && msg.getDirection() == direction
79 && (isExtended == null || msg.isExtended() == isExtended)).findFirst().orElse(null);
83 * Returns known message definitions
85 * @return currently known message definitions
87 public Map<String, Msg> getDefinitions() {
92 * Initializes message definition registry
95 protected void initialize() {
98 logger.debug("loaded {} message definitions", definitions.size());
99 if (logger.isTraceEnabled()) {
100 definitions.entrySet().stream()
101 .map(definition -> String.format("%s->%s", definition.getKey(), definition.getValue()))
102 .forEach(logger::trace);
107 * Parses message definition document
109 * @param element element to parse
110 * @throws SAXException
113 protected void parseDocument(Element element) throws SAXException {
114 NodeList nodes = element.getChildNodes();
115 for (int i = 0; i < nodes.getLength(); i++) {
116 Node node = nodes.item(i);
117 if (node.getNodeType() == Node.ELEMENT_NODE) {
118 Element child = (Element) node;
119 String nodeName = child.getNodeName();
120 if ("msg".equals(nodeName)) {
121 parseMsgDefinition(child);
128 * Parses message definition node
130 * @param element element to parse
131 * @throws SAXException
133 private void parseMsgDefinition(Element element) throws SAXException {
134 LinkedHashMap<Field, Object> fields = new LinkedHashMap<>();
135 String name = element.getAttribute("name");
136 Direction direction = Direction.valueOf(element.getAttribute("direction"));
137 int length = element.hasAttribute("length") ? Integer.parseInt(element.getAttribute("length")) : 0;
138 int headerLength = 0;
141 NodeList nodes = element.getChildNodes();
142 for (int i = 0; i < nodes.getLength(); i++) {
143 Node node = nodes.item(i);
144 if (node.getNodeType() == Node.ELEMENT_NODE) {
145 Element child = (Element) node;
146 String nodeName = child.getNodeName();
147 if ("header".equals(nodeName)) {
148 headerLength = parseHeader(child, fields);
149 // Increment the offset by the header length
150 offset += headerLength;
152 // Increment the offset by the field data type length
153 offset += parseField(child, offset, fields);
159 } else if (offset != length) {
160 throw new SAXException("actual msg length " + offset + " differs from given msg length " + length);
164 Msg msg = makeMsgTemplate(fields, headerLength, length, direction);
165 definitions.put(name, msg);
166 } catch (FieldException e) {
167 throw new SAXException("failed to create message definition " + name + ":", e);
174 * @param element element to parse
175 * @param fields fields map to update
176 * @return header length
177 * @throws SAXException
179 private int parseHeader(Element element, LinkedHashMap<Field, Object> fields) throws SAXException {
180 int length = Integer.parseInt(element.getAttribute("length"));
183 NodeList nodes = element.getChildNodes();
184 for (int i = 0; i < nodes.getLength(); i++) {
185 Node node = nodes.item(i);
186 if (node.getNodeType() == Node.ELEMENT_NODE) {
187 Element child = (Element) node;
188 // Increment the offset by the field data type length
189 offset += parseField(child, offset, fields);
192 if (length != offset) {
193 throw new SAXException("actual header length " + offset + " differs from given length " + length);
201 * @param element element to parse
202 * @param offset msg offset
203 * @param fields fields map to update
204 * @return field data type length
205 * @throws SAXException
207 private int parseField(Element element, int offset, LinkedHashMap<Field, Object> fields) throws SAXException {
208 String name = element.getAttribute("name");
210 throw new SAXException("undefined field name");
212 DataType dataType = DataType.get(element.getNodeName());
213 Field field = new Field(name, dataType, offset);
214 Object value = getFieldValue(dataType, element.getTextContent().trim());
215 fields.put(field, value);
216 return dataType.getSize();
220 * Returns field value
222 * @param dataType field data type
223 * @param value value to convert
224 * @return field value
225 * @throws SAXException
227 private Object getFieldValue(DataType dataType, String value) throws SAXException {
230 return getByteValue(value);
232 return getAddressValue(value);
234 throw new SAXException("invalid field data type");
239 * Returns field value as a byte
241 * @param value value to convert
243 * @throws SAXException
245 private byte getByteValue(String value) throws SAXException {
247 return value.isEmpty() ? 0x00 : (byte) HexUtils.toInteger(value);
248 } catch (NumberFormatException e) {
249 throw new SAXException("invalid field byte value: " + value);
254 * Returns field value as an insteon address
256 * @param value value to convert
257 * @return insteon address
258 * @throws SAXException
260 private InsteonAddress getAddressValue(String value) throws SAXException {
262 return value.isEmpty() ? InsteonAddress.UNKNOWN : new InsteonAddress(value);
263 } catch (IllegalArgumentException e) {
264 throw new SAXException("invalid field address value: " + value);
269 * Returns new message template
271 * @param fields msg fields
272 * @param length msg length
273 * @param headerLength header length
274 * @param direction msg direction
275 * @return new msg template
276 * @throws FieldException
278 private Msg makeMsgTemplate(Map<Field, Object> fields, int headerLength, int length, Direction direction)
279 throws FieldException {
280 Msg msg = new Msg(headerLength, length, direction);
281 for (Entry<Field, Object> entry : fields.entrySet()) {
282 Field field = entry.getKey();
283 byte[] data = msg.getData();
284 field.set(data, entry.getValue());
285 if (!field.getName().isEmpty()) {
293 * Singleton instance function
295 * @return MsgDefinitionRegistry singleton reference
297 public static synchronized MsgDefinitionRegistry getInstance() {
298 if (MSG_DEFINITION_REGISTRY.getDefinitions().isEmpty()) {
299 MSG_DEFINITION_REGISTRY.initialize();
301 return MSG_DEFINITION_REGISTRY;