[deutschebahn] Implemented filters for trains in timetable (#11745)

* Implemented filters within timetable.

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Added position information for filtertokens, to allow detailled failure information

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Added documentation for non matching values.

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Applied review remarks.

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

Co-authored-by: Sönke Küper <soenkekueper@gmx.de>
This commit is contained in:
Sönke Küper 2021-12-12 19:32:58 +01:00 committed by GitHub
parent e752b51662
commit 26729956bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 2040 additions and 59 deletions

View File

@ -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

View File

@ -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<DTO_TYPE extends JaxbEntity,
private final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState;
private final String channelTypeName;
private final Class<STATE_TYPE> stateType;
private final Function<VALUE_TYPE, List<String>> valueToList;
/**
* Creates an new {@link EventAttribute}.
@ -49,11 +51,13 @@ public abstract class AbstractDtoAttributeSelector<DTO_TYPE extends JaxbEntity,
final Function<DTO_TYPE, @Nullable VALUE_TYPE> getter, //
final BiConsumer<DTO_TYPE, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
final Function<VALUE_TYPE, List<String>> valueToList, //
final Class<STATE_TYPE> stateType) {
this.channelTypeName = channelTypeName;
this.getter = getter;
this.setter = setter;
this.getState = getState;
this.valueToList = valueToList;
this.stateType = stateType;
}
@ -92,6 +96,14 @@ public abstract class AbstractDtoAttributeSelector<DTO_TYPE extends JaxbEntity,
return this.getter.apply(object);
}
/**
* Returns a list of values as string list.
* Returns empty list if value is not present, singleton list if attribute is not single-valued.
*/
public final List<String> getStringValues(DTO_TYPE object) {
return this.valueToList.apply(getValue(object));
}
/**
* Sets the value for the selected attribute in the given DTO object
*/

View File

@ -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<String> getStringValues(TimetableStop t);
}

View File

@ -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 <code>null</code> if not specified.
*/
public @Nullable TimetableStopPredicate getAdditionalFilter() throws FilterScannerException, FilterParserException {
if (additionalFilter.isBlank()) {
return null;
} else {
final FilterScanner scanner = new FilterScanner();
final List<FilterToken> filterTokens = scanner.processInput(additionalFilter);
return FilterParser.parse(filterTokens);
}
}
}

View File

@ -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());

View File

@ -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<VALUE_TYPE, STATE_TYPE extends State>
* Planned Path.
*/
public static final EventAttribute<String, StringType> PPTH = new EventAttribute<>("planned-path", Event::getPpth,
Event::setPpth, StringType::new, StringType.class);
Event::setPpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
/**
* Changed Path.
*/
public static final EventAttribute<String, StringType> CPTH = new EventAttribute<>("changed-path", Event::getCpth,
Event::setCpth, StringType::new, StringType.class);
Event::setCpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
/**
* Planned platform.
*/
public static final EventAttribute<String, StringType> PP = new EventAttribute<>("planned-platform", Event::getPp,
Event::setPp, StringType::new, StringType.class);
Event::setPp, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed platform.
*/
public static final EventAttribute<String, StringType> CP = new EventAttribute<>("changed-platform", Event::getCp,
Event::setCp, StringType::new, StringType.class);
Event::setCp, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned time.
*/
public static final EventAttribute<Date, DateTimeType> PT = new EventAttribute<>("planned-time",
getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType, DateTimeType.class);
getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType,
EventAttribute::mapDateToStringList, DateTimeType.class);
/**
* Changed time.
*/
public static final EventAttribute<Date, DateTimeType> CT = new EventAttribute<>("changed-time",
getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType, DateTimeType.class);
getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType,
EventAttribute::mapDateToStringList, DateTimeType.class);
/**
* Planned status.
*/
public static final EventAttribute<EventStatus, StringType> PS = new EventAttribute<>("planned-status",
Event::getPs, Event::setPs, EventAttribute::fromEventStatus, StringType.class);
Event::getPs, Event::setPs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
StringType.class);
/**
* Changed status.
*/
public static final EventAttribute<EventStatus, StringType> CS = new EventAttribute<>("changed-status",
Event::getCs, Event::setCs, EventAttribute::fromEventStatus, StringType.class);
Event::getCs, Event::setCs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
StringType.class);
/**
* Hidden.
*/
public static final EventAttribute<Integer, OnOffType> HI = new EventAttribute<>("hidden", Event::getHi,
Event::setHi, EventAttribute::parseHidden, OnOffType.class);
Event::setHi, EventAttribute::parseHidden, EventAttribute::mapIntegerToStringList, OnOffType.class);
/**
* Cancellation time.
*/
public static final EventAttribute<Date, DateTimeType> CLT = new EventAttribute<>("cancellation-time",
getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType, DateTimeType.class);
getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType,
EventAttribute::mapDateToStringList, DateTimeType.class);
/**
* Wing.
*/
public static final EventAttribute<String, StringType> WINGS = new EventAttribute<>("wings", Event::getWings,
Event::setWings, StringType::new, StringType.class);
Event::setWings, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
/**
* Transition.
*/
public static final EventAttribute<String, StringType> TRA = new EventAttribute<>("transition", Event::getTra,
Event::setTra, StringType::new, StringType.class);
Event::setTra, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned distant endpoint.
*/
public static final EventAttribute<String, StringType> PDE = new EventAttribute<>("planned-distant-endpoint",
Event::getPde, Event::setPde, StringType::new, StringType.class);
Event::getPde, Event::setPde, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed distant endpoint.
*/
public static final EventAttribute<String, StringType> CDE = new EventAttribute<>("changed-distant-endpoint",
Event::getCde, Event::setCde, StringType::new, StringType.class);
Event::getCde, Event::setCde, StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Distant change.
*/
public static final EventAttribute<Integer, DecimalType> DC = new EventAttribute<>("distant-change", Event::getDc,
Event::setDc, DecimalType::new, DecimalType.class);
Event::setDc, DecimalType::new, EventAttribute::mapIntegerToStringList, DecimalType.class);
/**
* Line.
*/
public static final EventAttribute<String, StringType> L = new EventAttribute<>("line", Event::getL, Event::setL,
StringType::new, StringType.class);
StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Messages.
*/
public static final EventAttribute<List<Message>, StringType> MESSAGES = new EventAttribute<>("messages",
EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages, StringType.class);
EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages,
EventAttribute::mapMessagesToList, StringType.class);
/**
* Planned Start station.
*/
public static final EventAttribute<String, StringType> PLANNED_START_STATION = new EventAttribute<>(
"planned-start-station", EventAttribute.getSingleStationFromPath(Event::getPpth, true),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned Previous stations.
*/
public static final EventAttribute<String, StringType> PLANNED_PREVIOUS_STATIONS = new EventAttribute<>(
public static final EventAttribute<List<String>, StringType> PLANNED_PREVIOUS_STATIONS = new EventAttribute<>(
"planned-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, true),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
StringType.class);
/**
* Planned Target station.
*/
public static final EventAttribute<String, StringType> PLANNED_TARGET_STATION = new EventAttribute<>(
"planned-target-station", EventAttribute.getSingleStationFromPath(Event::getPpth, false),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Planned Following stations.
*/
public static final EventAttribute<String, StringType> PLANNED_FOLLOWING_STATIONS = new EventAttribute<>(
public static final EventAttribute<List<String>, StringType> PLANNED_FOLLOWING_STATIONS = new EventAttribute<>(
"planned-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, false),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
StringType.class);
/**
* Changed Start station.
*/
public static final EventAttribute<String, StringType> CHANGED_START_STATION = new EventAttribute<>(
"changed-start-station", EventAttribute.getSingleStationFromPath(Event::getCpth, true),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed Previous stations.
*/
public static final EventAttribute<String, StringType> CHANGED_PREVIOUS_STATIONS = new EventAttribute<>(
public static final EventAttribute<List<String>, StringType> CHANGED_PREVIOUS_STATIONS = new EventAttribute<>(
"changed-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, true),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
StringType.class);
/**
* Changed Target station.
*/
public static final EventAttribute<String, StringType> CHANGED_TARGET_STATION = new EventAttribute<>(
"changed-target-station", EventAttribute.getSingleStationFromPath(Event::getCpth, false),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
/**
* Changed Following stations.
*/
public static final EventAttribute<String, StringType> CHANGED_FOLLOWING_STATIONS = new EventAttribute<>(
public static final EventAttribute<List<String>, StringType> CHANGED_FOLLOWING_STATIONS = new EventAttribute<>(
"changed-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, false),
EventAttribute.voidSetter(), StringType::new, StringType.class);
EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
StringType.class);
/**
* List containing all known {@link EventAttribute}.
@ -214,14 +225,38 @@ public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
final Function<Event, @Nullable VALUE_TYPE> getter, //
final BiConsumer<Event, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
final Function<VALUE_TYPE, List<String>> valueToList, //
final Class<STATE_TYPE> stateType) {
super(channelTypeName, getter, setter, getState, stateType);
super(channelTypeName, getter, setter, getState, valueToList, stateType);
}
private static StringType fromEventStatus(final EventStatus value) {
return new StringType(value.value());
}
private static List<String> listFromEventStatus(final @Nullable EventStatus value) {
if (value == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(value.value());
}
}
private static StringType fromStringList(final List<String> value) {
return new StringType(value.stream().collect(Collectors.joining(" - ")));
}
private static List<String> nullToEmptyList(@Nullable final List<String> value) {
return value == null ? Collections.emptyList() : value;
}
/**
* Returns a list containing only the given value or empty list if value is <code>null</code>.
*/
private static List<String> singletonList(@Nullable String value) {
return value == null ? Collections.emptyList() : Collections.singletonList(value);
}
private static OnOffType parseHidden(@Nullable Integer value) {
return OnOffType.from(value != null && value == 1);
}
@ -291,6 +326,24 @@ public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
}
}
/**
* Maps the status codes from the messages into string list.
*/
private static List<String> mapMessagesToList(final @Nullable List<Message> messages) {
if (messages == null || messages.isEmpty()) {
return Collections.emptyList();
} else {
return messages //
.stream()//
.filter((Message message) -> message.getC() != null) //
.map(Message::getC) //
.distinct() //
.map(MessageCodes::getMessage) //
.filter((String messageText) -> !messageText.isEmpty()) //
.collect(Collectors.toList());
}
}
private static Function<Event, @Nullable List<Message>> getMessages() {
return new Function<Event, @Nullable List<Message>>() {
@ -305,6 +358,22 @@ public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
};
}
private static List<String> mapIntegerToStringList(@Nullable Integer value) {
if (value == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(String.valueOf(value));
}
}
private static List<String> mapDateToStringList(@Nullable Date value) {
if (value == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(DATETIME_FORMAT.format(value));
}
}
/**
* Returns an single station from an path value (i.e. pipe separated value of stations).
*
@ -337,7 +406,7 @@ public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
* @param removeFirst if <code>true</code> the first value will be removed, <code>false</code> will remove the last
* value.
*/
private static Function<Event, @Nullable String> getIntermediateStationsFromPath(
private static Function<Event, @Nullable List<String>> getIntermediateStationsFromPath(
final Function<Event, @Nullable String> getPath, boolean removeFirst) {
return (final Event event) -> {
final String path = getPath.apply(event);
@ -351,7 +420,7 @@ public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
} 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<VALUE_TYPE, STATE_TYPE extends State>
return path.split("\\|");
}
private static List<String> splitOnPipeToList(final String value) {
return Arrays.asList(value.split("\\|"));
}
/**
* Returns an {@link EventAttribute} for the given channel-type and {@link EventType}.
*/

View File

@ -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<String> getStringValues(TimetableStop stop) {
final Event event = eventType.getEvent(stop);
if (event == null) {
return Collections.emptyList();
} else {
return this.eventAttribute.getStringValues(event);
}
}
@Override
public int hashCode() {
return Objects.hash(eventAttribute, eventType);
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof EventAttributeSelection)) {
return false;
}
final EventAttributeSelection other = (EventAttributeSelection) obj;
return Objects.equals(eventAttribute, other.eventAttribute) && eventType == other.eventType;
}
}

View File

@ -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<TimetableStop> {
public enum TimetableStopFilter implements TimetableStopPredicate {
/**
* Selects all entries.

View File

@ -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<VALUE_TYPE, STATE_TYPE extends State> exte
* Trip category.
*/
public static final TripLabelAttribute<String, StringType> C = new TripLabelAttribute<>("category", TripLabel::getC,
TripLabel::setC, StringType::new, StringType.class);
TripLabel::setC, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Number.
*/
public static final TripLabelAttribute<String, StringType> N = new TripLabelAttribute<>("number", TripLabel::getN,
TripLabel::setN, StringType::new, StringType.class);
TripLabel::setN, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Filter flags.
*/
public static final TripLabelAttribute<String, StringType> F = new TripLabelAttribute<>("filter-flags",
TripLabel::getF, TripLabel::setF, StringType::new, StringType.class);
TripLabel::getF, TripLabel::setF, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Trip Type.
*/
public static final TripLabelAttribute<TripType, StringType> T = new TripLabelAttribute<>("trip-type",
TripLabel::getT, TripLabel::setT, TripLabelAttribute::fromTripType, StringType.class);
TripLabel::getT, TripLabel::setT, TripLabelAttribute::fromTripType, TripLabelAttribute::listFromTripType,
StringType.class);
/**
* Owner.
*/
public static final TripLabelAttribute<String, StringType> O = new TripLabelAttribute<>("owner", TripLabel::getO,
TripLabel::setO, StringType::new, StringType.class);
TripLabel::setO, StringType::new, TripLabelAttribute::singletonList, StringType.class);
/**
* Creates an new {@link TripLabelAttribute}.
@ -79,8 +82,9 @@ public final class TripLabelAttribute<VALUE_TYPE, STATE_TYPE extends State> exte
final Function<TripLabel, @Nullable VALUE_TYPE> getter, //
final BiConsumer<TripLabel, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
final Function<VALUE_TYPE, List<String>> valueToList, //
final Class<STATE_TYPE> stateType) {
super(channelTypeName, getter, setter, getState, stateType);
super(channelTypeName, getter, setter, getState, valueToList, stateType);
}
@Nullable
@ -92,10 +96,41 @@ public final class TripLabelAttribute<VALUE_TYPE, STATE_TYPE extends State> 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<String> getStringValues(TimetableStop stop) {
if (stop.getTl() == null) {
return Collections.emptyList();
}
return this.getStringValues(stop.getTl());
}
private static StringType fromTripType(final TripType value) {
return new StringType(value.value());
}
private static List<String> listFromTripType(@Nullable final TripType value) {
if (value == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(value.value());
}
}
/**
* Returns a list containing only the given value or empty list if value is <code>null</code>.
*/
private static List<String> singletonList(@Nullable String value) {
return value == null ? Collections.emptyList() : Collections.singletonList(value);
}
/**
* Returns an {@link TripLabelAttribute} for the given channel-name.
*/

View File

@ -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> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
return visitor.handle(this);
}
}

View File

@ -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;
}
}

View File

@ -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> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
return visitor.handle(this);
}
}

View File

@ -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> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
return visitor.handle(this);
}
}

View File

@ -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> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
return visitor.handle(this);
}
/**
* Maps this into an {@link TimetableStopByStringEventAttributeFilter}.
*/
public TimetableStopByStringEventAttributeFilter mapToPredicate() throws FilterParserException {
return new TimetableStopByStringEventAttributeFilter(mapAttributeSelection(), filterValue);
}
private AttributeSelection mapAttributeSelection() throws FilterParserException {
switch (this.channelGroup) {
case "trip":
final TripLabelAttribute<?, ?> tripAttribute = TripLabelAttribute.getByChannelName(this.channelName);
if (tripAttribute == null) {
throw new FilterParserException("Invalid trip channel: " + channelName);
}
return tripAttribute;
case "departure":
final EventType eventTypeDeparture = EventType.DEPARTURE;
final EventAttribute<?, ?> departureAttribute = EventAttribute.getByChannelName(this.channelName,
eventTypeDeparture);
if (departureAttribute == null) {
throw new FilterParserException("Invalid departure channel: " + channelName);
}
return new EventAttributeSelection(eventTypeDeparture, departureAttribute);
case "arrival":
final EventType eventTypeArrival = EventType.ARRIVAL;
final EventAttribute<?, ?> arrivalAttribute = EventAttribute.getByChannelName(this.channelName,
eventTypeArrival);
if (arrivalAttribute == null) {
throw new FilterParserException("Invalid arrival channel: " + channelName);
}
return new EventAttributeSelection(eventTypeArrival, arrivalAttribute);
default:
throw new FilterParserException("Unknown channel group: " + channelGroup);
}
}
}

View File

@ -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<State> {
@Nullable
private State previousState;
public State(@Nullable State previousState) {
this.previousState = previousState;
}
private final State handle(FilterToken token) throws FilterParserException {
return token.accept(this);
}
protected abstract State handleChildResult(TimetableStopPredicate predicate) throws FilterParserException;
@Override
public final State handle(ChannelNameEquals channelEquals) throws FilterParserException {
final TimetableStopByStringEventAttributeFilter predicate = channelEquals.mapToPredicate();
return this.handleChildResult(predicate);
}
protected final State publishResultToPrevious(TimetableStopPredicate predicate) throws FilterParserException {
return this.getPreviousState().handleChildResult(predicate);
}
protected State getPreviousState() throws FilterParserException {
final State previousStateValue = this.previousState;
if (previousStateValue == null) {
throw new FilterParserException("Invalid filter");
} else {
return previousStateValue;
}
}
/**
* Returns the result.
*/
public abstract TimetableStopPredicate getResult() throws FilterParserException;
}
/**
* Initial state for the parser.
*/
private static final class InitialState extends State {
@Nullable
private TimetableStopPredicate result;
public InitialState() {
super(null);
}
@Override
public State handle(OrOperator operator) throws FilterParserException {
final TimetableStopPredicate currentResult = this.result;
this.result = null;
if (currentResult == null) {
throw new FilterParserException(
"Invalid filter: first argument missing for '|' at " + operator.getPosition());
}
return new OrState(this, currentResult);
}
@Override
public State handle(AndOperator operator) throws FilterParserException {
final TimetableStopPredicate currentResult = this.result;
this.result = null;
if (currentResult == null) {
throw new FilterParserException(
"Invalid filter: first argument missing for '&' at " + operator.getPosition());
}
return new AndState(this, currentResult);
}
@Override
public State handle(BracketOpenToken token) throws FilterParserException {
this.result = null;
return new SubQueryState(this);
}
@Override
public State handle(BracketCloseToken token) throws FilterParserException {
throw new FilterParserException("Unexpected token " + token.toString() + " at " + token.getPosition());
}
@Override
protected State handleChildResult(TimetableStopPredicate predicate) throws FilterParserException {
if (this.result == null) {
this.result = predicate;
return this;
} else {
throw new FilterParserException("Invalid filter: Operator for multiple filters missing.");
}
}
@Override
public TimetableStopPredicate getResult() throws FilterParserException {
final TimetableStopPredicate currentResult = this.result;
if (currentResult != null) {
return currentResult;
}
throw new FilterParserException("Invalid filter.");
}
}
/**
* State while parsing an conjunction.
*/
private static final class AndState extends State {
private final TimetableStopPredicate first;
public AndState(State previousState, final TimetableStopPredicate first) {
super(previousState);
this.first = first;
}
@Override
public State handle(OrOperator operator) throws FilterParserException {
throw new FilterParserException("Invalid second argument for '&' operator " + operator.toString() + " at "
+ operator.getPosition());
}
@Override
public State handle(AndOperator operator) throws FilterParserException {
throw new FilterParserException("Invalid second argument for '&' operator " + operator.toString() + " at "
+ operator.getPosition());
}
@Override
public State handle(BracketOpenToken token) throws FilterParserException {
return new SubQueryState(this);
}
@Override
public State handle(BracketCloseToken token) throws FilterParserException {
throw new FilterParserException(
"Invalid second argument for '&' operator " + token.toString() + " at " + token.getPosition());
}
@Override
protected State handleChildResult(TimetableStopPredicate predicate) throws FilterParserException {
return this.publishResultToPrevious(new AndPredicate(first, predicate));
}
@Override
public TimetableStopPredicate getResult() throws FilterParserException {
throw new FilterParserException("Invalid filter");
}
}
/**
* State while parsing an disjunction.
*/
private static final class OrState extends State {
private final TimetableStopPredicate first;
public OrState(State previousState, final TimetableStopPredicate first) {
super(previousState);
this.first = first;
}
@Override
public State handle(OrOperator operator) throws FilterParserException {
throw new FilterParserException("Invalid second argument for '|' operator " + operator.toString() + " at "
+ operator.getPosition());
}
@Override
public State handle(AndOperator operator) throws FilterParserException {
throw new FilterParserException("Invalid second argument for '|' operator " + operator.toString() + " at "
+ operator.getPosition());
}
@Override
public State handle(BracketOpenToken token) throws FilterParserException {
return new SubQueryState(this);
}
@Override
public State handle(BracketCloseToken token) throws FilterParserException {
throw new FilterParserException(
"Invalid second argument for '|' operator " + token.toString() + " at " + token.getPosition());
}
@Override
protected State handleChildResult(TimetableStopPredicate second) throws FilterParserException {
return this.publishResultToPrevious(new OrPredicate(first, second));
}
@Override
public TimetableStopPredicate getResult() throws FilterParserException {
throw new FilterParserException("Invalid filter");
}
}
/**
* State while parsing an Subquery.
*/
private static final class SubQueryState extends State {
@Nullable
private TimetableStopPredicate currentResult;
public SubQueryState(State previousState) {
super(previousState);
}
@Override
public State handle(OrOperator operator) throws FilterParserException {
TimetableStopPredicate result = this.currentResult;
if (result == null) {
throw new FilterParserException(
"Operator '|' at " + operator.getPosition() + " must not be first element in subquery.");
}
return new OrState(this, result);
}
@Override
public State handle(AndOperator operator) throws FilterParserException {
TimetableStopPredicate result = this.currentResult;
if (result == null) {
throw new FilterParserException(
"Operator '&' at" + operator.getPosition() + " must not be first element in subquery.");
}
return new AndState(this, result);
}
@Override
public State handle(BracketOpenToken token) throws FilterParserException {
return new SubQueryState(this);
}
@Override
public State handle(BracketCloseToken token) throws FilterParserException {
TimetableStopPredicate result = this.currentResult;
if (result == null) {
throw new FilterParserException("Subquery must not be empty at " + token.getPosition());
}
return publishResultToPrevious(result);
}
@Override
protected State handleChildResult(TimetableStopPredicate predicate) {
this.currentResult = predicate;
return this;
}
@Override
public TimetableStopPredicate getResult() throws FilterParserException {
throw new FilterParserException("Invalid filter");
}
}
private FilterParser() {
}
/**
* Parses the given {@link FilterToken} into an {@link TimetableStopPredicate}.
*/
public static TimetableStopPredicate parse(final List<FilterToken> tokens) throws FilterParserException {
State state = new InitialState();
for (FilterToken token : tokens) {
state = state.handle(token);
}
return state.getResult();
}
}

