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

* Implemented filters within timetable.

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

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

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

* Added documentation for non matching values.

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

* Applied review remarks.

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

Co-authored-by: Sönke Küper <soenkekueper@gmx.de>
This commit is contained in:
Sönke Küper
2021-12-12 19:32:58 +01:00
committed by GitHub
parent e752b51662
commit 26729956bc
31 changed files with 2040 additions and 59 deletions

View File

@@ -18,6 +18,7 @@ import static org.hamcrest.Matchers.*;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.function.Consumer;
@@ -218,24 +219,32 @@ public class EventAttributeTest {
public void testPlannedIntermediateStations() {
String expectedFollowing = "Bielefeld Hbf - Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf";
doTestEventAttribute("planned-intermediate-stations", "planned-following-stations",
(Event e) -> e.setPpth(SAMPLE_PATH), expectedFollowing, new StringType(expectedFollowing),
EventType.DEPARTURE, false);
(Event e) -> e.setPpth(SAMPLE_PATH),
Arrays.asList("Bielefeld Hbf", "Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica",
"Minden(Westf)", "Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf"),
new StringType(expectedFollowing), EventType.DEPARTURE, false);
String expectedPrevious = "Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf - Lehrte";
doTestEventAttribute("planned-intermediate-stations", "planned-previous-stations",
(Event e) -> e.setPpth(SAMPLE_PATH), expectedPrevious, new StringType(expectedPrevious),
EventType.ARRIVAL, false);
(Event e) -> e.setPpth(SAMPLE_PATH),
Arrays.asList("Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica", "Minden(Westf)",
"Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf", "Lehrte"),
new StringType(expectedPrevious), EventType.ARRIVAL, false);
}
@Test
public void testChangedIntermediateStations() {
String expectedFollowing = "Bielefeld Hbf - Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf";
doTestEventAttribute("changed-intermediate-stations", "changed-following-stations",
(Event e) -> e.setCpth(SAMPLE_PATH), expectedFollowing, new StringType(expectedFollowing),
EventType.DEPARTURE, false);
(Event e) -> e.setCpth(SAMPLE_PATH),
Arrays.asList("Bielefeld Hbf", "Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica",
"Minden(Westf)", "Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf"),
new StringType(expectedFollowing), EventType.DEPARTURE, false);
String expectedPrevious = "Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf - Lehrte";
doTestEventAttribute("changed-intermediate-stations", "changed-previous-stations",
(Event e) -> e.setCpth(SAMPLE_PATH), expectedPrevious, new StringType(expectedPrevious),
EventType.ARRIVAL, false);
(Event e) -> e.setCpth(SAMPLE_PATH),
Arrays.asList("Herford", "Löhne(Westf)", "Bad Oeynhausen", "Porta Westfalica", "Minden(Westf)",
"Bückeburg", "Stadthagen", "Haste", "Wunstorf", "Hannover Hbf", "Lehrte"),
new StringType(expectedPrevious), EventType.ARRIVAL, false);
}
@Test

View File

