* Implemented filters within timetable.
Signed-off-by: Sönke Küper <soenkekueper@gmx.de>
* Added position information for filtertokens, to allow detailled failure information
Signed-off-by: Sönke Küper <soenkekueper@gmx.de>
* Added documentation for non matching values.
Signed-off-by: Sönke Küper <soenkekueper@gmx.de>
* Applied review remarks.
Signed-off-by: Sönke Küper <soenkekueper@gmx.de>
Co-authored-by: Sönke Küper <soenkekueper@gmx.de>
| `accessToken` | | Yes | The access token for the timetable api within the developer portal of Deutsche Bahn. |
| `evaNo` | | Yes | The eva nr. of the train station for which the timetable will be requested.|
| `trainFilter` | | Yes | Selects the trains that will be displayed in the timetable. Either only arrivals, only departures or all trains can be displayed. |
+| `additionalFilter` | | No | Specifies additional filters for trains, that should be displayed within the timetable. |
+** Additional filter **
+If you only want to display certain trains within your timetable, you can specify an additional filter. This will be evaluated when loading trains,
+and only trains that matches the given filter will be contained within the timetable.
+
+To specify an advanced filter you can
+
+- specify a filter for the value of a given channel. Therefore you must specify the channel name (with channel group) and specify a compare value like this:
+`departure#line="RE60"` this will select all trains with line RE60
+- use regular expressions for expected channel values, for example: `departure#line="RE.*"`, this will match all lines starting with "RE".
+- combine multiple statements as "and" conjunction by using `&`. If used, both parts must match, for example: `departure#line="RE60" & trip#category="WFB"`
+- combine multiple statements as "or" disjunction by using `|`. If used, one of the parts must match, for example: `departure#line="RE60" | departure#line="RE60"`
+- use brackets to build more complex queries like `trip#category="RE" AND (departure#line="17" OR departure#line="57")`
+
+If a channel has multiple values, like the channels `arrival#planned-path` and `departure#planned-path` have a list of stations,
+only one of the values must match the given expression. So you can specify a filter like `departure#planned-path="Hannover Hbf"`
+to easily display only trains that will go to Hannover Hbf.
+
+If the filtered value is not present for a train, for example if you filter a departure attribute but train ends at the selected station,
+the filter will not match.
### Configuring the trains
*/
package org.openhab.binding.deutschebahn.internal;
+import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
private final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState;
private final String channelTypeName;
private final Class<STATE_TYPE> stateType;
+ private final Function<VALUE_TYPE, List<String>> valueToList;
/**
* Creates an new {@link EventAttribute}.
final Function<DTO_TYPE, @Nullable VALUE_TYPE> getter, //
final BiConsumer<DTO_TYPE, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
+ final Function<VALUE_TYPE, List<String>> valueToList, //
final Class<STATE_TYPE> stateType) {
this.channelTypeName = channelTypeName;
this.getter = getter;
this.setter = setter;
this.getState = getState;
+ this.valueToList = valueToList;
this.stateType = stateType;
}
return this.getter.apply(object);
}
+ /**
+ * Returns a list of values as string list.
+ * Returns empty list if value is not present, singleton list if attribute is not single-valued.
+ */
+ public final List<String> getStringValues(DTO_TYPE object) {
+ return this.valueToList.apply(getValue(object));
+ }
+
/**
* Sets the value for the selected attribute in the given DTO object
*/
*/
package org.openhab.binding.deutschebahn.internal;
+import java.util.List;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
@NonNullByDefault
public interface AttributeSelection {
+ /**
+ * Returns the value for this attribute.
+ */
+ @Nullable
+ public abstract Object getValue(TimetableStop stop);
+
/**
* Returns the {@link State} that should be set for the channels'value for this attribute.
*/
@Nullable
public abstract State getState(TimetableStop stop);
+
+ /**
+ * Returns a list of values as string list.
+ * Returns empty list if value is not present, singleton list if attribute is not single-valued.
+ */
+ public abstract List<String> getStringValues(TimetableStop t);
}
*/
package org.openhab.binding.deutschebahn.internal;
+import java.util.List;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.deutschebahn.internal.filter.FilterParser;
+import org.openhab.binding.deutschebahn.internal.filter.FilterParserException;
+import org.openhab.binding.deutschebahn.internal.filter.FilterScanner;
+import org.openhab.binding.deutschebahn.internal.filter.FilterScannerException;
+import org.openhab.binding.deutschebahn.internal.filter.FilterToken;
+import org.openhab.binding.deutschebahn.internal.filter.TimetableStopPredicate;
/**
* The {@link DeutscheBahnTimetableConfiguration} for the Timetable bridge-type.
*/
public String trainFilter = "";
+ /**
+ * Specifies additional filters for trains to be displayed within the timetable.
+ */
+ public String additionalFilter = "";
+
/**
* Returns the {@link TimetableStopFilter}.
*/
- public TimetableStopFilter getTimetableStopFilter() {
+ public TimetableStopFilter getTrainFilterFilter() {
return TimetableStopFilter.valueOf(this.trainFilter.toUpperCase());
}
+
+ /**
+ * Returns the additional configured {@link TimetableStopPredicate} or <code>null</code> if not specified.
+ */
+ public @Nullable TimetableStopPredicate getAdditionalFilter() throws FilterScannerException, FilterParserException {
+ if (additionalFilter.isBlank()) {
+ return null;
+ } else {
+ final FilterScanner scanner = new FilterScanner();
+ final List<FilterToken> filterTokens = scanner.processInput(additionalFilter);
+ return FilterParser.parse(filterTokens);
+ }
+ }
}
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.deutschebahn.internal.filter.AndPredicate;
+import org.openhab.binding.deutschebahn.internal.filter.FilterParserException;
+import org.openhab.binding.deutschebahn.internal.filter.FilterScannerException;
+import org.openhab.binding.deutschebahn.internal.filter.TimetableStopPredicate;
import org.openhab.binding.deutschebahn.internal.timetable.TimetableLoader;
import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Api;
import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1ApiFactory;
try {
final TimetablesV1Api api = this.timetablesV1ApiFactory.create(config.accessToken, HttpUtil::executeUrl);
- final TimetableStopFilter stopFilter = config.getTimetableStopFilter();
+ final TimetableStopFilter stopFilter = config.getTrainFilterFilter();
+ final TimetableStopPredicate additionalFilter = config.getAdditionalFilter();
+
+ final TimetableStopPredicate combinedFilter;
+ if (additionalFilter == null) {
+ combinedFilter = stopFilter;
+ } else {
+ combinedFilter = new AndPredicate(stopFilter, additionalFilter);
+ }
final EventType eventSelection = stopFilter == TimetableStopFilter.ARRIVALS ? EventType.ARRIVAL
: EventType.ARRIVAL;
this.loader = new TimetableLoader( //
api, //
- stopFilter, //
+ combinedFilter, //
eventSelection, //
currentTimeProvider, //
config.evaNo, //
this.updateChannels();
this.restartJob();
});
+ } catch (FilterScannerException | FilterParserException e) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (JAXBException | SAXException | URISyntaxException e) {
this.logger.error("Error initializing api", e);
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.function.BiConsumer;
* Planned Path.
*/
public static final EventAttribute<String, StringType> PPTH = new EventAttribute<>("planned-path", Event::getPpth,
- Event::setPpth, StringType::new, StringType.class);
+ Event::setPpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
/**
* Changed Path.
*/
public static final EventAttribute<String, StringType> CPTH = new EventAttribute<>("changed-path", Event::getCpth,
- Event::setCpth, StringType::new, StringType.class);
+ Event::setCpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
/**
* Planned platform.
*/
public static final EventAttribute<String, StringType> PP = new EventAttribute<>("planned-platform", Event::getPp,
- Event::setPp, StringType::new, StringType.class);
+ Event::setPp, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed platform.
*/
public static final EventAttribute<String, StringType> CP = new EventAttribute<>("changed-platform", Event::getCp,
- Event::setCp, StringType::new, StringType.class);
+ Event::setCp, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned time.
*/
public static final EventAttribute<Date, DateTimeType> PT = new EventAttribute<>("planned-time",
- getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType, DateTimeType.class);
+ getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType,
+ EventAttribute::mapDateToStringList, DateTimeType.class);
/**
* Changed time.
*/
public static final EventAttribute<Date, DateTimeType> CT = new EventAttribute<>("changed-time",
- getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType, DateTimeType.class);
+ getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType,
+ EventAttribute::mapDateToStringList, DateTimeType.class);
/**
* Planned status.
*/
public static final EventAttribute<EventStatus, StringType> PS = new EventAttribute<>("planned-status",
- Event::getPs, Event::setPs, EventAttribute::fromEventStatus, StringType.class);
+ Event::getPs, Event::setPs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
+ StringType.class);
/**
* Changed status.
*/
public static final EventAttribute<EventStatus, StringType> CS = new EventAttribute<>("changed-status",
- Event::getCs, Event::setCs, EventAttribute::fromEventStatus, StringType.class);
+ Event::getCs, Event::setCs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
+ StringType.class);
/**
* Hidden.
*/
public static final EventAttribute<Integer, OnOffType> HI = new EventAttribute<>("hidden", Event::getHi,
- Event::setHi, EventAttribute::parseHidden, OnOffType.class);
+ Event::setHi, EventAttribute::parseHidden, EventAttribute::mapIntegerToStringList, OnOffType.class);
/**
* Cancellation time.
*/
public static final EventAttribute<Date, DateTimeType> CLT = new EventAttribute<>("cancellation-time",
- getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType, DateTimeType.class);
+ getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType,
+ EventAttribute::mapDateToStringList, DateTimeType.class);
/**
* Wing.
*/
public static final EventAttribute<String, StringType> WINGS = new EventAttribute<>("wings", Event::getWings,
- Event::setWings, StringType::new, StringType.class);
+ Event::setWings, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
/**
* Transition.
*/
public static final EventAttribute<String, StringType> TRA = new EventAttribute<>("transition", Event::getTra,
- Event::setTra, StringType::new, StringType.class);
+ Event::setTra, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned distant endpoint.
*/
public static final EventAttribute<String, StringType> PDE = new EventAttribute<>("planned-distant-endpoint",
- Event::getPde, Event::setPde, StringType::new, StringType.class);
+ Event::getPde, Event::setPde, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed distant endpoint.
*/
public static final EventAttribute<String, StringType> CDE = new EventAttribute<>("changed-distant-endpoint",
- Event::getCde, Event::setCde, StringType::new, StringType.class);
+ Event::getCde, Event::setCde, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Distant change.
*/
public static final EventAttribute<Integer, DecimalType> DC = new EventAttribute<>("distant-change", Event::getDc,
- Event::setDc, DecimalType::new, DecimalType.class);
+ Event::setDc, DecimalType::new, EventAttribute::mapIntegerToStringList, DecimalType.class);
/**
* Line.
*/
public static final EventAttribute<String, StringType> L = new EventAttribute<>("line", Event::getL, Event::setL,
- StringType::new, StringType.class);
+ StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Messages.
*/
public static final EventAttribute<List<Message>, StringType> MESSAGES = new EventAttribute<>("messages",
- EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages, StringType.class);
+ EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages,
+ EventAttribute::mapMessagesToList, StringType.class);
/**
* Planned Start station.
*/
public static final EventAttribute<String, StringType> PLANNED_START_STATION = new EventAttribute<>(
"planned-start-station", EventAttribute.getSingleStationFromPath(Event::getPpth, true),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned Previous stations.
*/
- public static final EventAttribute<String, StringType> PLANNED_PREVIOUS_STATIONS = new EventAttribute<>(
+ public static final EventAttribute<List<String>, StringType> PLANNED_PREVIOUS_STATIONS = new EventAttribute<>(
"planned-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, true),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
+ StringType.class);
/**
* Planned Target station.
*/
public static final EventAttribute<String, StringType> PLANNED_TARGET_STATION = new EventAttribute<>(
"planned-target-station", EventAttribute.getSingleStationFromPath(Event::getPpth, false),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned Following stations.
*/
- public static final EventAttribute<String, StringType> PLANNED_FOLLOWING_STATIONS = new EventAttribute<>(
+ public static final EventAttribute<List<String>, StringType> PLANNED_FOLLOWING_STATIONS = new EventAttribute<>(
"planned-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, false),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
+ StringType.class);
/**
* Changed Start station.
*/
public static final EventAttribute<String, StringType> CHANGED_START_STATION = new EventAttribute<>(
"changed-start-station", EventAttribute.getSingleStationFromPath(Event::getCpth, true),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed Previous stations.
*/
- public static final EventAttribute<String, StringType> CHANGED_PREVIOUS_STATIONS = new EventAttribute<>(
+ public static final EventAttribute<List<String>, StringType> CHANGED_PREVIOUS_STATIONS = new EventAttribute<>(
"changed-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, true),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
+ StringType.class);
/**
* Changed Target station.
*/
public static final EventAttribute<String, StringType> CHANGED_TARGET_STATION = new EventAttribute<>(
"changed-target-station", EventAttribute.getSingleStationFromPath(Event::getCpth, false),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed Following stations.
*/
- public static final EventAttribute<String, StringType> CHANGED_FOLLOWING_STATIONS = new EventAttribute<>(
+ public static final EventAttribute<List<String>, StringType> CHANGED_FOLLOWING_STATIONS = new EventAttribute<>(
"changed-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, false),
- EventAttribute.voidSetter(), StringType::new, StringType.class);
+ EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
+ StringType.class);
/**
* List containing all known {@link EventAttribute}.
final Function<Event, @Nullable VALUE_TYPE> getter, //
final BiConsumer<Event, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
+ final Function<VALUE_TYPE, List<String>> valueToList, //
final Class<STATE_TYPE> stateType) {
- super(channelTypeName, getter, setter, getState, stateType);
+ super(channelTypeName, getter, setter, getState, valueToList, stateType);
}
private static StringType fromEventStatus(final EventStatus value) {
return new StringType(value.value());
}
+ private static List<String> listFromEventStatus(final @Nullable EventStatus value) {
+ if (value == null) {
+ return Collections.emptyList();
+ } else {
+ return Collections.singletonList(value.value());
+ }
+ }
+
+ private static StringType fromStringList(final List<String> value) {
+ return new StringType(value.stream().collect(Collectors.joining(" - ")));
+ }
+
+ private static List<String> nullToEmptyList(@Nullable final List<String> value) {
+ return value == null ? Collections.emptyList() : value;
+ }
+
+ /**
+ * Returns a list containing only the given value or empty list if value is <code>null</code>.
+ */
+ private static List<String> singletonList(@Nullable String value) {
+ return value == null ? Collections.emptyList() : Collections.singletonList(value);
+ }
+
private static OnOffType parseHidden(@Nullable Integer value) {
return OnOffType.from(value != null && value == 1);
}
}
}
+ /**
+ * Maps the status codes from the messages into string list.
+ */
+ private static List<String> mapMessagesToList(final @Nullable List<Message> messages) {
+ if (messages == null || messages.isEmpty()) {
+ return Collections.emptyList();
+ } else {
+ return messages //
+ .stream()//
+ .filter((Message message) -> message.getC() != null) //
+ .map(Message::getC) //
+ .distinct() //
+ .map(MessageCodes::getMessage) //
+ .filter((String messageText) -> !messageText.isEmpty()) //
+ .collect(Collectors.toList());
+ }
+ }
+
private static Function<Event, @Nullable List<Message>> getMessages() {
return new Function<Event, @Nullable List<Message>>() {
};
}
+ private static List<String> mapIntegerToStringList(@Nullable Integer value) {
+ if (value == null) {
+ return Collections.emptyList();
+ } else {
+ return Collections.singletonList(String.valueOf(value));
+ }
+ }
+
+ private static List<String> mapDateToStringList(@Nullable Date value) {
+ if (value == null) {
+ return Collections.emptyList();
+ } else {
+ return Collections.singletonList(DATETIME_FORMAT.format(value));
+ }
+ }
+
/**
* Returns an single station from an path value (i.e. pipe separated value of stations).
*
* @param removeFirst if <code>true</code> the first value will be removed, <code>false</code> will remove the last
* value.
*/
- private static Function<Event, @Nullable String> getIntermediateStationsFromPath(
+ private static Function<Event, @Nullable List<String>> getIntermediateStationsFromPath(
final Function<Event, @Nullable String> getPath, boolean removeFirst) {
return (final Event event) -> {
final String path = getPath.apply(event);
} else {
stations = stations.limit(stationValues.length - 1);
}
- return stations.collect(Collectors.joining(" - "));
+ return stations.collect(Collectors.toList());
};
}
return path.split("\\|");
}
+ private static List<String> splitOnPipeToList(final String value) {
+ return Arrays.asList(value.split("\\|"));
+ }
+
/**
* Returns an {@link EventAttribute} for the given channel-type and {@link EventType}.
*/
*/
package org.openhab.binding.deutschebahn.internal;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deutschebahn.internal.timetable.dto.Event;
return this.eventAttribute.getState(event);
}
}
+
+ @Override
+ public @Nullable Object getValue(TimetableStop stop) {
+ final Event event = eventType.getEvent(stop);
+ if (event == null) {
+ return UnDefType.UNDEF;
+ } else {
+ return this.eventAttribute.getValue(event);
+ }
+ }
+
+ @Override
+ public List<String> getStringValues(TimetableStop stop) {
+ final Event event = eventType.getEvent(stop);
+ if (event == null) {
+ return Collections.emptyList();
+ } else {
+ return this.eventAttribute.getStringValues(event);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(eventAttribute, eventType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof EventAttributeSelection)) {
+ return false;
+ }
+ final EventAttributeSelection other = (EventAttributeSelection) obj;
+ return Objects.equals(eventAttribute, other.eventAttribute) && eventType == other.eventType;
+ }
}
*/
package org.openhab.binding.deutschebahn.internal;
-import java.util.function.Predicate;
-
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.deutschebahn.internal.filter.TimetableStopPredicate;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
/**
* @author Sönke Küper - initial contribution.
*/
@NonNullByDefault
-public enum TimetableStopFilter implements Predicate<TimetableStop> {
+public enum TimetableStopFilter implements TimetableStopPredicate {
/**
* Selects all entries.
*/
package org.openhab.binding.deutschebahn.internal;
+import java.util.Collections;
+import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
* Trip category.
*/
public static final TripLabelAttribute<String, StringType> C = new TripLabelAttribute<>("category", TripLabel::getC,
- TripLabel::setC, StringType::new, StringType.class);
+ TripLabel::setC, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Number.
*/
public static final TripLabelAttribute<String, StringType> N = new TripLabelAttribute<>("number", TripLabel::getN,
- TripLabel::setN, StringType::new, StringType.class);
+ TripLabel::setN, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Filter flags.
*/
public static final TripLabelAttribute<String, StringType> F = new TripLabelAttribute<>("filter-flags",
- TripLabel::getF, TripLabel::setF, StringType::new, StringType.class);
+ TripLabel::getF, TripLabel::setF, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Trip Type.
*/
public static final TripLabelAttribute<TripType, StringType> T = new TripLabelAttribute<>("trip-type",
- TripLabel::getT, TripLabel::setT, TripLabelAttribute::fromTripType, StringType.class);
+ TripLabel::getT, TripLabel::setT, TripLabelAttribute::fromTripType, TripLabelAttribute::listFromTripType,
+ StringType.class);
/**
* Owner.
*/
public static final TripLabelAttribute<String, StringType> O = new TripLabelAttribute<>("owner", TripLabel::getO,
- TripLabel::setO, StringType::new, StringType.class);
+ TripLabel::setO, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Creates an new {@link TripLabelAttribute}.
final Function<TripLabel, @Nullable VALUE_TYPE> getter, //
final BiConsumer<TripLabel, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
+ final Function<VALUE_TYPE, List<String>> valueToList, //
final Class<STATE_TYPE> stateType) {
- super(channelTypeName, getter, setter, getState, stateType);
+ super(channelTypeName, getter, setter, getState, valueToList, stateType);
}
@Nullable
return super.getState(stop.getTl());
}
+ @Override
+ public @Nullable Object getValue(TimetableStop stop) {
+ if (stop.getTl() == null) {
+ return UnDefType.UNDEF;
+ }
+ return super.getValue(stop.getTl());
+ }
+
+ @Override
+ public List<String> getStringValues(TimetableStop stop) {
+ if (stop.getTl() == null) {
+ return Collections.emptyList();
+ }
+ return this.getStringValues(stop.getTl());
+ }
+
private static StringType fromTripType(final TripType value) {
return new StringType(value.value());
}
+ private static List<String> listFromTripType(@Nullable final TripType value) {
+ if (value == null) {
+ return Collections.emptyList();
+ } else {
+ return Collections.singletonList(value.value());
+ }
+ }
+
+ /**
+ * Returns a list containing only the given value or empty list if value is <code>null</code>.
+ */
+ private static List<String> singletonList(@Nullable String value) {
+ return value == null ? Collections.emptyList() : Collections.singletonList(value);
+ }
+
/**
* Returns an {@link TripLabelAttribute} for the given channel-name.
*/
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A token representing an conjunction.
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public final class AndOperator extends OperatorToken {
+
+ /**
+ * Creates new {@link AndOperator}.
+ */
+ public AndOperator(int position) {
+ super(position);
+ }
+
+ @Override
+ public String toString() {
+ return "&";
+ }
+
+ @Override
+ public <R> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
+ return visitor.handle(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
+
+/**
+ * And conjunction for {@link TimetableStopPredicate}.
+ *
+ * @author Sönke Küper - initial contribution
+ */
+@NonNullByDefault
+public final class AndPredicate implements TimetableStopPredicate {
+
+ private final TimetableStopPredicate first;
+ private final TimetableStopPredicate second;
+
+ /**
+ * Creates an new {@link AndPredicate}.
+ */
+ public AndPredicate(TimetableStopPredicate first, TimetableStopPredicate second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @Override
+ public boolean test(TimetableStop t) {
+ return first.test(t) && second.test(t);
+ }
+
+ /**
+ * Returns first argument.
+ */
+ TimetableStopPredicate getFirst() {
+ return first;
+ }
+
+ /**
+ * Returns second argument.
+ */
+ TimetableStopPredicate getSecond() {
+ return second;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A token representing an closing bracket.
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public final class BracketCloseToken extends OperatorToken {
+
+ /**
+ * Creates new {@link BracketCloseToken}.
+ */
+ public BracketCloseToken(int position) {
+ super(position);
+ }
+
+ @Override
+ public String toString() {
+ return ")";
+ }
+
+ @Override
+ public <R> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
+ return visitor.handle(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A token representing an opening bracket.
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public final class BracketOpenToken extends OperatorToken {
+
+ /**
+ * Creates new {@link BracketOpenToken}.
+ */
+ public BracketOpenToken(int position) {
+ super(position);
+ }
+
+ @Override
+ public String toString() {
+ return "(";
+ }
+
+ @Override
+ public <R> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
+ return visitor.handle(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.deutschebahn.internal.AttributeSelection;
+import org.openhab.binding.deutschebahn.internal.EventAttribute;
+import org.openhab.binding.deutschebahn.internal.EventAttributeSelection;
+import org.openhab.binding.deutschebahn.internal.EventType;
+import org.openhab.binding.deutschebahn.internal.TripLabelAttribute;
+
+/**
+ * Token representing an attribute filter.
+ *
+ * @author Sönke Küper - initial contribution.
+ */
+@NonNullByDefault
+public final class ChannelNameEquals extends FilterToken {
+
+ private final String channelName;
+ private final Pattern filterValue;
+ private String channelGroup;
+
+ /**
+ * Creates an new {@link ChannelNameEquals}.
+ */
+ public ChannelNameEquals(int position, String channelGroup, String channelName, Pattern filterPattern) {
+ super(position);
+ this.channelGroup = channelGroup;
+ this.channelName = channelName;
+ this.filterValue = filterPattern;
+ }
+
+ /**
+ * Returns the channel group.
+ */
+ public String getChannelGroup() {
+ return channelGroup;
+ }
+
+ /**
+ * Returns the channel name.
+ */
+ public String getChannelName() {
+ return channelName;
+ }
+
+ /**
+ * Returns the filter value.
+ */
+ public Pattern getFilterValue() {
+ return filterValue;
+ }
+
+ @Override
+ public String toString() {
+ return this.channelGroup + "#" + channelName + "=\"" + this.filterValue.toString() + "\"";
+ }
+
+ @Override
+ public <R> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
+ return visitor.handle(this);
+ }
+
+ /**
+ * Maps this into an {@link TimetableStopByStringEventAttributeFilter}.
+ */
+ public TimetableStopByStringEventAttributeFilter mapToPredicate() throws FilterParserException {
+ return new TimetableStopByStringEventAttributeFilter(mapAttributeSelection(), filterValue);
+ }
+
+ private AttributeSelection mapAttributeSelection() throws FilterParserException {
+ switch (this.channelGroup) {
+ case "trip":
+ final TripLabelAttribute<?, ?> tripAttribute = TripLabelAttribute.getByChannelName(this.channelName);
+ if (tripAttribute == null) {
+ throw new FilterParserException("Invalid trip channel: " + channelName);
+ }
+ return tripAttribute;
+
+ case "departure":
+ final EventType eventTypeDeparture = EventType.DEPARTURE;
+ final EventAttribute<?, ?> departureAttribute = EventAttribute.getByChannelName(this.channelName,
+ eventTypeDeparture);
+ if (departureAttribute == null) {
+ throw new FilterParserException("Invalid departure channel: " + channelName);
+ }
+ return new EventAttributeSelection(eventTypeDeparture, departureAttribute);
+
+ case "arrival":
+ final EventType eventTypeArrival = EventType.ARRIVAL;
+ final EventAttribute<?, ?> arrivalAttribute = EventAttribute.getByChannelName(this.channelName,
+ eventTypeArrival);
+ if (arrivalAttribute == null) {
+ throw new FilterParserException("Invalid arrival channel: " + channelName);
+ }
+ return new EventAttributeSelection(eventTypeArrival, arrivalAttribute);
+ default:
+ throw new FilterParserException("Unknown channel group: " + channelGroup);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Parses an {@link FilterToken}-Sequence into a {@link TimetableStopPredicate}.
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public final class FilterParser {
+
+ /**
+ * Parser's state.
+ */
+ private abstract static class State implements FilterTokenVisitor<State> {
+
+ @Nullable
+ private State previousState;
+
+ public State(@Nullable State previousState) {
+ this.previousState = previousState;
+ }
+
+ private final State handle(FilterToken token) throws FilterParserException {
+ return token.accept(this);
+ }
+
+ protected abstract State handleChildResult(TimetableStopPredicate predicate) throws FilterParserException;
+
+ @Override
+ public final State handle(ChannelNameEquals channelEquals) throws FilterParserException {
+ final TimetableStopByStringEventAttributeFilter predicate = channelEquals.mapToPredicate();
+ return this.handleChildResult(predicate);
+ }
+
+ protected final State publishResultToPrevious(TimetableStopPredicate predicate) throws FilterParserException {
+ return this.getPreviousState().handleChildResult(predicate);
+ }
+
+ protected State getPreviousState() throws FilterParserException {
+ final State previousStateValue = this.previousState;
+ if (previousStateValue == null) {
+ throw new FilterParserException("Invalid filter");
+ } else {
+ return previousStateValue;
+ }
+ }
+
+ /**
+ * Returns the result.
+ */
+ public abstract TimetableStopPredicate getResult() throws FilterParserException;
+ }
+
+ /**
+ * Initial state for the parser.
+ */
+ private static final class InitialState extends State {
+
+ @Nullable
+ private TimetableStopPredicate result;
+
+ public InitialState() {
+ super(null);
+ }
+
+ @Override
+ public State handle(OrOperator operator) throws FilterParserException {
+ final TimetableStopPredicate currentResult = this.result;
+ this.result = null;
+ if (currentResult == null) {
+ throw new FilterParserException(
+ "Invalid filter: first argument missing for '|' at " + operator.getPosition());
+ }
+ return new OrState(this, currentResult);
+ }
+
+ @Override
+ public State handle(AndOperator operator) throws FilterParserException {
+ final TimetableStopPredicate currentResult = this.result;
+ this.result = null;
+ if (currentResult == null) {
+ throw new FilterParserException(
+ "Invalid filter: first argument missing for '&' at " + operator.getPosition());
+ }
+ return new AndState(this, currentResult);
+ }
+
+ @Override
+ public State handle(BracketOpenToken token) throws FilterParserException {
+ this.result = null;
+ return new SubQueryState(this);
+ }
+
+ @Override
+ public State handle(BracketCloseToken token) throws FilterParserException {
+ throw new FilterParserException("Unexpected token " + token.toString() + " at " + token.getPosition());
+ }
+
+ @Override
+ protected State handleChildResult(TimetableStopPredicate predicate) throws FilterParserException {
+ if (this.result == null) {
+ this.result = predicate;
+ return this;
+ } else {
+ throw new FilterParserException("Invalid filter: Operator for multiple filters missing.");
+ }
+ }
+
+ @Override
+ public TimetableStopPredicate getResult() throws FilterParserException {
+ final TimetableStopPredicate currentResult = this.result;
+ if (currentResult != null) {
+ return currentResult;
+ }
+ throw new FilterParserException("Invalid filter.");
+ }
+ }
+
+ /**
+ * State while parsing an conjunction.
+ */
+ private static final class AndState extends State {
+
+ private final TimetableStopPredicate first;
+
+ public AndState(State previousState, final TimetableStopPredicate first) {
+ super(previousState);
+ this.first = first;
+ }
+
+ @Override
+ public State handle(OrOperator operator) throws FilterParserException {
+ throw new FilterParserException("Invalid second argument for '&' operator " + operator.toString() + " at "
+ + operator.getPosition());
+ }
+
+ @Override
+ public State handle(AndOperator operator) throws FilterParserException {
+ throw new FilterParserException("Invalid second argument for '&' operator " + operator.toString() + " at "
+ + operator.getPosition());
+ }
+
+ @Override
+ public State handle(BracketOpenToken token) throws FilterParserException {
+ return new SubQueryState(this);
+ }
+
+ @Override
+ public State handle(BracketCloseToken token) throws FilterParserException {
+ throw new FilterParserException(
+ "Invalid second argument for '&' operator " + token.toString() + " at " + token.getPosition());
+ }
+
+ @Override
+ protected State handleChildResult(TimetableStopPredicate predicate) throws FilterParserException {
+ return this.publishResultToPrevious(new AndPredicate(first, predicate));
+ }
+
+ @Override
+ public TimetableStopPredicate getResult() throws FilterParserException {
+ throw new FilterParserException("Invalid filter");
+ }
+ }
+
+ /**
+ * State while parsing an disjunction.
+ */
+ private static final class OrState extends State {
+
+ private final TimetableStopPredicate first;
+
+ public OrState(State previousState, final TimetableStopPredicate first) {
+ super(previousState);
+ this.first = first;
+ }
+
+ @Override
+ public State handle(OrOperator operator) throws FilterParserException {
+ throw new FilterParserException("Invalid second argument for '|' operator " + operator.toString() + " at "
+ + operator.getPosition());
+ }
+
+ @Override
+ public State handle(AndOperator operator) throws FilterParserException {
+ throw new FilterParserException("Invalid second argument for '|' operator " + operator.toString() + " at "
+ + operator.getPosition());
+ }
+
+ @Override
+ public State handle(BracketOpenToken token) throws FilterParserException {
+ return new SubQueryState(this);
+ }
+
+ @Override
+ public State handle(BracketCloseToken token) throws FilterParserException {
+ throw new FilterParserException(
+ "Invalid second argument for '|' operator " + token.toString() + " at " + token.getPosition());
+ }
+
+ @Override
+ protected State handleChildResult(TimetableStopPredicate second) throws FilterParserException {
+ return this.publishResultToPrevious(new OrPredicate(first, second));
+ }
+
+ @Override
+ public TimetableStopPredicate getResult() throws FilterParserException {
+ throw new FilterParserException("Invalid filter");
+ }
+ }
+
+ /**
+ * State while parsing an Subquery.
+ */
+ private static final class SubQueryState extends State {
+
+ @Nullable
+ private TimetableStopPredicate currentResult;
+
+ public SubQueryState(State previousState) {
+ super(previousState);
+ }
+
+ @Override
+ public State handle(OrOperator operator) throws FilterParserException {
+ TimetableStopPredicate result = this.currentResult;
+ if (result == null) {
+ throw new FilterParserException(
+ "Operator '|' at " + operator.getPosition() + " must not be first element in subquery.");
+ }
+ return new OrState(this, result);
+ }
+
+ @Override
+ public State handle(AndOperator operator) throws FilterParserException {
+ TimetableStopPredicate result = this.currentResult;
+ if (result == null) {
+ throw new FilterParserException(
+ "Operator '&' at" + operator.getPosition() + " must not be first element in subquery.");
+ }
+ return new AndState(this, result);
+ }
+
+ @Override
+ public State handle(BracketOpenToken token) throws FilterParserException {
+ return new SubQueryState(this);
+ }
+
+ @Override
+ public State handle(BracketCloseToken token) throws FilterParserException {
+ TimetableStopPredicate result = this.currentResult;
+ if (result == null) {
+ throw new FilterParserException("Subquery must not be empty at " + token.getPosition());
+ }
+ return publishResultToPrevious(result);
+ }
+
+ @Override
+ protected State handleChildResult(TimetableStopPredicate predicate) {
+ this.currentResult = predicate;
+ return this;
+ }
+
+ @Override
+ public TimetableStopPredicate getResult() throws FilterParserException {
+ throw new FilterParserException("Invalid filter");
+ }
+ }
+
+ private FilterParser() {
+ }
+
+ /**
+ * Parses the given {@link FilterToken} into an {@link TimetableStopPredicate}.
+ */
+ public static TimetableStopPredicate parse(final List<FilterToken> tokens) throws FilterParserException {
+ State state = new InitialState();
+ for (FilterToken token : tokens) {
+ state = state.handle(token);
+ }
+ return state.getResult();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception showing problems during parsing a filter expression.
+ *
+ * @author Sönke Küper - initial contribution.
+ */
+@NonNullByDefault
+public final class FilterParserException extends Exception {
+
+ private static final long serialVersionUID = 3104578924298682889L;
+
+ /**
+ * Creates an new {@link FilterParserException}.
+ */
+ public FilterParserException(String message) {
+ super(message);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Scanner for filter expression.
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public final class FilterScanner {
+
+ private static final Set<Character> OP_CHARS = new HashSet<>(Arrays.asList('&', '|', '!', '(', ')'));
+ private static final Pattern CHANNEL_NAME = Pattern.compile("(trip|arrival|departure)#(\\S+)");
+
+ /**
+ * State of the scanner.
+ */
+ private interface State {
+
+ /**
+ * Handles the next read character.
+ *
+ * @return Returns the next scanner state.
+ */
+ public abstract State handle(int position, char currentChar) throws FilterScannerException;
+
+ /**
+ * Called when no more input is available.
+ */
+ public abstract void finish(int position) throws FilterScannerException;
+ }
+
+ /**
+ * Initial state of the scanner.
+ */
+ private final class InitialState implements State {
+
+ @Override
+ public State handle(int position, char currentChar) throws FilterScannerException {
+ // Skip white spaces
+ if (Character.isWhitespace(currentChar)) {
+ return this;
+ }
+
+ switch (currentChar) {
+ // Handle all operator tokens
+ case '&':
+ result.add(new AndOperator(position));
+ return this;
+ case '|':
+ result.add(new OrOperator(position));
+ return this;
+ case '(':
+ result.add(new BracketOpenToken(position));
+ return this;
+ case ')':
+ result.add(new BracketCloseToken(position));
+ return this;
+ default:
+ final ChannelNameState channelNameState = new ChannelNameState();
+ return channelNameState.handle(position, currentChar);
+ }
+ }
+
+ @Override
+ public void finish(int position) {
+ }
+ }
+
+ /**
+ * State scanning an channel name until the equals-sign.
+ */
+ private final class ChannelNameState implements State {
+
+ private final StringBuilder channelName = new StringBuilder();
+ private int startPosition = -1;
+
+ @Override
+ public State handle(int position, final char currentChar) throws FilterScannerException {
+ // Skip white spaces at front
+ if (Character.isWhitespace(currentChar) && channelName.toString().isEmpty()) {
+ return this;
+ }
+
+ if (Character.isWhitespace(currentChar)) {
+ throw new FilterScannerException(position, "Channel name must not contain whitespace.");
+ }
+
+ if (currentChar == '=') {
+ final String channelNameValue = this.channelName.toString();
+ if (channelNameValue.isEmpty()) {
+ throw new FilterScannerException(position, "Channel name must not be empty.");
+ }
+
+ final Matcher matcher = CHANNEL_NAME.matcher(channelNameValue);
+ if (!matcher.matches()) {
+ throw new FilterScannerException(position, "Invalid channel name: " + channelNameValue);
+ }
+
+ return new ExpectQuotesState(startPosition, matcher.group(1), matcher.group(2));
+ }
+
+ if (OP_CHARS.contains(currentChar)) {
+ throw new FilterScannerException(position, "Channel name must not contain operation char.");
+ }
+
+ this.channelName.append(currentChar);
+ if (startPosition == -1) {
+ startPosition = position;
+ }
+ return this;
+ }
+
+ @Override
+ public void finish(int position) throws FilterScannerException {
+ throw new FilterScannerException(position, "Filter value is missing.");
+ }
+ }
+
+ /**
+ * State after channel name, wiating for quotes.
+ */
+ private final class ExpectQuotesState implements State {
+
+ private final int startPosition;
+ private final String channelName;
+ private String channelGroup;
+
+ /**
+ * Creates an new {@link ExpectQuotesState}.
+ */
+ public ExpectQuotesState(int startPosition, final String channelGroup, String channelName) {
+ this.startPosition = startPosition;
+ this.channelGroup = channelGroup;
+ this.channelName = channelName;
+ }
+
+ @Override
+ public State handle(int position, char currentChar) throws FilterScannerException {
+ if (currentChar != '"') {
+ throw new FilterScannerException(position, "Filter value must start with quotes");
+ }
+ return new FilterValueState(startPosition, channelGroup, channelName);
+ }
+
+ @Override
+ public void finish(int position) throws FilterScannerException {
+ throw new FilterScannerException(position, "Filter value is missing.");
+ }
+ }
+
+ /**
+ * State scanning the filter value until next quotes.
+ */
+ private final class FilterValueState implements State {
+
+ private final int startPosition;
+ private final String channelGroup;
+ private final String channelName;
+ private final StringBuilder filterValue;
+
+ /**
+ * Creates an new {@link FilterValueState}.
+ */
+ public FilterValueState(int startPosition, String channelGroup, String channelName) {
+ this.startPosition = startPosition;
+ this.channelGroup = channelGroup;
+ this.channelName = channelName;
+ this.filterValue = new StringBuilder();
+ }
+
+ @Override
+ public State handle(int position, char currentChar) throws FilterScannerException {
+ if (currentChar == '"') {
+ finish(position);
+ return new InitialState();
+ }
+ filterValue.append(currentChar);
+ return this;
+ }
+
+ @Override
+ public void finish(int position) throws FilterScannerException {
+ String filterPattern = this.filterValue.toString();
+ try {
+ result.add(new ChannelNameEquals(startPosition, this.channelGroup, this.channelName,
+ Pattern.compile(filterPattern)));
+ } catch (PatternSyntaxException e) {
+ throw new FilterScannerException(position, "Filter pattern is invalid: " + filterPattern, e);
+ }
+ }
+ }
+
+ private List<FilterToken> result;
+
+ /**
+ * Creates an new {@link FilterScanner}.
+ */
+ public FilterScanner() {
+ this.result = new ArrayList<>();
+ }
+
+ /**
+ * Scans the given filter expression and returns the result sequence of {@link FilterToken}.
+ */
+ public List<FilterToken> processInput(String value) throws FilterScannerException {
+ State state = new InitialState();
+ for (int pos = 0; pos < value.length(); pos++) {
+ char currentChar = value.charAt(pos);
+ state = state.handle(pos + 1, currentChar);
+ }
+
+ state.finish(value.length());
+
+ return this.result;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import java.util.regex.PatternSyntaxException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception for errors within the filter scanner.
+ *
+ * @author Sönke Küper - initial contribution
+ */
+@NonNullByDefault
+public final class FilterScannerException extends Exception {
+
+ private static final long serialVersionUID = -7319023069454747511L;
+
+ /**
+ * Creates an exception with given position and message.
+ */
+ FilterScannerException(int position, String message) {
+ super("Scanner failed at positon: " + position + ": " + message);
+ }
+
+ /**
+ * Creates an exception with given position, message and cause.
+ */
+ FilterScannerException(int position, String message, PatternSyntaxException e) {
+ super("Scanner failed at positon: " + position + ": " + message, e);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A token representing a part of an filter expression.
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public abstract class FilterToken {
+
+ private final int position;
+
+ /**
+ * Creates an new {@link FilterToken}.
+ */
+ public FilterToken(int position) {
+ this.position = position;
+ }
+
+ /**
+ * Returns the start position of the token.
+ */
+ public final int getPosition() {
+ return position;
+ }
+
+ /**
+ * Accept for {@link FilterTokenVisitor}.
+ */
+ public abstract <R> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Visitor for {@link FilterToken}.
+ *
+ * @author Sönke Küper - Initial Contribution.
+ *
+ * @param <R> Return type.
+ */
+@NonNullByDefault
+public interface FilterTokenVisitor<R> {
+
+ /**
+ * Handles {@link ChannelNameEquals}.
+ */
+ public abstract R handle(ChannelNameEquals equals) throws FilterParserException;
+
+ /**
+ * Handles {@link OrOperator}.
+ */
+ public abstract R handle(OrOperator operator) throws FilterParserException;
+
+ /**
+ * Handles {@link AndOperator}.
+ */
+ public abstract R handle(AndOperator operator) throws FilterParserException;
+
+ /**
+ * Handles {@link BracketOpenToken}.
+ */
+ public abstract R handle(BracketOpenToken token) throws FilterParserException;
+
+ /**
+ * Handles {@link BracketCloseToken}.
+ */
+ public abstract R handle(BracketCloseToken token) throws FilterParserException;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Abstraction for all operators.
+ *
+ * @author Sönke Küper - initial contribution.
+ */
+@NonNullByDefault
+public abstract class OperatorToken extends FilterToken {
+
+ /**
+ * Creates an new {@link OperatorToken}.
+ */
+ public OperatorToken(int position) {
+ super(position);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A token representing an disjunction.
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public final class OrOperator extends OperatorToken {
+
+ /**
+ * Creates new {@link OrOperator}.
+ */
+ public OrOperator(int position) {
+ super(position);
+ }
+
+ @Override
+ public String toString() {
+ return "|";
+ }
+
+ @Override
+ public <R> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
+ return visitor.handle(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
+
+/**
+ * Disjunction for {@link TimetableStopPredicate}.
+ *
+ * @author Sönke Küper - initial contribution
+ */
+@NonNullByDefault
+final class OrPredicate implements TimetableStopPredicate {
+
+ private final TimetableStopPredicate first;
+ private final TimetableStopPredicate second;
+
+ /**
+ * Creates an new {@link OrPredicate}.
+ */
+ public OrPredicate(TimetableStopPredicate first, TimetableStopPredicate second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @Override
+ public boolean test(TimetableStop t) {
+ return first.test(t) || second.test(t);
+ }
+
+ /**
+ * Returns first argument.
+ */
+ TimetableStopPredicate getFirst() {
+ return first;
+ }
+
+ /**
+ * Returns second argument.
+ */
+ TimetableStopPredicate getSecond() {
+ return second;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.deutschebahn.internal.AttributeSelection;
+import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
+
+/**
+ * Abstract predicate that filters timetable stops by an selected attribute of an {@link TimetableStop}.
+ *
+ * If value has multiple values (for example stations on the planned-path) the predicate will return <code>true</code>,
+ * if at least one value matches the given filter.
+ *
+ * @author Sönke Küper - initial contribution
+ */
+@NonNullByDefault
+public final class TimetableStopByStringEventAttributeFilter implements TimetableStopPredicate {
+
+ private final AttributeSelection attributeSelection;
+ private final Pattern filter;
+
+ /**
+ * Creates an new {@link TimetableStopByStringEventAttributeFilter}.
+ */
+ TimetableStopByStringEventAttributeFilter(final AttributeSelection attributeSelection, final Pattern filter) {
+ this.attributeSelection = attributeSelection;
+ this.filter = filter;
+ }
+
+ @Override
+ public boolean test(TimetableStop t) {
+ final List<String> values = attributeSelection.getStringValues(t);
+
+ for (String actualValue : values) {
+ if (filter.matcher(actualValue).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the {@link AttributeSelection}.
+ */
+ final AttributeSelection getAttributeSelection() {
+ return attributeSelection;
+ }
+
+ /**
+ * Returns the filter pattern.
+ */
+ final Pattern getFilter() {
+ return filter;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import java.util.function.Predicate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
+
+/**
+ * Predicate to match an TimetableStop
+ *
+ * @author Sönke Küper - initial contribution.
+ */
+@NonNullByDefault
+public interface TimetableStopPredicate extends Predicate<TimetableStop> {
+
+}
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deutschebahn.internal.EventAttribute;
import org.openhab.binding.deutschebahn.internal.EventType;
-import org.openhab.binding.deutschebahn.internal.TimetableStopFilter;
+import org.openhab.binding.deutschebahn.internal.filter.TimetableStopPredicate;
import org.openhab.binding.deutschebahn.internal.timetable.dto.Event;
import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
private final Map<String, TimetableStop> cachedChanges;
private final TimetablesV1Api api;
- private final TimetableStopFilter stopFilter;
+ private final TimetableStopPredicate stopPredicate;
private final TimetableStopComparator comparator;
private final Supplier<Date> currentTimeProvider;
private int stopCount;
* Creates an new {@link TimetableLoader}.
*
* @param api {@link TimetablesV1Api} to use.
- * @param stopFilter Filter for selection of loaded {@link TimetableStop}.
+ * @param stopPredicate Filter for selection of loaded {@link TimetableStop}.
* @param requestedStopCount Count of stops to be loaded on each call.
* @param currentTimeProvider {@link Supplier} for the current time.
*/
- public TimetableLoader(final TimetablesV1Api api, final TimetableStopFilter stopFilter, final EventType eventToSort,
- final Supplier<Date> currentTimeProvider, final String evaNo, final int requestedStopCount) {
+ public TimetableLoader(final TimetablesV1Api api, final TimetableStopPredicate stopPredicate,
+ final EventType eventToSort, final Supplier<Date> currentTimeProvider, final String evaNo,
+ final int requestedStopCount) {
this.api = api;
- this.stopFilter = stopFilter;
+ this.stopPredicate = stopPredicate;
this.currentTimeProvider = currentTimeProvider;
this.evaNo = evaNo;
this.stopCount = requestedStopCount;
final List<TimetableStop> stops = timetable //
.getS() //
.stream() //
- .filter(this.stopFilter) //
+ .filter(this.stopPredicate) //
.collect(Collectors.toList());
// Merge the loaded stops with the cached changes and put them into the plan cache.
<option value="departures">Departures</option>
</options>
</parameter>
+ <parameter name="additionalFilter" type="text" required="false">
+ <advanced>true</advanced>
+ <label>Additional Filter</label>
+ <description>Specifies additional filters for trains, that should be displayed within the timetable.</description>
+ </parameter>
</config-description>
</bridge-type>
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.function.Consumer;
public void testPlannedIntermediateStations() {
String expectedFollowing = "Bielefeld Hbf - Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf";
doTestEventAttribute("planned-intermediate-stations", "planned-following-stations",
- (Event e) -> e.setPpth(SAMPLE_PATH), expectedFollowing, new StringType(expectedFollowing),
- EventType.DEPARTURE, false);
+ (Event e) -> e.setPpth(SAMPLE_PATH),
+ Arrays.asList("Bielefeld Hbf", "Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica",
+ "Minden(Westf)", "Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf"),
+ new StringType(expectedFollowing), EventType.DEPARTURE, false);
String expectedPrevious = "Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf - Lehrte";
doTestEventAttribute("planned-intermediate-stations", "planned-previous-stations",
- (Event e) -> e.setPpth(SAMPLE_PATH), expectedPrevious, new StringType(expectedPrevious),
- EventType.ARRIVAL, false);
+ (Event e) -> e.setPpth(SAMPLE_PATH),
+ Arrays.asList("Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica", "Minden(Westf)",
+ "Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf", "Lehrte"),
+ new StringType(expectedPrevious), EventType.ARRIVAL, false);
}
@Test
public void testChangedIntermediateStations() {
String expectedFollowing = "Bielefeld Hbf - Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf";
doTestEventAttribute("changed-intermediate-stations", "changed-following-stations",
- (Event e) -> e.setCpth(SAMPLE_PATH), expectedFollowing, new StringType(expectedFollowing),
- EventType.DEPARTURE, false);
+ (Event e) -> e.setCpth(SAMPLE_PATH),
+ Arrays.asList("Bielefeld Hbf", "Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica",
+ "Minden(Westf)", "Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf"),
+ new StringType(expectedFollowing), EventType.DEPARTURE, false);
String expectedPrevious = "Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf - Lehrte";
doTestEventAttribute("changed-intermediate-stations", "changed-previous-stations",
- (Event e) -> e.setCpth(SAMPLE_PATH), expectedPrevious, new StringType(expectedPrevious),
- EventType.ARRIVAL, false);
+ (Event e) -> e.setCpth(SAMPLE_PATH),
+ Arrays.asList("Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica", "Minden(Westf)",
+ "Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf", "Lehrte"),
+ new StringType(expectedPrevious), EventType.ARRIVAL, false);
}
@Test
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.deutschebahn.internal.AttributeSelection;
+import org.openhab.binding.deutschebahn.internal.EventAttribute;
+import org.openhab.binding.deutschebahn.internal.EventAttributeSelection;
+import org.openhab.binding.deutschebahn.internal.EventType;
+import org.openhab.binding.deutschebahn.internal.TripLabelAttribute;
+
+/**
+ * Tests for {@link FilterParser}
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public class FilterParserTest {
+
+ private static final class FilterTokenSequenceBuilder {
+
+ private final List<FilterToken> tokens = new ArrayList<>();
+ private int position = 0;
+
+ private int getPos() {
+ this.position++;
+ return this.position;
+ }
+
+ public List<FilterToken> build() {
+ return this.tokens;
+ }
+
+ public FilterTokenSequenceBuilder and() {
+ this.tokens.add(new AndOperator(getPos()));
+ return this;
+ }
+
+ public FilterTokenSequenceBuilder or() {
+ this.tokens.add(new OrOperator(getPos()));
+ return this;
+ }
+
+ public FilterTokenSequenceBuilder bracketOpen() {
+ this.tokens.add(new BracketOpenToken(getPos()));
+ return this;
+ }
+
+ public FilterTokenSequenceBuilder bracketClose() {
+ this.tokens.add(new BracketCloseToken(getPos()));
+ return this;
+ }
+
+ public ChannelNameEquals channelFilter(String channelGroup, String channelName, String pattern) {
+ ChannelNameEquals channelNameEquals = new ChannelNameEquals(getPos(), channelGroup, channelName,
+ Pattern.compile(pattern));
+ this.tokens.add(channelNameEquals);
+ return channelNameEquals;
+ }
+
+ public FilterTokenSequenceBuilder channelFilter(ChannelNameEquals equals) {
+ this.tokens.add(equals);
+ return this;
+ }
+ }
+
+ private static FilterTokenSequenceBuilder builder() {
+ return new FilterTokenSequenceBuilder();
+ }
+
+ private static void checkAttributeFilter(TimetableStopPredicate predicate, ChannelNameEquals channelEquals,
+ EventType eventType, EventAttribute<?, ?> eventAttribute) {
+ checkAttributeFilter(predicate, channelEquals, new EventAttributeSelection(eventType, eventAttribute));
+ }
+
+ private static void checkAttributeFilter(TimetableStopPredicate predicate, ChannelNameEquals channelEquals,
+ AttributeSelection attributeSelection) {
+ assertThat(predicate, is(instanceOf(TimetableStopByStringEventAttributeFilter.class)));
+ TimetableStopByStringEventAttributeFilter attributeFilter = (TimetableStopByStringEventAttributeFilter) predicate;
+ assertThat(attributeFilter.getFilter(), is(channelEquals.getFilterValue()));
+ assertThat(attributeFilter.getAttributeSelection(), is(attributeSelection));
+ }
+
+ private static OrPredicate assertOr(TimetableStopPredicate predicate) {
+ assertThat(predicate, is(instanceOf(OrPredicate.class)));
+ return (OrPredicate) predicate;
+ }
+
+ private static AndPredicate assertAnd(TimetableStopPredicate predicate) {
+ assertThat(predicate, is(instanceOf(AndPredicate.class)));
+ return (AndPredicate) predicate;
+ }
+
+ @Test
+ public void testParseSimple() throws FilterParserException {
+ final List<FilterToken> input = new ArrayList<>();
+ ChannelNameEquals channelEquals = new ChannelNameEquals(1, "trip", "number", Pattern.compile("20"));
+ input.add(channelEquals);
+ final TimetableStopPredicate result = FilterParser.parse(input);
+ checkAttributeFilter(result, channelEquals, TripLabelAttribute.N);
+ }
+
+ @Test
+ public void testParseAnd() throws FilterParserException {
+ final FilterTokenSequenceBuilder b = builder();
+ final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
+ b.and();
+ final ChannelNameEquals channelEquals02 = b.channelFilter("trip", "number", "30");
+ final TimetableStopPredicate result = FilterParser.parse(b.build());
+ final AndPredicate andPredicate = assertAnd(result);
+
+ checkAttributeFilter(andPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
+ checkAttributeFilter(andPredicate.getSecond(), channelEquals02, TripLabelAttribute.N);
+ }
+
+ @Test
+ public void testParseOr() throws FilterParserException {
+ final FilterTokenSequenceBuilder b = builder();
+ final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
+ b.or();
+ final ChannelNameEquals channelEquals02 = b.channelFilter("trip", "number", "30");
+ final TimetableStopPredicate result = FilterParser.parse(b.build());
+ final OrPredicate orPredicate = assertOr(result);
+
+ checkAttributeFilter(orPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
+ checkAttributeFilter(orPredicate.getSecond(), channelEquals02, TripLabelAttribute.N);
+ }
+
+ @Test
+ public void testParseWithBrackets() throws FilterParserException {
+ final FilterTokenSequenceBuilder b = new FilterTokenSequenceBuilder();
+ final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
+ b.and();
+ b.bracketOpen();
+ final ChannelNameEquals channelEquals02 = b.channelFilter("departure", "line", "RE10");
+ b.or();
+ final ChannelNameEquals channelEquals03 = b.channelFilter("departure", "line", "RE20");
+ b.bracketClose();
+ final List<FilterToken> input = b.build();
+
+ final TimetableStopPredicate result = FilterParser.parse(input);
+ final AndPredicate andPredicate = assertAnd(result);
+
+ checkAttributeFilter(andPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
+ final OrPredicate orPredicate = assertOr(andPredicate.getSecond());
+
+ checkAttributeFilter(orPredicate.getFirst(), channelEquals02, EventType.DEPARTURE, EventAttribute.L);
+ checkAttributeFilter(orPredicate.getSecond(), channelEquals03, EventType.DEPARTURE, EventAttribute.L);
+ }
+
+ @Test
+ public void testParseWithMultipleBrackets() throws FilterParserException {
+ final FilterTokenSequenceBuilder b = builder();
+ b.bracketOpen();
+ b.bracketOpen();
+ final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
+ b.and();
+ final ChannelNameEquals channelEquals02 = b.channelFilter("departure", "line", "RE22");
+ b.bracketClose();
+ b.or();
+ b.bracketOpen();
+ final ChannelNameEquals channelEquals03 = b.channelFilter("trip", "number", "30");
+ b.and();
+ final ChannelNameEquals channelEquals04 = b.channelFilter("departure", "line", "RE33");
+ b.bracketClose();
+ b.bracketClose();
+
+ final List<FilterToken> input = b.build();
+
+ final TimetableStopPredicate result = FilterParser.parse(input);
+ final OrPredicate orPredicate = assertOr(result);
+
+ final AndPredicate firstAnd = assertAnd(orPredicate.getFirst());
+ checkAttributeFilter(firstAnd.getFirst(), channelEquals01, TripLabelAttribute.N);
+ checkAttributeFilter(firstAnd.getSecond(), channelEquals02, EventType.DEPARTURE, EventAttribute.L);
+
+ final AndPredicate secondAnd = assertAnd(orPredicate.getSecond());
+ checkAttributeFilter(secondAnd.getFirst(), channelEquals03, TripLabelAttribute.N);
+ checkAttributeFilter(secondAnd.getSecond(), channelEquals04, EventType.DEPARTURE, EventAttribute.L);
+ }
+
+ @Test
+ public void testParseErrors() {
+ final ChannelNameEquals channelEquals = new ChannelNameEquals(1, "trip", "number", Pattern.compile("20"));
+ try {
+ FilterParser.parse(Collections.emptyList());
+ fail();
+ } catch (FilterParserException e) {
+ }
+
+ try {
+ FilterParser.parse(builder().and().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+
+ try {
+ FilterParser.parse(builder().or().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().bracketOpen().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().bracketClose().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().bracketOpen().bracketClose().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().bracketOpen().and().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().bracketOpen().and().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().channelFilter(channelEquals).and().bracketOpen().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().channelFilter(channelEquals).and().bracketClose().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().channelFilter(channelEquals).or().bracketOpen().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().channelFilter(channelEquals).or().bracketClose().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().channelFilter(channelEquals).and().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(builder().channelFilter(channelEquals).or().build());
+ fail();
+ } catch (FilterParserException e) {
+ }
+ try {
+ FilterParser.parse(Arrays.asList(channelEquals, channelEquals));
+ fail();
+ } catch (FilterParserException e) {
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link FilterScanner}
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public class FilterScannerTest {
+
+ private static void assertAttributeEquals(FilterToken token, String expectedChannelGroup,
+ String expectedChannelName, String expectedFilter, int expectedPosition) {
+ assertThat(token, is(instanceOf(ChannelNameEquals.class)));
+ ChannelNameEquals actual = (ChannelNameEquals) token;
+ assertThat(actual.getChannelGroup(), is(expectedChannelGroup));
+ assertThat(actual.getChannelName(), is(expectedChannelName));
+ assertThat(actual.getFilterValue().toString(), is(expectedFilter));
+ assertThat(actual.getPosition(), is(expectedPosition));
+ }
+
+ private static void assertOperator(FilterToken token, OperatorToken expected) {
+ assertThat(token.getClass(), is(expected.getClass()));
+ assertThat(token.getPosition(), is(expected.getPosition()));
+ }
+
+ private static List<FilterToken> processInput(String input, int expectedCount) throws FilterScannerException {
+ final List<FilterToken> tokens = new FilterScanner().processInput(input);
+ assertThat(tokens, hasSize(expectedCount));
+ return tokens;
+ }
+
+ @Test
+ public void testSimpleAttributEquals() throws FilterScannerException {
+ String input = "trip#number=\"20\"";
+ List<FilterToken> tokens = processInput(input, 1);
+ assertAttributeEquals(tokens.get(0), "trip", "number", "20", 1);
+ }
+
+ @Test
+ public void testAttributeEqualsWithWhitespace() throws FilterScannerException {
+ String input = "departure#planned-path=\"Hannover Hbf\"";
+ List<FilterToken> tokens = processInput(input, 1);
+ assertAttributeEquals(tokens.get(0), "departure", "planned-path", "Hannover Hbf", 1);
+ }
+
+ @Test
+ public void testInvalidAttributEquals() {
+ try {
+ new FilterScanner().processInput("trip#number=20");
+ fail();
+ } catch (FilterScannerException e) {
+ }
+
+ try {
+ new FilterScanner().processInput("trip#number");
+ fail();
+ } catch (FilterScannerException e) {
+ }
+
+ try {
+ new FilterScanner().processInput("trip#number=");
+ fail();
+ } catch (FilterScannerException e) {
+ }
+
+ try {
+ new FilterScanner().processInput("=abc");
+ fail();
+ } catch (FilterScannerException e) {
+ }
+
+ try {
+ new FilterScanner().processInput("train#number=\"abc\"");
+ fail();
+ } catch (FilterScannerException e) {
+ }
+ }
+
+ @Test
+ public void testComplexExample() throws FilterScannerException {
+ String input = "trip#category=\"RE\" & (departure#line=\"17\" | departure#line=\"57\") & departure#planned-path=\"Cologne\"";
+ List<FilterToken> tokens = processInput(input, 9);
+ assertAttributeEquals(tokens.get(0), "trip", "category", "RE", 1);
+ assertOperator(tokens.get(1), new AndOperator(20));
+ assertOperator(tokens.get(2), new BracketOpenToken(22));
+ assertAttributeEquals(tokens.get(3), "departure", "line", "17", 23);
+ assertOperator(tokens.get(4), new OrOperator(43));
+ assertAttributeEquals(tokens.get(5), "departure", "line", "57", 45);
+ assertOperator(tokens.get(6), new BracketCloseToken(64));
+ assertOperator(tokens.get(7), new AndOperator(66));
+ assertAttributeEquals(tokens.get(8), "departure", "planned-path", "Cologne", 68);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.deutschebahn.internal.filter;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.deutschebahn.internal.EventAttribute;
+import org.openhab.binding.deutschebahn.internal.EventAttributeSelection;
+import org.openhab.binding.deutschebahn.internal.EventType;
+import org.openhab.binding.deutschebahn.internal.TripLabelAttribute;
+import org.openhab.binding.deutschebahn.internal.timetable.dto.Event;
+import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
+import org.openhab.binding.deutschebahn.internal.timetable.dto.TripLabel;
+
+/**
+ * Tests for {@link TimetableStopByStringEventAttributeFilter}
+ *
+ * @author Sönke Küper - Initial contribution.
+ */
+@NonNullByDefault
+public final class TimetableByStringEventAttributeFilterTest {
+
+ @Test
+ public void testFilterTripLabelAttribute() {
+ final TimetableStopByStringEventAttributeFilter filter = new TimetableStopByStringEventAttributeFilter(
+ TripLabelAttribute.C, Pattern.compile("IC.*"));
+ final TimetableStop stop = new TimetableStop();
+
+ // TripLabel is not set -> does not match
+ assertFalse(filter.test(stop));
+
+ final TripLabel label = new TripLabel();
+ stop.setTl(label);
+
+ // Attribute is not set -> does not match
+ assertFalse(filter.test(stop));
+
+ // Set attribute -> matches depending on value
+ label.setC("RE");
+ assertFalse(filter.test(stop));
+ label.setC("ICE");
+ assertTrue(filter.test(stop));
+ label.setC("IC");
+ assertTrue(filter.test(stop));
+ }
+
+ @Test
+ public void testFilterEventAttribute() {
+ final EventAttributeSelection eventAttribute = new EventAttributeSelection(EventType.DEPARTURE,
+ EventAttribute.L);
+ final TimetableStopByStringEventAttributeFilter filter = new TimetableStopByStringEventAttributeFilter(
+ eventAttribute, Pattern.compile("RE.*"));
+ final TimetableStop stop = new TimetableStop();
+
+ // Event is not set -> does not match
+ assertFalse(filter.test(stop));
+
+ Event event = new Event();
+ stop.setDp(event);
+
+ // Attribute is not set -> does not match
+ assertFalse(filter.test(stop));
+
+ // Set attribute -> matches depending on value
+ event.setL("S5");
+ assertFalse(filter.test(stop));
+ event.setL("5");
+ assertFalse(filter.test(stop));
+ event.setL("RE60");
+ assertTrue(filter.test(stop));
+
+ // Set wrong event
+ stop.setAr(event);
+ stop.setDp(null);
+ assertFalse(filter.test(stop));
+ }
+
+ @Test
+ public void testFilterEventAttributeList() {
+ final EventAttributeSelection eventAttribute = new EventAttributeSelection(EventType.DEPARTURE,
+ EventAttribute.PPTH);
+ final TimetableStopByStringEventAttributeFilter filter = new TimetableStopByStringEventAttributeFilter(
+ eventAttribute, Pattern.compile("Hannover.*"));
+ final TimetableStop stop = new TimetableStop();
+ Event event = new Event();
+ stop.setDp(event);
+
+ event.setPpth("Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten");
+ assertTrue(filter.test(stop));
+ event.setPpth(
+ "Ahlten|Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten");
+ assertTrue(filter.test(stop));
+ event.setPpth(
+ "Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke");
+ assertFalse(filter.test(stop));
+ }
+}