View File

@ -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);
}
}

View File

@ -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<Character> OP_CHARS = new HashSet<>(Arrays.asList('&', '|', '!', '(', ')'));
private static final Pattern CHANNEL_NAME = Pattern.compile("(trip|arrival|departure)#(\\S+)");
/**
* State of the scanner.
*/
private interface State {
/**
* Handles the next read character.
*
* @return Returns the next scanner state.
*/
public abstract State handle(int position, char currentChar) throws FilterScannerException;
/**
* Called when no more input is available.
*/
public abstract void finish(int position) throws FilterScannerException;
}
/**
* Initial state of the scanner.
*/
private final class InitialState implements State {
@Override
public State handle(int position, char currentChar) throws FilterScannerException {
// Skip white spaces
if (Character.isWhitespace(currentChar)) {
return this;
}
switch (currentChar) {
// Handle all operator tokens
case '&':
result.add(new AndOperator(position));
return this;
case '|':
result.add(new OrOperator(position));
return this;
case '(':
result.add(new BracketOpenToken(position));
return this;
case ')':
result.add(new BracketCloseToken(position));
return this;
default:
final ChannelNameState channelNameState = new ChannelNameState();
return channelNameState.handle(position, currentChar);
}
}
@Override
public void finish(int position) {
}
}
/**
* State scanning an channel name until the equals-sign.
*/
private final class ChannelNameState implements State {
private final StringBuilder channelName = new StringBuilder();
private int startPosition = -1;
@Override
public State handle(int position, final char currentChar) throws FilterScannerException {
// Skip white spaces at front
if (Character.isWhitespace(currentChar) && channelName.toString().isEmpty()) {
return this;
}
if (Character.isWhitespace(currentChar)) {
throw new FilterScannerException(position, "Channel name must not contain whitespace.");
}
if (currentChar == '=') {
final String channelNameValue = this.channelName.toString();
if (channelNameValue.isEmpty()) {
throw new FilterScannerException(position, "Channel name must not be empty.");
}
final Matcher matcher = CHANNEL_NAME.matcher(channelNameValue);
if (!matcher.matches()) {
throw new FilterScannerException(position, "Invalid channel name: " + channelNameValue);
}
return new ExpectQuotesState(startPosition, matcher.group(1), matcher.group(2));
}
if (OP_CHARS.contains(currentChar)) {
throw new FilterScannerException(position, "Channel name must not contain operation char.");
}
this.channelName.append(currentChar);
if (startPosition == -1) {
startPosition = position;
}
return this;
}
@Override
public void finish(int position) throws FilterScannerException {
throw new FilterScannerException(position, "Filter value is missing.");
}
}
/**
* State after channel name, wiating for quotes.
*/
private final class ExpectQuotesState implements State {
private final int startPosition;
private final String channelName;
private String channelGroup;
/**
* Creates an new {@link ExpectQuotesState}.
*/
public ExpectQuotesState(int startPosition, final String channelGroup, String channelName) {
this.startPosition = startPosition;
this.channelGroup = channelGroup;
this.channelName = channelName;
}
@Override
public State handle(int position, char currentChar) throws FilterScannerException {
if (currentChar != '"') {
throw new FilterScannerException(position, "Filter value must start with quotes");
}
return new FilterValueState(startPosition, channelGroup, channelName);
}
@Override
public void finish(int position) throws FilterScannerException {
throw new FilterScannerException(position, "Filter value is missing.");
}
}
/**
* State scanning the filter value until next quotes.
*/
private final class FilterValueState implements State {
private final int startPosition;
private final String channelGroup;
private final String channelName;
private final StringBuilder filterValue;
/**
* Creates an new {@link FilterValueState}.
*/
public FilterValueState(int startPosition, String channelGroup, String channelName) {
this.startPosition = startPosition;
this.channelGroup = channelGroup;
this.channelName = channelName;
this.filterValue = new StringBuilder();
}
@Override
public State handle(int position, char currentChar) throws FilterScannerException {
if (currentChar == '"') {
finish(position);
return new InitialState();
}
filterValue.append(currentChar);
return this;
}
@Override
public void finish(int position) throws FilterScannerException {
String filterPattern = this.filterValue.toString();
try {
result.add(new ChannelNameEquals(startPosition, this.channelGroup, this.channelName,
Pattern.compile(filterPattern)));
} catch (PatternSyntaxException e) {
throw new FilterScannerException(position, "Filter pattern is invalid: " + filterPattern, e);
}
}
}
private List<FilterToken> result;
/**
* Creates an new {@link FilterScanner}.
*/
public FilterScanner() {
this.result = new ArrayList<>();
}
/**
* Scans the given filter expression and returns the result sequence of {@link FilterToken}.
*/
public List<FilterToken> processInput(String value) throws FilterScannerException {
State state = new InitialState();
for (int pos = 0; pos < value.length(); pos++) {
char currentChar = value.charAt(pos);
state = state.handle(pos + 1, currentChar);
}
state.finish(value.length());
return this.result;
}
}