@@ -0,0 +1,284 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deutschebahn.internal.filter;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.deutschebahn.internal.AttributeSelection;
import org.openhab.binding.deutschebahn.internal.EventAttribute;
import org.openhab.binding.deutschebahn.internal.EventAttributeSelection;
import org.openhab.binding.deutschebahn.internal.EventType;
import org.openhab.binding.deutschebahn.internal.TripLabelAttribute;
/**
* Tests for {@link FilterParser}
*
* @author Sönke Küper - Initial contribution.
*/
@NonNullByDefault
public class FilterParserTest {
private static final class FilterTokenSequenceBuilder {
private final List<FilterToken> tokens = new ArrayList<>();
private int position = 0;
private int getPos() {
this.position++;
return this.position;
}
public List<FilterToken> build() {
return this.tokens;
}
public FilterTokenSequenceBuilder and() {
this.tokens.add(new AndOperator(getPos()));
return this;
}
public FilterTokenSequenceBuilder or() {
this.tokens.add(new OrOperator(getPos()));
return this;
}
public FilterTokenSequenceBuilder bracketOpen() {
this.tokens.add(new BracketOpenToken(getPos()));
return this;
}
public FilterTokenSequenceBuilder bracketClose() {
this.tokens.add(new BracketCloseToken(getPos()));
return this;
}
public ChannelNameEquals channelFilter(String channelGroup, String channelName, String pattern) {
ChannelNameEquals channelNameEquals = new ChannelNameEquals(getPos(), channelGroup, channelName,
Pattern.compile(pattern));
this.tokens.add(channelNameEquals);
return channelNameEquals;
}
public FilterTokenSequenceBuilder channelFilter(ChannelNameEquals equals) {
this.tokens.add(equals);
return this;
}
}
private static FilterTokenSequenceBuilder builder() {
return new FilterTokenSequenceBuilder();
}
private static void checkAttributeFilter(TimetableStopPredicate predicate, ChannelNameEquals channelEquals,
EventType eventType, EventAttribute<?, ?> eventAttribute) {
checkAttributeFilter(predicate, channelEquals, new EventAttributeSelection(eventType, eventAttribute));
}
private static void checkAttributeFilter(TimetableStopPredicate predicate, ChannelNameEquals channelEquals,
AttributeSelection attributeSelection) {
assertThat(predicate, is(instanceOf(TimetableStopByStringEventAttributeFilter.class)));
TimetableStopByStringEventAttributeFilter attributeFilter = (TimetableStopByStringEventAttributeFilter) predicate;
assertThat(attributeFilter.getFilter(), is(channelEquals.getFilterValue()));
assertThat(attributeFilter.getAttributeSelection(), is(attributeSelection));
}
private static OrPredicate assertOr(TimetableStopPredicate predicate) {
assertThat(predicate, is(instanceOf(OrPredicate.class)));
return (OrPredicate) predicate;
}
private static AndPredicate assertAnd(TimetableStopPredicate predicate) {
assertThat(predicate, is(instanceOf(AndPredicate.class)));
return (AndPredicate) predicate;
}
@Test
public void testParseSimple() throws FilterParserException {
final List<FilterToken> input = new ArrayList<>();
ChannelNameEquals channelEquals = new ChannelNameEquals(1, "trip", "number", Pattern.compile("20"));
input.add(channelEquals);
final TimetableStopPredicate result = FilterParser.parse(input);
checkAttributeFilter(result, channelEquals, TripLabelAttribute.N);
}
@Test
public void testParseAnd() throws FilterParserException {
final FilterTokenSequenceBuilder b = builder();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.and();
final ChannelNameEquals channelEquals02 = b.channelFilter("trip", "number", "30");
final TimetableStopPredicate result = FilterParser.parse(b.build());
final AndPredicate andPredicate = assertAnd(result);
checkAttributeFilter(andPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
checkAttributeFilter(andPredicate.getSecond(), channelEquals02, TripLabelAttribute.N);
}
@Test
public void testParseOr() throws FilterParserException {
final FilterTokenSequenceBuilder b = builder();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.or();
final ChannelNameEquals channelEquals02 = b.channelFilter("trip", "number", "30");
final TimetableStopPredicate result = FilterParser.parse(b.build());
final OrPredicate orPredicate = assertOr(result);
checkAttributeFilter(orPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
checkAttributeFilter(orPredicate.getSecond(), channelEquals02, TripLabelAttribute.N);
}
@Test
public void testParseWithBrackets() throws FilterParserException {
final FilterTokenSequenceBuilder b = new FilterTokenSequenceBuilder();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.and();
b.bracketOpen();
final ChannelNameEquals channelEquals02 = b.channelFilter("departure", "line", "RE10");
b.or();
final ChannelNameEquals channelEquals03 = b.channelFilter("departure", "line", "RE20");
b.bracketClose();
final List<FilterToken> input = b.build();
final TimetableStopPredicate result = FilterParser.parse(input);
final AndPredicate andPredicate = assertAnd(result);
checkAttributeFilter(andPredicate.getFirst(), channelEquals01, TripLabelAttribute.N);
final OrPredicate orPredicate = assertOr(andPredicate.getSecond());
checkAttributeFilter(orPredicate.getFirst(), channelEquals02, EventType.DEPARTURE, EventAttribute.L);
checkAttributeFilter(orPredicate.getSecond(), channelEquals03, EventType.DEPARTURE, EventAttribute.L);
}
@Test
public void testParseWithMultipleBrackets() throws FilterParserException {
final FilterTokenSequenceBuilder b = builder();
b.bracketOpen();
b.bracketOpen();
final ChannelNameEquals channelEquals01 = b.channelFilter("trip", "number", "20");
b.and();
final ChannelNameEquals channelEquals02 = b.channelFilter("departure", "line", "RE22");
b.bracketClose();
b.or();
b.bracketOpen();
final ChannelNameEquals channelEquals03 = b.channelFilter("trip", "number", "30");
b.and();
final ChannelNameEquals channelEquals04 = b.channelFilter("departure", "line", "RE33");
b.bracketClose();
b.bracketClose();
final List<FilterToken> input = b.build();
final TimetableStopPredicate result = FilterParser.parse(input);
final OrPredicate orPredicate = assertOr(result);
final AndPredicate firstAnd = assertAnd(orPredicate.getFirst());
checkAttributeFilter(firstAnd.getFirst(), channelEquals01, TripLabelAttribute.N);
checkAttributeFilter(firstAnd.getSecond(), channelEquals02, EventType.DEPARTURE, EventAttribute.L);
final AndPredicate secondAnd = assertAnd(orPredicate.getSecond());
checkAttributeFilter(secondAnd.getFirst(), channelEquals03, TripLabelAttribute.N);
checkAttributeFilter(secondAnd.getSecond(), channelEquals04, EventType.DEPARTURE, EventAttribute.L);
}
@Test
public void testParseErrors() {
final ChannelNameEquals channelEquals = new ChannelNameEquals(1, "trip", "number", Pattern.compile("20"));
try {
FilterParser.parse(Collections.emptyList());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().or().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().bracketOpen().and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).and().bracketOpen().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).and().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).or().bracketOpen().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).or().bracketClose().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).and().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(builder().channelFilter(channelEquals).or().build());
fail();
} catch (FilterParserException e) {
}
try {
FilterParser.parse(Arrays.asList(channelEquals, channelEquals));
fail();
} catch (FilterParserException e) {
}
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deutschebahn.internal.filter;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link FilterScanner}
*
* @author Sönke Küper - Initial contribution.
*/
@NonNullByDefault
public class FilterScannerTest {
private static void assertAttributeEquals(FilterToken token, String expectedChannelGroup,
String expectedChannelName, String expectedFilter, int expectedPosition) {
assertThat(token, is(instanceOf(ChannelNameEquals.class)));
ChannelNameEquals actual = (ChannelNameEquals) token;
assertThat(actual.getChannelGroup(), is(expectedChannelGroup));
assertThat(actual.getChannelName(), is(expectedChannelName));
assertThat(actual.getFilterValue().toString(), is(expectedFilter));
assertThat(actual.getPosition(), is(expectedPosition));
}
private static void assertOperator(FilterToken token, OperatorToken expected) {
assertThat(token.getClass(), is(expected.getClass()));
assertThat(token.getPosition(), is(expected.getPosition()));
}
private static List<FilterToken> processInput(String input, int expectedCount) throws FilterScannerException {
final List<FilterToken> tokens = new FilterScanner().processInput(input);
assertThat(tokens, hasSize(expectedCount));
return tokens;
}
@Test
public void testSimpleAttributEquals() throws FilterScannerException {
String input = "trip#number=\"20\"";
List<FilterToken> tokens = processInput(input, 1);
assertAttributeEquals(tokens.get(0), "trip", "number", "20", 1);
}
@Test
public void testAttributeEqualsWithWhitespace() throws FilterScannerException {
String input = "departure#planned-path=\"Hannover Hbf\"";
List<FilterToken> tokens = processInput(input, 1);
assertAttributeEquals(tokens.get(0), "departure", "planned-path", "Hannover Hbf", 1);
}
@Test
public void testInvalidAttributEquals() {
try {
new FilterScanner().processInput("trip#number=20");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("trip#number");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("trip#number=");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("=abc");
fail();
} catch (FilterScannerException e) {
}
try {
new FilterScanner().processInput("train#number=\"abc\"");
fail();
} catch (FilterScannerException e) {
}
}
@Test
public void testComplexExample() throws FilterScannerException {
String input = "trip#category=\"RE\" & (departure#line=\"17\" | departure#line=\"57\") & departure#planned-path=\"Cologne\"";
List<FilterToken> tokens = processInput(input, 9);
assertAttributeEquals(tokens.get(0), "trip", "category", "RE", 1);
assertOperator(tokens.get(1), new AndOperator(20));
assertOperator(tokens.get(2), new BracketOpenToken(22));
assertAttributeEquals(tokens.get(3), "departure", "line", "17", 23);
assertOperator(tokens.get(4), new OrOperator(43));
assertAttributeEquals(tokens.get(5), "departure", "line", "57", 45);
assertOperator(tokens.get(6), new BracketCloseToken(64));
assertOperator(tokens.get(7), new AndOperator(66));
assertAttributeEquals(tokens.get(8), "departure", "planned-path", "Cologne", 68);
}
}

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deutschebahn.internal.filter;
import static org.junit.jupiter.api.Assertions.*;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.deutschebahn.internal.EventAttribute;
import org.openhab.binding.deutschebahn.internal.EventAttributeSelection;
import org.openhab.binding.deutschebahn.internal.EventType;
import org.openhab.binding.deutschebahn.internal.TripLabelAttribute;
import org.openhab.binding.deutschebahn.internal.timetable.dto.Event;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TripLabel;
/**
* Tests for {@link TimetableStopByStringEventAttributeFilter}
*
* @author Sönke Küper - Initial contribution.
*/
@NonNullByDefault
public final class TimetableByStringEventAttributeFilterTest {
@Test
public void testFilterTripLabelAttribute() {
final TimetableStopByStringEventAttributeFilter filter = new TimetableStopByStringEventAttributeFilter(
TripLabelAttribute.C, Pattern.compile("IC.*"));
final TimetableStop stop = new TimetableStop();
// TripLabel is not set -> does not match
assertFalse(filter.test(stop));
final TripLabel label = new TripLabel();
stop.setTl(label);
// Attribute is not set -> does not match
assertFalse(filter.test(stop));
// Set attribute -> matches depending on value
label.setC("RE");
assertFalse(filter.test(stop));
label.setC("ICE");
assertTrue(filter.test(stop));
label.setC("IC");
assertTrue(filter.test(stop));
}
@Test
public void testFilterEventAttribute() {
final EventAttributeSelection eventAttribute = new EventAttributeSelection(EventType.DEPARTURE,
EventAttribute.L);
final TimetableStopByStringEventAttributeFilter filter = new TimetableStopByStringEventAttributeFilter(
eventAttribute, Pattern.compile("RE.*"));
final TimetableStop stop = new TimetableStop();
// Event is not set -> does not match
assertFalse(filter.test(stop));
Event event = new Event();
stop.setDp(event);
// Attribute is not set -> does not match
assertFalse(filter.test(stop));
// Set attribute -> matches depending on value
event.setL("S5");
assertFalse(filter.test(stop));
event.setL("5");
assertFalse(filter.test(stop));
event.setL("RE60");
assertTrue(filter.test(stop));
// Set wrong event
stop.setAr(event);
stop.setDp(null);
assertFalse(filter.test(stop));
}
@Test
public void testFilterEventAttributeList() {
final EventAttributeSelection eventAttribute = new EventAttributeSelection(EventType.DEPARTURE,
EventAttribute.PPTH);
final TimetableStopByStringEventAttributeFilter filter = new TimetableStopByStringEventAttributeFilter(
eventAttribute, Pattern.compile("Hannover.*"));
final TimetableStop stop = new TimetableStop();
Event event = new Event();
stop.setDp(event);
event.setPpth("Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten");
assertTrue(filter.test(stop));
event.setPpth(
"Ahlten|Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten");
assertTrue(filter.test(stop));
event.setPpth(
"Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke");
assertFalse(filter.test(stop));
}
}