diff --git a/bundles/org.openhab.binding.deutschebahn/README.md b/bundles/org.openhab.binding.deutschebahn/README.md index 23184d523fe..9a2aec47e50 100644 --- a/bundles/org.openhab.binding.deutschebahn/README.md +++ b/bundles/org.openhab.binding.deutschebahn/README.md @@ -40,7 +40,27 @@ In addition you can configure if only arrivals, only departures or all trains sh | `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 diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AbstractDtoAttributeSelector.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AbstractDtoAttributeSelector.java index b5c6db1040b..827008f156f 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AbstractDtoAttributeSelector.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AbstractDtoAttributeSelector.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.deutschebahn.internal; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Function; @@ -37,6 +38,7 @@ public abstract class AbstractDtoAttributeSelector getState; private final String channelTypeName; private final Class stateType; + private final Function> valueToList; /** * Creates an new {@link EventAttribute}. @@ -49,11 +51,13 @@ public abstract class AbstractDtoAttributeSelector getter, // final BiConsumer setter, // final Function getState, // + final Function> valueToList, // final Class stateType) { this.channelTypeName = channelTypeName; this.getter = getter; this.setter = setter; this.getState = getState; + this.valueToList = valueToList; this.stateType = stateType; } @@ -92,6 +96,14 @@ public abstract class AbstractDtoAttributeSelector getStringValues(DTO_TYPE object) { + return this.valueToList.apply(getValue(object)); + } + /** * Sets the value for the selected attribute in the given DTO object */ diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AttributeSelection.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AttributeSelection.java index 6c0d7670669..54226218025 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AttributeSelection.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AttributeSelection.java @@ -12,6 +12,8 @@ */ 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; @@ -25,9 +27,21 @@ import org.openhab.core.types.State; @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 getStringValues(TimetableStop t); } diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableConfiguration.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableConfiguration.java index ee93c69650e..f04d0a09a94 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableConfiguration.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableConfiguration.java @@ -12,7 +12,16 @@ */ 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. @@ -37,10 +46,28 @@ public class DeutscheBahnTimetableConfiguration { */ 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 null if not specified. + */ + public @Nullable TimetableStopPredicate getAdditionalFilter() throws FilterScannerException, FilterParserException { + if (additionalFilter.isBlank()) { + return null; + } else { + final FilterScanner scanner = new FilterScanner(); + final List filterTokens = scanner.processInput(additionalFilter); + return FilterParser.parse(filterTokens); + } + } } diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandler.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandler.java index 616493a9991..6daaca73205 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandler.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandler.java @@ -30,6 +30,10 @@ import javax.xml.bind.JAXBException; 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; @@ -151,14 +155,22 @@ public class DeutscheBahnTimetableHandler extends BaseBridgeHandler { 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, // @@ -170,6 +182,8 @@ public class DeutscheBahnTimetableHandler extends BaseBridgeHandler { 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()); diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttribute.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttribute.java index 26ad3e5a098..d67cd6bd65d 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttribute.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttribute.java @@ -17,6 +17,7 @@ import java.text.SimpleDateFormat; 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; @@ -55,145 +56,155 @@ public final class EventAttribute * Planned Path. */ public static final EventAttribute 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 L = new EventAttribute<>("line", Event::getL, Event::setL, - StringType::new, StringType.class); + StringType::new, EventAttribute::singletonList, StringType.class); /** * Messages. */ public static final EventAttribute, 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 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 PLANNED_PREVIOUS_STATIONS = new EventAttribute<>( + public static final EventAttribute, 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 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 PLANNED_FOLLOWING_STATIONS = new EventAttribute<>( + public static final EventAttribute, 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 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 CHANGED_PREVIOUS_STATIONS = new EventAttribute<>( + public static final EventAttribute, 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 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 CHANGED_FOLLOWING_STATIONS = new EventAttribute<>( + public static final EventAttribute, 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}. @@ -214,14 +225,38 @@ public final class EventAttribute final Function getter, // final BiConsumer setter, // final Function getState, // + final Function> valueToList, // final Class 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 listFromEventStatus(final @Nullable EventStatus value) { + if (value == null) { + return Collections.emptyList(); + } else { + return Collections.singletonList(value.value()); + } + } + + private static StringType fromStringList(final List value) { + return new StringType(value.stream().collect(Collectors.joining(" - "))); + } + + private static List nullToEmptyList(@Nullable final List value) { + return value == null ? Collections.emptyList() : value; + } + + /** + * Returns a list containing only the given value or empty list if value is null. + */ + private static List 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); } @@ -291,6 +326,24 @@ public final class EventAttribute } } + /** + * Maps the status codes from the messages into string list. + */ + private static List mapMessagesToList(final @Nullable List 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> getMessages() { return new Function>() { @@ -305,6 +358,22 @@ public final class EventAttribute }; } + private static List mapIntegerToStringList(@Nullable Integer value) { + if (value == null) { + return Collections.emptyList(); + } else { + return Collections.singletonList(String.valueOf(value)); + } + } + + private static List 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). * @@ -337,7 +406,7 @@ public final class EventAttribute * @param removeFirst if true the first value will be removed, false will remove the last * value. */ - private static Function getIntermediateStationsFromPath( + private static Function> getIntermediateStationsFromPath( final Function getPath, boolean removeFirst) { return (final Event event) -> { final String path = getPath.apply(event); @@ -351,7 +420,7 @@ public final class EventAttribute } else { stations = stations.limit(stationValues.length - 1); } - return stations.collect(Collectors.joining(" - ")); + return stations.collect(Collectors.toList()); }; } @@ -372,6 +441,10 @@ public final class EventAttribute return path.split("\\|"); } + private static List splitOnPipeToList(final String value) { + return Arrays.asList(value.split("\\|")); + } + /** * Returns an {@link EventAttribute} for the given channel-type and {@link EventType}. */ diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttributeSelection.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttributeSelection.java index 51224949f9a..ee681ec3b65 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttributeSelection.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttributeSelection.java @@ -12,6 +12,10 @@ */ 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; @@ -49,4 +53,38 @@ public final class EventAttributeSelection implements AttributeSelection { 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 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; + } } diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TimetableStopFilter.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TimetableStopFilter.java index e0256f42453..192b4b39e35 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TimetableStopFilter.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TimetableStopFilter.java @@ -12,9 +12,8 @@ */ 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; /** @@ -23,7 +22,7 @@ import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; * @author Sönke Küper - initial contribution. */ @NonNullByDefault -public enum TimetableStopFilter implements Predicate { +public enum TimetableStopFilter implements TimetableStopPredicate { /** * Selects all entries. diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TripLabelAttribute.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TripLabelAttribute.java index 2acbaeaab5e..3b0750bd0a8 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TripLabelAttribute.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TripLabelAttribute.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.deutschebahn.internal; +import java.util.Collections; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Function; @@ -44,29 +46,30 @@ public final class TripLabelAttribute exte * Trip category. */ public static final TripLabelAttribute 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 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 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 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 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}. @@ -79,8 +82,9 @@ public final class TripLabelAttribute exte final Function getter, // final BiConsumer setter, // final Function getState, // + final Function> valueToList, // final Class stateType) { - super(channelTypeName, getter, setter, getState, stateType); + super(channelTypeName, getter, setter, getState, valueToList, stateType); } @Nullable @@ -92,10 +96,41 @@ public final class TripLabelAttribute exte 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 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 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 null. + */ + private static List singletonList(@Nullable String value) { + return value == null ? Collections.emptyList() : Collections.singletonList(value); + } + /** * Returns an {@link TripLabelAttribute} for the given channel-name. */ diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/AndOperator.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/AndOperator.java new file mode 100644 index 00000000000..eb5a5b0de2e --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/AndOperator.java @@ -0,0 +1,41 @@ +/** + * 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 accept(FilterTokenVisitor visitor) throws FilterParserException { + return visitor.handle(this); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/AndPredicate.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/AndPredicate.java new file mode 100644 index 00000000000..a2162a17bb4 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/AndPredicate.java @@ -0,0 +1,55 @@ +/** + * 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; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/BracketCloseToken.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/BracketCloseToken.java new file mode 100644 index 00000000000..f0565693373 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/BracketCloseToken.java @@ -0,0 +1,41 @@ +/** + * 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 accept(FilterTokenVisitor visitor) throws FilterParserException { + return visitor.handle(this); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/BracketOpenToken.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/BracketOpenToken.java new file mode 100644 index 00000000000..03d00cdf708 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/BracketOpenToken.java @@ -0,0 +1,41 @@ +/** + * 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 accept(FilterTokenVisitor visitor) throws FilterParserException { + return visitor.handle(this); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/ChannelNameEquals.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/ChannelNameEquals.java new file mode 100644 index 00000000000..34313311f18 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/ChannelNameEquals.java @@ -0,0 +1,114 @@ +/** + * 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 accept(FilterTokenVisitor 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); + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterParser.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterParser.java new file mode 100644 index 00000000000..e030997bbaa --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterParser.java @@ -0,0 +1,299 @@ +/** + * 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 { + + @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 tokens) throws FilterParserException { + State state = new InitialState(); + for (FilterToken token : tokens) { + state = state.handle(token); + } + return state.getResult(); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterParserException.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterParserException.java new file mode 100644 index 00000000000..8efb6f4002a --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterParserException.java @@ -0,0 +1,33 @@ +/** + * 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); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterScanner.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterScanner.java new file mode 100644 index 00000000000..82828074264 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterScanner.java @@ -0,0 +1,239 @@ +/** + * 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 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 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 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; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterScannerException.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterScannerException.java new file mode 100644 index 00000000000..abb64f66f1c --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterScannerException.java @@ -0,0 +1,42 @@ +/** + * 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); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterToken.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterToken.java new file mode 100644 index 00000000000..03f9e03af3b --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterToken.java @@ -0,0 +1,45 @@ +/** + * 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 accept(FilterTokenVisitor visitor) throws FilterParserException; +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterTokenVisitor.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterTokenVisitor.java new file mode 100644 index 00000000000..79c28089d54 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/FilterTokenVisitor.java @@ -0,0 +1,51 @@ +/** + * 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 Return type. + */ +@NonNullByDefault +public interface FilterTokenVisitor { + + /** + * 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; +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OperatorToken.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OperatorToken.java new file mode 100644 index 00000000000..4c4d86762bd --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OperatorToken.java @@ -0,0 +1,31 @@ +/** + * 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); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OrOperator.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OrOperator.java new file mode 100644 index 00000000000..14a18beb63d --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OrOperator.java @@ -0,0 +1,41 @@ +/** + * 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 accept(FilterTokenVisitor visitor) throws FilterParserException { + return visitor.handle(this); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OrPredicate.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OrPredicate.java new file mode 100644 index 00000000000..5224a96d368 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/OrPredicate.java @@ -0,0 +1,55 @@ +/** + * 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; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/TimetableStopByStringEventAttributeFilter.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/TimetableStopByStringEventAttributeFilter.java new file mode 100644 index 00000000000..b3ab27f6fc6 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/TimetableStopByStringEventAttributeFilter.java @@ -0,0 +1,69 @@ +/** + * 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 true, + * 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 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; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/TimetableStopPredicate.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/TimetableStopPredicate.java new file mode 100644 index 00000000000..c90a41197c1 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/filter/TimetableStopPredicate.java @@ -0,0 +1,28 @@ +/** + * 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 { + +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoader.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoader.java index 96d1cf38639..9efa6b3132a 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoader.java +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoader.java @@ -31,7 +31,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; 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; @@ -60,7 +60,7 @@ public final class TimetableLoader { private final Map cachedChanges; private final TimetablesV1Api api; - private final TimetableStopFilter stopFilter; + private final TimetableStopPredicate stopPredicate; private final TimetableStopComparator comparator; private final Supplier currentTimeProvider; private int stopCount; @@ -76,14 +76,15 @@ public final class TimetableLoader { * 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 currentTimeProvider, final String evaNo, final int requestedStopCount) { + public TimetableLoader(final TimetablesV1Api api, final TimetableStopPredicate stopPredicate, + final EventType eventToSort, final Supplier 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; @@ -206,7 +207,7 @@ public final class TimetableLoader { final List 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. diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/thing/thing-types.xml index d85a7c028eb..18eebbe775a 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/thing/thing-types.xml @@ -32,6 +32,11 @@ + + true + + Specifies additional filters for trains, that should be displayed within the timetable. + diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/EventAttributeTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/EventAttributeTest.java index 1f11a0891b5..fa5929f4a20 100644 --- a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/EventAttributeTest.java +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/EventAttributeTest.java @@ -18,6 +18,7 @@ import static org.hamcrest.Matchers.*; 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; @@ -218,24 +219,32 @@ public class EventAttributeTest { 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 diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/FilterParserTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/FilterParserTest.java new file mode 100644 index 00000000000..813cc81cad9 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/FilterParserTest.java @@ -0,0 +1,284 @@ +/** + * 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 tokens = new ArrayList<>(); + private int position = 0; + + private int getPos() { + this.position++; + return this.position; + } + + public List 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 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 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 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) { + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/FilterScannerTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/FilterScannerTest.java new file mode 100644 index 00000000000..68fc3fbf122 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/FilterScannerTest.java @@ -0,0 +1,114 @@ +/** + * 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 processInput(String input, int expectedCount) throws FilterScannerException { + final List tokens = new FilterScanner().processInput(input); + assertThat(tokens, hasSize(expectedCount)); + return tokens; + } + + @Test + public void testSimpleAttributEquals() throws FilterScannerException { + String input = "trip#number=\"20\""; + List 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 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 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); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/TimetableByStringEventAttributeFilterTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/TimetableByStringEventAttributeFilterTest.java new file mode 100644 index 00000000000..3a799be43b6 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/filter/TimetableByStringEventAttributeFilterTest.java @@ -0,0 +1,111 @@ +/** + * 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)); + } +}