View File

@ -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);
}
}

View File

@ -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> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException;
}

View File

@ -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 <R> Return type.
*/
@NonNullByDefault
public interface FilterTokenVisitor<R> {
/**
* Handles {@link ChannelNameEquals}.
*/
public abstract R handle(ChannelNameEquals equals) throws FilterParserException;
/**
* Handles {@link OrOperator}.
*/
public abstract R handle(OrOperator operator) throws FilterParserException;
/**
* Handles {@link AndOperator}.
*/
public abstract R handle(AndOperator operator) throws FilterParserException;
/**
* Handles {@link BracketOpenToken}.
*/
public abstract R handle(BracketOpenToken token) throws FilterParserException;
/**
* Handles {@link BracketCloseToken}.
*/
public abstract R handle(BracketCloseToken token) throws FilterParserException;
}

View File

@ -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);
}
}

View File

@ -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> R accept(FilterTokenVisitor<R> visitor) throws FilterParserException {
return visitor.handle(this);
}
}

View File

@ -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;
}
}

View File

@ -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 <code>true</code>,
* if at least one value matches the given filter.
*
* @author Sönke Küper - initial contribution
*/
@NonNullByDefault
public final class TimetableStopByStringEventAttributeFilter implements TimetableStopPredicate {
private final AttributeSelection attributeSelection;
private final Pattern filter;
/**
* Creates an new {@link TimetableStopByStringEventAttributeFilter}.
*/
TimetableStopByStringEventAttributeFilter(final AttributeSelection attributeSelection, final Pattern filter) {
this.attributeSelection = attributeSelection;
this.filter = filter;
}
@Override
public boolean test(TimetableStop t) {
final List<String> values = attributeSelection.getStringValues(t);
for (String actualValue : values) {
if (filter.matcher(actualValue).matches()) {
return true;
}
}
return false;
}
/**
* Returns the {@link AttributeSelection}.
*/
final AttributeSelection getAttributeSelection() {
return attributeSelection;
}
/**
* Returns the filter pattern.
*/
final Pattern getFilter() {
return filter;
}
}

