[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:
parent
e752b51662
commit
26729956bc
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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}.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue