]> git.basschouten.com Git - openhab-addons.git/blob
a084ed3d9909889fe21990c9c4f251f040267352
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.tacmi.internal.schema;
14
15 import java.util.LinkedHashMap;
16 import java.util.Map;
17
18 import org.attoparser.ParseException;
19 import org.attoparser.simple.AbstractSimpleMarkupHandler;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.tacmi.internal.schema.ChangerX2Entry.OptionType;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 /**
27  * The {@link ApiPageParser} class parses the 'changerx2' page from the CMI and
28  * maps it to the results
29  *
30  * @author Christian Niessner - Initial contribution
31  */
32 @NonNullByDefault
33 public class ChangerX2Parser extends AbstractSimpleMarkupHandler {
34
35     private final Logger logger = LoggerFactory.getLogger(ChangerX2Parser.class);
36
37     static enum ParserState {
38         INIT,
39         INPUT,
40         INPUT_DATA,
41         SELECT,
42         SELECT_OPTION,
43         UNKNOWN
44     }
45
46     private final String channelName;
47     private @Nullable String curOptionId;
48     private ParserState parserState = ParserState.INIT;
49     private @Nullable String address;
50     private @Nullable String addressFieldName;
51     private @Nullable String optionFieldName;
52     private @Nullable OptionType optionType;
53     private @Nullable StringBuilder curOptionValue;
54     private Map<String, @Nullable String> options;
55
56     public ChangerX2Parser(String channelName) {
57         super();
58         this.options = new LinkedHashMap<>();
59         this.channelName = channelName;
60     }
61
62     @Override
63     public void handleDocumentStart(final long startTimeNanos, final int line, final int col) throws ParseException {
64         this.parserState = ParserState.INIT;
65         this.options.clear();
66     }
67
68     @Override
69     public void handleDocumentEnd(final long endTimeNanos, final long totalTimeNanos, final int line, final int col)
70             throws ParseException {
71         if (this.parserState != ParserState.INIT) {
72             logger.debug("Parserstate == Init expected, but is {}", this.parserState);
73         }
74     }
75
76     @Override
77     @NonNullByDefault({})
78     public void handleStandaloneElement(final String elementName, final Map<String, String> attributes,
79             final boolean minimized, final int line, final int col) throws ParseException {
80         logger.debug("Error parsing options for {}: Unexpected StandaloneElement in {}{}: {} [{}]", channelName, line,
81                 col, elementName, attributes);
82     }
83
84     @Override
85     @NonNullByDefault({})
86     public void handleOpenElement(final String elementName, final Map<String, String> attributes, final int line,
87             final int col) throws ParseException {
88         String id = attributes == null ? null : attributes.get("id");
89
90         if (this.parserState == ParserState.INIT && "input".equals(elementName) && "changeadr".equals(id)) {
91             this.parserState = ParserState.INPUT;
92             if (attributes == null) {
93                 this.address = null;
94                 this.addressFieldName = null;
95             } else {
96                 this.addressFieldName = attributes.get("name");
97                 this.address = attributes.get("value");
98             }
99         } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
100                 && "select".equals(elementName)) {
101             this.parserState = ParserState.SELECT;
102             this.optionFieldName = attributes == null ? null : attributes.get("name");
103         } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
104                 && "br".equals(elementName)) {
105             // ignored
106         } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
107                 && "input".equals(elementName) && "changeto".equals(id)) {
108             this.parserState = ParserState.INPUT_DATA;
109             if (attributes != null) {
110                 this.optionFieldName = attributes.get("name");
111                 String type = attributes.get("type");
112                 if ("number".equals(type)) {
113                     this.optionType = OptionType.NUMBER;
114                     // we transfer the limits from the input element...
115                     this.options.put(ChangerX2Entry.NUMBER_MIN, attributes.get(ChangerX2Entry.NUMBER_MIN));
116                     this.options.put(ChangerX2Entry.NUMBER_MAX, attributes.get(ChangerX2Entry.NUMBER_MAX));
117                     this.options.put(ChangerX2Entry.NUMBER_STEP, attributes.get(ChangerX2Entry.NUMBER_STEP));
118                 } else {
119                     logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line,
120                             col, attributes);
121                 }
122             }
123         } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
124                 && "input".equals(elementName) && "changetotimeh".equals(id)) {
125             this.parserState = ParserState.INPUT_DATA;
126             if (attributes != null) {
127                 this.optionFieldName = attributes.get("name");
128                 String type = attributes.get("type");
129                 if ("number".equals(attributes.get("type"))) {
130                     this.optionType = OptionType.TIME;
131                     // validate hour limits
132                     if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN))
133                             || !"24".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) {
134                         logger.warn(
135                                 "Error parsing options for {}: Unexpected MIN/MAX values for hour input field in {}:{}: {}",
136                                 channelName, line, col, attributes);
137                     }
138                     ;
139                 } else {
140                     logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line,
141                             col, attributes);
142                 }
143             }
144         } else if ((this.parserState == ParserState.INPUT_DATA || this.parserState == ParserState.INPUT)
145                 && "input".equals(elementName) && "changetotimem".equals(id)) {
146             this.parserState = ParserState.INPUT_DATA;
147             if (attributes != null) {
148                 if ("number".equals(attributes.get("type"))) {
149                     this.optionType = OptionType.TIME;
150                     if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN))
151                             || !"59".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) {
152                         logger.warn(
153                                 "Error parsing options for {}: Unexpected MIN/MAX values for minute input field in {}:{}: {}",
154                                 channelName, line, col, attributes);
155                     }
156                     ;
157                 } else {
158                     logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line,
159                             col, attributes);
160                 }
161             }
162         } else if (this.parserState == ParserState.SELECT && "option".equals(elementName)) {
163             this.parserState = ParserState.SELECT_OPTION;
164             this.optionType = OptionType.SELECT;
165             this.curOptionValue = new StringBuilder();
166             this.curOptionId = attributes == null ? null : attributes.get("value");
167         } else {
168             logger.debug("Error parsing options for {}: Unexpected OpenElement in {}:{}: {} [{}]", channelName, line,
169                     col, elementName, attributes);
170         }
171     }
172
173     @Override
174     public void handleCloseElement(final @Nullable String elementName, final int line, final int col)
175             throws ParseException {
176         if (this.parserState == ParserState.INPUT && "input".equals(elementName)) {
177             this.parserState = ParserState.INIT;
178         } else if (this.parserState == ParserState.INPUT_DATA && "input".equals(elementName)) {
179             this.parserState = ParserState.INPUT;
180         } else if (this.parserState == ParserState.SELECT && "select".equals(elementName)) {
181             this.parserState = ParserState.INIT;
182         } else if (this.parserState == ParserState.SELECT_OPTION && "option".equals(elementName)) {
183             this.parserState = ParserState.SELECT;
184             StringBuilder sb = this.curOptionValue;
185             String value = sb != null && sb.length() > 0 ? sb.toString().trim() : null;
186             this.curOptionValue = null;
187             String id = this.curOptionId;
188             this.curOptionId = null;
189             if (value != null) {
190                 if (id == null || id.trim().isEmpty()) {
191                     logger.debug("Error parsing options for {}: Got option with empty 'value' in {}:{}: [{}]",
192                             channelName, line, col, value);
193                     return;
194                 }
195                 // we use the value as key and the id as value, as we have to map from the value to the id...
196                 @Nullable
197                 String prev = this.options.put(value, id);
198                 if (prev != null && !prev.equals(value)) {
199                     logger.debug("Error parsing options for {}: Got duplicate options in {}:{} for {}: {} and {}",
200                             channelName, line, col, value, prev, id);
201                 }
202             }
203         } else if (this.parserState == ParserState.INPUT && "span".equals(elementName)) {
204             // span's are ignored...
205         } else {
206             logger.debug("Error parsing options for {}: Unexpected CloseElement in {}:{}: {}", channelName, line, col,
207                     elementName);
208         }
209     }
210
211     @Override
212     public void handleAutoCloseElement(final @Nullable String elementName, final int line, final int col)
213             throws ParseException {
214         logger.debug("Unexpected AutoCloseElement in {}:{}: {}", line, col,
215                 elementName == null ? "<null>" : elementName);
216     }
217
218     @Override
219     public void handleUnmatchedCloseElement(final @Nullable String elementName, final int line, final int col)
220             throws ParseException {
221         logger.debug("Unexpected UnmatchedCloseElement in {}:{}: {}", line, col,
222                 elementName == null ? "<null>" : elementName);
223     }
224
225     @Override
226     public void handleDocType(final @Nullable String elementName, final @Nullable String publicId,
227             final @Nullable String systemId, final @Nullable String internalSubset, final int line, final int col)
228             throws ParseException {
229         logger.debug("Unexpected DocType in {}:{}: {}/{}/{}/{}", line, col, elementName, publicId, systemId,
230                 internalSubset);
231     }
232
233     @Override
234     public void handleComment(final char @Nullable [] buffer, final int offset, final int len, final int line,
235             final int col) throws ParseException {
236         logger.debug("Unexpected comment in {}:{}: {}", line, col,
237                 buffer == null ? "<null>" : new String(buffer, offset, len));
238     }
239
240     @Override
241     public void handleCDATASection(final char @Nullable [] buffer, final int offset, final int len, final int line,
242             final int col) throws ParseException {
243         logger.debug("Unexpected CDATA in {}:{}: {}", line, col,
244                 buffer == null ? "<null>" : new String(buffer, offset, len));
245     }
246
247     @Override
248     public void handleText(final char @Nullable [] buffer, final int offset, final int len, final int line,
249             final int col) throws ParseException {
250         if (buffer == null) {
251             return;
252         }
253
254         if (this.parserState == ParserState.SELECT_OPTION) {
255             // logger.debug("Text {}:{}: {}", line, col, new String(buffer, offset, len));
256             StringBuilder sb = this.curOptionValue;
257             if (sb != null) {
258                 sb.append(buffer, offset, len);
259             }
260         } else if (this.parserState == ParserState.INIT && len == 1 && buffer[offset] == '\n') {
261             // single newline - ignore/drop it...
262         } else if (this.parserState == ParserState.INPUT) {
263             // this is a label next to the value input field - we currently have no use for it so
264             // it's dropped...
265         } else {
266             logger.debug("Error parsing options for {}: Unexpected Text {}:{}: (ctx: {} len: {}) '{}' ",
267                     this.channelName, line, col, this.parserState, len, new String(buffer, offset, len));
268         }
269     }
270
271     @Override
272     public void handleXmlDeclaration(final @Nullable String version, final @Nullable String encoding,
273             final @Nullable String standalone, final int line, final int col) throws ParseException {
274         logger.debug("Unexpected XML Declaration {}:{}: {} {} {}", line, col, version, encoding, standalone);
275     }
276
277     @Override
278     public void handleProcessingInstruction(final @Nullable String target, final @Nullable String content,
279             final int line, final int col) throws ParseException {
280         logger.debug("Unexpected ProcessingInstruction {}:{}: {} {}", line, col, target, content);
281     }
282
283     @Nullable
284     protected ChangerX2Entry getParsedEntry() {
285         String addressFieldName = this.addressFieldName;
286         String address = this.address;
287         String optionFieldName = this.optionFieldName;
288         OptionType optionType = this.optionType;
289         if (address == null || addressFieldName == null || optionType == null || optionFieldName == null) {
290             return null;
291         }
292         return new ChangerX2Entry(addressFieldName, address, optionFieldName, optionType, this.options);
293     }
294 }