View File

@ -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<TimetableStop> {
}

View File

@ -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<String, TimetableStop> cachedChanges;
private final TimetablesV1Api api;
private final TimetableStopFilter stopFilter;
private final TimetableStopPredicate stopPredicate;
private final TimetableStopComparator comparator;
private final Supplier<Date> currentTimeProvider;
private int stopCount;
@ -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<Date> currentTimeProvider, final String evaNo, final int requestedStopCount) {
public TimetableLoader(final TimetablesV1Api api, final TimetableStopPredicate stopPredicate,
final EventType eventToSort, final Supplier<Date> currentTimeProvider, final String evaNo,
final int requestedStopCount) {
this.api = api;
this.stopFilter = stopFilter;
this.stopPredicate = stopPredicate;
this.currentTimeProvider = currentTimeProvider;
this.evaNo = evaNo;
this.stopCount = requestedStopCount;
@ -206,7 +207,7 @@ public final class TimetableLoader {
final List<TimetableStop> stops = timetable //
.getS() //
.stream() //
.filter(this.stopFilter) //
.filter(this.stopPredicate) //
.collect(Collectors.toList());
// Merge the loaded stops with the cached changes and put them into the plan cache.

View File

@ -32,6 +32,11 @@
<option value="departures">Departures</option>
</options>
</parameter>
<parameter name="additionalFilter" type="text" required="false">
<advanced>true</advanced>
<label>Additional Filter</label>
<description>Specifies additional filters for trains, that should be displayed within the timetable.</description>
</parameter>
</config-description>
</bridge-type>

View File

@ -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

View File

@ -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<FilterToken> tokens = new ArrayList<>();
private int position = 0;
private int getPos() {
this.position++;
return this.position;
}
public List<FilterToken> build() {
return this.tokens;
}
public FilterTokenSequenceBuilder and() {
this.tokens.add(new AndOperator(getPos()));
return this;
}
public FilterTokenSequenceBuilder or() {
this.tokens.add(new OrOperator(getPos()));
return this;
}
public FilterTokenSequenceBuilder bracketOpen() {
this.tokens.add(new BracketOpenToken(getPos()));
return this;
}
public FilterTokenSequenceBuilder bracketClose() {
this.tokens.add(new BracketCloseToken(getPos()));
return this;
}
public ChannelNameEquals channelFilter(String channelGroup, String channelName, String pattern) {
ChannelNameEquals channelNameEquals = new ChannelNameEquals(getPos(), channelGroup, channelName,
Pattern.compile(pattern));
this.tokens.add(channelNameEquals);
return channelNameEquals;
}
public FilterTokenSequenceBuilder channelFilter(ChannelNameEquals equals) {
this.tokens.add(equals);
return this;
}
}
private static FilterTokenSequenceBuilder builder() {
return new FilterTokenSequenceBuilder();
}
private static void checkAttributeFilter(TimetableStopPredicate predicate, ChannelNameEquals channelEquals,
EventType eventType, EventAttribute<?, ?> eventAttribute) {
checkAttributeFilter(predicate, channelEquals, new EventAttributeSelection(eventType, eventAttribute));
}
private static void checkAttributeFilter(TimetableStopPredicate predicate, ChannelNameEquals channelEquals,
AttributeSelection attributeSelection) {
assertThat(predicate, is(instanceOf(TimetableStopByStringEventAttributeFilter.class)));
TimetableStopByStringEventAttributeFilter attributeFilter = (TimetableStopByStringEventAttributeFilter) predicate;
assertThat(attributeFilter.getFilter(), is(channelEquals.getFilterValue()));
assertThat(attributeFilter.getAttributeSelection(), is(attributeSelection));
}
private static OrPredicate assertOr(TimetableStopPredicate predicate) {
assertThat(predicate, is(instanceOf(OrPredicate.class)));
return (OrPredicate) predicate;
}
private static AndPredicate assertAnd(TimetableStopPredicate predicate) {
assertThat(predicate, is(instanceOf(AndPredicate.class)));
return (AndPredicate) predicate;
}
@Test
public void testParseSimple() throws FilterParserException {
final List<FilterToken> input = new ArrayList<>();
ChannelNameEquals channelEquals = new ChannelNameEquals(1, "trip", "number", Pattern.compile("20"));
input.add(channelEquals);
final TimetableStopPredicate result = FilterParser.parse(input);
checkAttributeFilter(result, channelEquals, TripLabelAttribute.N);
}
@Test
public void testParseAnd() throws FilterParserException {
final FilterTokenSequenceBuilder b = builder();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.and();
final ChannelNameEquals channelEquals02 = b.channelFilter("trip", "number", "30");
final TimetableStopPredicate result = FilterParser.parse(b.build());
final AndPredicate andPredicate = assertAnd(result);
checkAttributeFilter(andPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
checkAttributeFilter(andPredicate.getSecond(), channelEquals02, TripLabelAttribute.N);
}
@Test
public void testParseOr() throws FilterParserException {
final FilterTokenSequenceBuilder b = builder();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.or();
final ChannelNameEquals channelEquals02 = b.channelFilter("trip", "number", "30");
final TimetableStopPredicate result = FilterParser.parse(b.build());
final OrPredicate orPredicate = assertOr(result);
checkAttributeFilter(orPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
checkAttributeFilter(orPredicate.getSecond(), channelEquals02, TripLabelAttribute.N);
}
@Test
public void testParseWithBrackets() throws FilterParserException {
final FilterTokenSequenceBuilder b = new FilterTokenSequenceBuilder();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.and();
b.bracketOpen();
final ChannelNameEquals channelEquals02 = b.channelFilter("departure", "line", "RE10");
b.or();
final ChannelNameEquals channelEquals03 = b.channelFilter("departure", "line", "RE20");
b.bracketClose();
final List<FilterToken> input = b.build();
final TimetableStopPredicate result = FilterParser.parse(input);
final AndPredicate andPredicate = assertAnd(result);
checkAttributeFilter(andPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
final OrPredicate orPredicate = assertOr(andPredicate.getSecond());
checkAttributeFilter(orPredicate.getFirst(), channelEquals02, EventType.DEPARTURE, EventAttribute.L);
checkAttributeFilter(orPredicate.getSecond(), channelEquals03, EventType.DEPARTURE, EventAttribute.L);
}
@Test
public void testParseWithMultipleBrackets() throws FilterParserException {
final FilterTokenSequenceBuilder b = builder();
b.bracketOpen();
b.bracketOpen();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.and();
final ChannelNameEquals channelEquals02 = b.channelFilter("departure", "line", "RE22");
b.bracketClose();
b.or();
b.bracketOpen();
final ChannelNameEquals channelEquals03 = b.channelFilter("trip", "number", "30");
b.and();
final ChannelNameEquals channelEquals04 = b.channelFilter("departure", "line", "RE33");
b.bracketClose();
b.bracketClose();
final List<FilterToken> input = b.build();
final TimetableStopPredicate result = FilterParser.parse(input);
final OrPredicate orPredicate = assertOr(result);
final AndPredicate firstAnd = assertAnd(orPredicate.getFirst());
checkAttributeFilter(firstAnd.getFirst(), channelEquals01, TripLabelAttribute.N);
checkAttributeFilter(firstAnd.getSecond(), channelEquals02, EventType.DEPARTURE, EventAttribute.L);
final AndPredicate secondAnd = assertAnd(orPredicate.getSecond());
checkAttributeFilter(secondAnd.getFirst(), channelEquals03, TripLabelAttribute.N);
checkAttributeFilter(secondAnd.getSecond(), channelEquals04, EventType.DEPARTURE, EventAttribute.L);
}
@Test
public void testParseErrors() {
final ChannelNameEquals channelEquals = new ChannelNameEquals(1, "trip", "number", Pattern.compile("20"));
try {
FilterParser.parse(Collections.emptyList());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().or().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).and().bracketOpen().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).and().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).or().bracketOpen().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).or().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).or().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(Arrays.asList(channelEquals, channelEquals));
fail();
} catch (FilterParserException e) {
}
}
}

