2 * Copyright (c) 2010-2023 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.deutschebahn.internal.filter;
15 import java.util.ArrayList;
16 import java.util.Arrays;
17 import java.util.HashSet;
18 import java.util.List;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 import java.util.regex.PatternSyntaxException;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
27 * Scanner for filter expression.
29 * @author Sönke Küper - Initial contribution.
32 public final class FilterScanner {
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+)");
38 * State of the scanner.
40 private interface State {
43 * Handles the next read character.
45 * @return Returns the next scanner state.
47 public abstract State handle(int position, char currentChar) throws FilterScannerException;
50 * Called when no more input is available.
52 public abstract void finish(int position) throws FilterScannerException;
56 * Initial state of the scanner.
58 private final class InitialState implements State {
61 public State handle(int position, char currentChar) throws FilterScannerException {
63 if (Character.isWhitespace(currentChar)) {
67 switch (currentChar) {
68 // Handle all operator tokens
70 result.add(new AndOperator(position));
73 result.add(new OrOperator(position));
76 result.add(new BracketOpenToken(position));
79 result.add(new BracketCloseToken(position));
82 final ChannelNameState channelNameState = new ChannelNameState();
83 return channelNameState.handle(position, currentChar);
88 public void finish(int position) {
93 * State scanning a channel name until the equals-sign.
95 private final class ChannelNameState implements State {
97 private final StringBuilder channelName = new StringBuilder();
98 private int startPosition = -1;
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()) {
107 if (Character.isWhitespace(currentChar)) {
108 throw new FilterScannerException(position, "Channel name must not contain whitespace.");
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.");
117 final Matcher matcher = CHANNEL_NAME.matcher(channelNameValue);
118 if (!matcher.matches()) {
119 throw new FilterScannerException(position, "Invalid channel name: " + channelNameValue);
122 return new ExpectQuotesState(startPosition, matcher.group(1), matcher.group(2));
125 if (OP_CHARS.contains(currentChar)) {
126 throw new FilterScannerException(position, "Channel name must not contain operation char.");
129 this.channelName.append(currentChar);
130 if (startPosition == -1) {
131 startPosition = position;
137 public void finish(int position) throws FilterScannerException {
138 throw new FilterScannerException(position, "Filter value is missing.");
143 * State after channel name, wiating for quotes.
145 private final class ExpectQuotesState implements State {
147 private final int startPosition;
148 private final String channelName;
149 private final String channelGroup;
152 * Creates a new {@link ExpectQuotesState}.
154 public ExpectQuotesState(int startPosition, final String channelGroup, String channelName) {
155 this.startPosition = startPosition;
156 this.channelGroup = channelGroup;
157 this.channelName = channelName;
161 public State handle(int position, char currentChar) throws FilterScannerException {
162 if (currentChar != '"') {
163 throw new FilterScannerException(position, "Filter value must start with quotes");
165 return new FilterValueState(startPosition, channelGroup, channelName);
169 public void finish(int position) throws FilterScannerException {
170 throw new FilterScannerException(position, "Filter value is missing.");
175 * State scanning the filter value until next quotes.
177 private final class FilterValueState implements State {
179 private final int startPosition;
180 private final String channelGroup;
181 private final String channelName;
182 private final StringBuilder filterValue;
185 * Creates a new {@link FilterValueState}.
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();
195 public State handle(int position, char currentChar) throws FilterScannerException {
196 if (currentChar == '"') {
198 return new InitialState();
200 filterValue.append(currentChar);
205 public void finish(int position) throws FilterScannerException {
206 String filterPattern = this.filterValue.toString();
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);
216 private List<FilterToken> result;
219 * Creates a new {@link FilterScanner}.
221 public FilterScanner() {
222 this.result = new ArrayList<>();
226 * Scans the given filter expression and returns the result sequence of {@link FilterToken}.
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);
235 state.finish(value.length());