]> git.basschouten.com Git - openhab-addons.git/blob
bb9b5630a3bd68e0d5cc64cc4889b8e7548cd1a0
[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.deutschebahn.internal.filter;
14
15 import java.util.ArrayList;
16 import java.util.Arrays;
17 import java.util.HashSet;
18 import java.util.List;
19 import java.util.Set;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 import java.util.regex.PatternSyntaxException;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25
26 /**
27  * Scanner for filter expression.
28  * 
29  * @author Sönke Küper - Initial contribution.
30  */
31 @NonNullByDefault
32 public final class FilterScanner {
33
34     private static final Set<Character> OP_CHARS = new HashSet<>(Arrays.asList('&', '|', '!', '(', ')'));
35     private static final Pattern CHANNEL_NAME = Pattern.compile("(trip|arrival|departure)#(\\S+)");
36
37     /**
38      * State of the scanner.
39      */
40     private interface State {
41
42         /**
43          * Handles the next read character.
44          * 
45          * @return Returns the next scanner state.
46          */
47         public abstract State handle(int position, char currentChar) throws FilterScannerException;
48
49         /**
50          * Called when no more input is available.
51          */
52         public abstract void finish(int position) throws FilterScannerException;
53     }
54
55     /**
56      * Initial state of the scanner.
57      */
58     private final class InitialState implements State {
59
60         @Override
61         public State handle(int position, char currentChar) throws FilterScannerException {
62             // Skip white spaces
63             if (Character.isWhitespace(currentChar)) {
64                 return this;
65             }
66
67             switch (currentChar) {
68                 // Handle all operator tokens
69                 case '&':
70                     result.add(new AndOperator(position));
71                     return this;
72                 case '|':
73                     result.add(new OrOperator(position));
74                     return this;
75                 case '(':
76                     result.add(new BracketOpenToken(position));
77                     return this;
78                 case ')':
79                     result.add(new BracketCloseToken(position));
80                     return this;
81                 default:
82                     final ChannelNameState channelNameState = new ChannelNameState();
83                     return channelNameState.handle(position, currentChar);
84             }
85         }
86
87         @Override
88         public void finish(int position) {
89         }
90     }
91
92     /**
93      * State scanning a channel name until the equals-sign.
94      */
95     private final class ChannelNameState implements State {
96
97         private final StringBuilder channelName = new StringBuilder();
98         private int startPosition = -1;
99
100         @Override
101         public State handle(int position, final char currentChar) throws FilterScannerException {
102             // Skip white spaces at front
103             if (Character.isWhitespace(currentChar) && channelName.toString().isEmpty()) {
104                 return this;
105             }
106
107             if (Character.isWhitespace(currentChar)) {
108                 throw new FilterScannerException(position, "Channel name must not contain whitespace.");
109             }
110
111             if (currentChar == '=') {
112                 final String channelNameValue = this.channelName.toString();
113                 if (channelNameValue.isEmpty()) {
114                     throw new FilterScannerException(position, "Channel name must not be empty.");
115                 }
116
117                 final Matcher matcher = CHANNEL_NAME.matcher(channelNameValue);
118                 if (!matcher.matches()) {
119                     throw new FilterScannerException(position, "Invalid channel name: " + channelNameValue);
120                 }
121
122                 return new ExpectQuotesState(startPosition, matcher.group(1), matcher.group(2));
123             }
124
125             if (OP_CHARS.contains(currentChar)) {
126                 throw new FilterScannerException(position, "Channel name must not contain operation char.");
127             }
128
129             this.channelName.append(currentChar);
130             if (startPosition == -1) {
131                 startPosition = position;
132             }
133             return this;
134         }
135
136         @Override
137         public void finish(int position) throws FilterScannerException {
138             throw new FilterScannerException(position, "Filter value is missing.");
139         }
140     }
141
142     /**
143      * State after channel name, wiating for quotes.
144      */
145     private final class ExpectQuotesState implements State {
146
147         private final int startPosition;
148         private final String channelName;
149         private final String channelGroup;
150
151         /**
152          * Creates a new {@link ExpectQuotesState}.
153          */
154         public ExpectQuotesState(int startPosition, final String channelGroup, String channelName) {
155             this.startPosition = startPosition;
156             this.channelGroup = channelGroup;
157             this.channelName = channelName;
158         }
159
160         @Override
161         public State handle(int position, char currentChar) throws FilterScannerException {
162             if (currentChar != '"') {
163                 throw new FilterScannerException(position, "Filter value must start with quotes");
164             }
165             return new FilterValueState(startPosition, channelGroup, channelName);
166         }
167
168         @Override
169         public void finish(int position) throws FilterScannerException {
170             throw new FilterScannerException(position, "Filter value is missing.");
171         }
172     }
173
174     /**
175      * State scanning the filter value until next quotes.
176      */
177     private final class FilterValueState implements State {
178
179         private final int startPosition;
180         private final String channelGroup;
181         private final String channelName;
182         private final StringBuilder filterValue;
183
184         /**
185          * Creates a new {@link FilterValueState}.
186          */
187         public FilterValueState(int startPosition, String channelGroup, String channelName) {
188             this.startPosition = startPosition;
189             this.channelGroup = channelGroup;
190             this.channelName = channelName;
191             this.filterValue = new StringBuilder();
192         }
193
194         @Override
195         public State handle(int position, char currentChar) throws FilterScannerException {
196             if (currentChar == '"') {
197                 finish(position);
198                 return new InitialState();
199             }
200             filterValue.append(currentChar);
201             return this;
202         }
203
204         @Override
205         public void finish(int position) throws FilterScannerException {
206             String filterPattern = this.filterValue.toString();
207             try {
208                 result.add(new ChannelNameEquals(startPosition, this.channelGroup, this.channelName,
209                         Pattern.compile(filterPattern)));
210             } catch (PatternSyntaxException e) {
211                 throw new FilterScannerException(position, "Filter pattern is invalid: " + filterPattern, e);
212             }
213         }
214     }
215
216     private List<FilterToken> result;
217
218     /**
219      * Creates a new {@link FilterScanner}.
220      */
221     public FilterScanner() {
222         this.result = new ArrayList<>();
223     }
224
225     /**
226      * Scans the given filter expression and returns the result sequence of {@link FilterToken}.
227      */
228     public List<FilterToken> processInput(String value) throws FilterScannerException {
229         State state = new InitialState();
230         for (int pos = 0; pos < value.length(); pos++) {
231             char currentChar = value.charAt(pos);
232             state = state.handle(pos + 1, currentChar);
233         }
234
235         state.finish(value.length());
236
237         return this.result;
238     }
239 }