View File

@ -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<FilterToken> processInput(String input, int expectedCount) throws FilterScannerException {
final List<FilterToken> tokens = new FilterScanner().processInput(input);
assertThat(tokens, hasSize(expectedCount));
return tokens;
}
@Test
public void testSimpleAttributEquals() throws FilterScannerException {
String input = "trip#number=\"20\"";
List<FilterToken> tokens = processInput(input, 1);
assertAttributeEquals(tokens.get(0), "trip", "number", "20", 1);
}
@Test
public void testAttributeEqualsWithWhitespace() throws FilterScannerException {
String input = "departure#planned-path=\"Hannover Hbf\"";
List<FilterToken> tokens = processInput(input, 1);
assertAttributeEquals(tokens.get(0), "departure", "planned-path", "Hannover Hbf", 1);
}
@Test
public void testInvalidAttributEquals() {
try {
new FilterScanner().processInput("trip#number=20");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("trip#number");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("trip#number=");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("=abc");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("train#number=\"abc\"");
fail();
} catch (FilterScannerException e) {
}
}
@Test
public void testComplexExample() throws FilterScannerException {
String input = "trip#category=\"RE\" & (departure#line=\"17\" | departure#line=\"57\") & departure#planned-path=\"Cologne\"";
List<FilterToken> tokens = processInput(input, 9);
assertAttributeEquals(tokens.get(0), "trip", "category", "RE", 1);
assertOperator(tokens.get(1), new AndOperator(20));
assertOperator(tokens.get(2), new BracketOpenToken(22));
assertAttributeEquals(tokens.get(3), "departure", "line", "17", 23);
assertOperator(tokens.get(4), new OrOperator(43));
assertAttributeEquals(tokens.get(5), "departure", "line", "57", 45);
assertOperator(tokens.get(6), new BracketCloseToken(64));
assertOperator(tokens.get(7), new AndOperator(66));
assertAttributeEquals(tokens.get(8), "departure", "planned-path", "Cologne", 68);
}
}

View File

@ -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));
}
}