[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
31 changed files with 2040 additions and 59 deletions

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