diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java index c3c8ba420e4..e3fbf90a35d 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java @@ -10,13 +10,13 @@ import graphql.schema.GraphQLOutputType; import java.time.LocalDate; import java.util.List; -import java.util.stream.Stream; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.support.GqlUtil; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequestBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.TripAlteration; -import org.opentripplanner.transit.model.timetable.TripOnServiceDate; /** * A GraphQL query for retrieving data on DatedServiceJourneys @@ -93,72 +93,34 @@ public static GraphQLFieldDefinition createQuery( .type(new GraphQLList(new GraphQLNonNull(Scalars.GraphQLString))) ) .dataFetcher(environment -> { - Stream stream = GqlUtil - .getTransitService(environment) - .getAllTripOnServiceDates() - .stream(); - + // The null safety checks are not needed here - they are taken care of by the request + // object, but reuse let's use the mapping method and leave this improvement until all APIs + // are pushing this check into the domain request. + var authorities = mapIDsToDomainNullSafe(environment.getArgument("authorities")); var lines = mapIDsToDomainNullSafe(environment.getArgument("lines")); var serviceJourneys = mapIDsToDomainNullSafe(environment.getArgument("serviceJourneys")); + var replacementFor = mapIDsToDomainNullSafe(environment.getArgument("replacementFor")); var privateCodes = environment.>getArgument("privateCodes"); var operatingDays = environment.>getArgument("operatingDays"); var alterations = environment.>getArgument("alterations"); - var authorities = mapIDsToDomainNullSafe(environment.getArgument("authorities")); - var replacementFor = mapIDsToDomainNullSafe(environment.getArgument("replacementFor")); - if (!lines.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - lines.contains(tripOnServiceDate.getTrip().getRoute().getId()) - ); - } + TripOnServiceDateRequestBuilder tripOnServiceDateRequestBuilder = TripOnServiceDateRequest + .of() + .withOperatingDays(operatingDays) + .withAuthorities(authorities) + .withLines(lines) + .withServiceJourneys(serviceJourneys) + .withReplacementFor(replacementFor); - if (!serviceJourneys.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - serviceJourneys.contains(tripOnServiceDate.getTrip().getId()) - ); - } + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withPrivateCodes(privateCodes); - if (privateCodes != null && !privateCodes.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - privateCodes.contains(tripOnServiceDate.getTrip().getNetexInternalPlanningCode()) - ); - } + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withAlterations(alterations); - // At least one operationg day is required - var days = operatingDays.stream().toList(); - - stream = - stream.filter(tripOnServiceDate -> days.contains(tripOnServiceDate.getServiceDate())); - - if (alterations != null && !alterations.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - alterations.contains(tripOnServiceDate.getTripAlteration()) - ); - } - - if (!authorities.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - authorities.contains(tripOnServiceDate.getTrip().getRoute().getAgency().getId()) - ); - } - - if (!replacementFor.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - !tripOnServiceDate.getReplacementFor().isEmpty() && - tripOnServiceDate - .getReplacementFor() - .stream() - .anyMatch(replacement -> replacementFor.contains(replacement.getId())) - ); - } - - return stream.toList(); + return GqlUtil + .getTransitService(environment) + .getTripOnServiceDates(tripOnServiceDateRequestBuilder.build()); }) .build(); } diff --git a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java index b34db13e270..0ac83079870 100644 --- a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java +++ b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java @@ -36,6 +36,9 @@ public static String toString(@Nullable Collection c, String nullText) { /** * A null-safe version of isEmpty() for a collection. *

+ * The main strategy handling collections in OTP is to avoid nullable collection fields and use empty + * collections instead. So, before using this method check if the variable/field is indeed `@Nullable`. + *

* If the collection is {@code null} then {@code true} is returned. *

* If the collection is empty then {@code true} is returned. diff --git a/src/main/java/org/opentripplanner/framework/collection/ListUtils.java b/src/main/java/org/opentripplanner/framework/collection/ListUtils.java index 5964a1674e3..35b7e083695 100644 --- a/src/main/java/org/opentripplanner/framework/collection/ListUtils.java +++ b/src/main/java/org/opentripplanner/framework/collection/ListUtils.java @@ -69,4 +69,13 @@ public static List ofNullable(T input) { return List.of(input); } } + + /** + * This method converts the given collection to an instance of a List. If the input is + * {@code null} an empty collection is returned. If not the {@link List#copyOf(Collection)} is + * called. + */ + public static List nullSafeImmutableList(Collection c) { + return (c == null) ? List.of() : List.copyOf(c); + } } diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java new file mode 100644 index 00000000000..6735dc1db29 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java @@ -0,0 +1,77 @@ +package org.opentripplanner.transit.api.request; + +import java.time.LocalDate; +import java.util.List; +import org.opentripplanner.framework.collection.ListUtils; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.TripAlteration; + +/* + * A request for trips on a specific service date. + * + * This request is used to retrieve TripsOnServiceDates that match the provided criteria. + * At least one operatingDay must be provided. + */ +public class TripOnServiceDateRequest { + + private final List operatingDays; + private final List authorities; + private final List lines; + private final List serviceJourneys; + private final List replacementFor; + private final List privateCodes; + private final List alterations; + + protected TripOnServiceDateRequest( + List operatingDays, + List authorities, + List lines, + List serviceJourneys, + List replacementFor, + List privateCodes, + List alterations + ) { + if (operatingDays == null || operatingDays.isEmpty()) { + throw new IllegalArgumentException("operatingDays must have at least one date"); + } + this.operatingDays = ListUtils.nullSafeImmutableList(operatingDays); + this.authorities = ListUtils.nullSafeImmutableList(authorities); + this.lines = ListUtils.nullSafeImmutableList(lines); + this.serviceJourneys = ListUtils.nullSafeImmutableList(serviceJourneys); + this.replacementFor = ListUtils.nullSafeImmutableList(replacementFor); + this.privateCodes = ListUtils.nullSafeImmutableList(privateCodes); + this.alterations = ListUtils.nullSafeImmutableList(alterations); + } + + public static TripOnServiceDateRequestBuilder of() { + return new TripOnServiceDateRequestBuilder(); + } + + public List authorities() { + return authorities; + } + + public List lines() { + return lines; + } + + public List serviceJourneys() { + return serviceJourneys; + } + + public List replacementFor() { + return replacementFor; + } + + public List privateCodes() { + return privateCodes; + } + + public List alterations() { + return alterations; + } + + public List operatingDays() { + return operatingDays; + } +} diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java new file mode 100644 index 00000000000..7aa2644fdc9 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java @@ -0,0 +1,66 @@ +package org.opentripplanner.transit.api.request; + +import java.time.LocalDate; +import java.util.List; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.TripAlteration; + +public class TripOnServiceDateRequestBuilder { + + private List authorities; + private List lines; + private List serviceJourneys; + private List replacementFor; + private List privateCodes; + private List alterations; + private List operatingDays; + + protected TripOnServiceDateRequestBuilder() {} + + public TripOnServiceDateRequestBuilder withOperatingDays(List operatingDays) { + this.operatingDays = operatingDays; + return this; + } + + public TripOnServiceDateRequestBuilder withAuthorities(List authorities) { + this.authorities = authorities; + return this; + } + + public TripOnServiceDateRequestBuilder withLines(List lines) { + this.lines = lines; + return this; + } + + public TripOnServiceDateRequestBuilder withServiceJourneys(List serviceJourneys) { + this.serviceJourneys = serviceJourneys; + return this; + } + + public TripOnServiceDateRequestBuilder withReplacementFor(List replacementFor) { + this.replacementFor = replacementFor; + return this; + } + + public TripOnServiceDateRequestBuilder withPrivateCodes(List privateCodes) { + this.privateCodes = privateCodes; + return this; + } + + public TripOnServiceDateRequestBuilder withAlterations(List alterations) { + this.alterations = alterations; + return this; + } + + public TripOnServiceDateRequest build() { + return new TripOnServiceDateRequest( + operatingDays, + authorities, + lines, + serviceJourneys, + replacementFor, + privateCodes, + alterations + ); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java new file mode 100644 index 00000000000..74f38efa8b7 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java @@ -0,0 +1,43 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.opentripplanner.transit.model.filter.expr.BinaryOperator.AND; + +import java.util.List; + +/** + * Takes a list of matchers and provides a single interface. All matchers in the list must match for + * the composite matcher to return a match. + * + * @param The entity type the AndMatcher matches. + */ +public final class AndMatcher implements Matcher { + + private final Matcher[] matchers; + + private AndMatcher(List> matchers) { + this.matchers = matchers.toArray(Matcher[]::new); + } + + public static Matcher of(List> matchers) { + // simplify a list of one element + if (matchers.size() == 1) { + return matchers.get(0); + } + return new AndMatcher<>(matchers); + } + + @Override + public boolean match(T entity) { + for (var m : matchers) { + if (!m.match(entity)) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return "(" + AND.arrayToString(matchers) + ')'; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/BinaryOperator.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/BinaryOperator.java new file mode 100644 index 00000000000..62f3fa30f27 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/BinaryOperator.java @@ -0,0 +1,37 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Used to concatenate matches with either the logical "AND" or "OR" operator. + */ +enum BinaryOperator { + AND("&"), + OR("|"); + + private final String token; + + BinaryOperator(String token) { + this.token = token; + } + + @Override + public String toString() { + return token; + } + + String arrayToString(T[] values) { + return colToString(Arrays.asList(values)); + } + + String colToString(Collection values) { + return values.stream().map(Objects::toString).collect(Collectors.joining(" " + token + " ")); + } + + String toString(T a, T b) { + return a.toString() + " " + token + " " + b.toString(); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java new file mode 100644 index 00000000000..ed3731897ec --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java @@ -0,0 +1,55 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.function.Function; + +/** + * A matcher that applies a provided matcher to an iterable of child entities returned from the main + * entity that this matcher is for. + *

+ * If any of the iterable entities match the valueMatcher, then the match method returns true. In + * this way it is similar to an OR. + *

+ * @param The main entity type this matcher is applied to. + * @param The type of the child entities, for which there is a mapping from S to T. + */ +public class ContainsMatcher implements Matcher { + + private final String relationshipName; + private final Function> valuesProvider; + private final Matcher valueMatcher; + + /** + * @param relationshipName The name of the type of relationship between the main entity and the + * entity matched by the valueMatcher. + * @param valuesProvider The function that maps the entity being matched by this matcher (S) to + * the iterable of items being matched by valueMatcher. + * @param valueMatcher The matcher that is applied each of the iterable entities returned from the + * valuesProvider function. + */ + public ContainsMatcher( + String relationshipName, + Function> valuesProvider, + Matcher valueMatcher + ) { + this.relationshipName = relationshipName; + this.valuesProvider = valuesProvider; + this.valueMatcher = valueMatcher; + } + + public boolean match(S entity) { + if (valuesProvider.apply(entity) == null) { + return false; + } + for (T it : valuesProvider.apply(entity)) { + if (valueMatcher.match(it)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "ContainsMatcher: " + relationshipName + ": " + valueMatcher.toString(); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java new file mode 100644 index 00000000000..1380131e07a --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java @@ -0,0 +1,40 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.function.Function; + +/** + * A matcher that checks if a value is equal to another value derived from the matched entities. + *

+ * The derived entity value is provided by a function that takes the entity being matched as an argument. + *

+ * @param The type of the entity being matched. + * @param The type of the value that the matcher will test equality for. + */ +public class EqualityMatcher implements Matcher { + + private final String typeName; + private final V value; + private final Function valueProvider; + + /** + * @param typeName The typeName appears in the toString for easier debugging. + * @param value The value that this matcher will check equality for. + * @param valueProvider The function that maps the entity being matched by this matcher (T) to + * the value being matched by this matcher. + */ + public EqualityMatcher(String typeName, V value, Function valueProvider) { + this.typeName = typeName; + this.value = value; + this.valueProvider = valueProvider; + } + + @Override + public boolean match(T entity) { + return value.equals(valueProvider.apply(entity)); + } + + @Override + public String toString() { + return typeName + "==" + value; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/ExpressionBuilder.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/ExpressionBuilder.java new file mode 100644 index 00000000000..b1b4d5be322 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/ExpressionBuilder.java @@ -0,0 +1,37 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +/** + * A builder for creating complex matchers composed of other matchers. + *

+ * This builder contains convenience methods for creating complex matchers from simpler ones. The + * resulting matcher "ands" together all the matchers it has built up. This supports the common + * pattern of narrowing results with multiple filters. + * + * @param The type of entity to match in the expression. + */ +public class ExpressionBuilder { + + private final List> matchers = new ArrayList<>(); + + public static ExpressionBuilder of() { + return new ExpressionBuilder<>(); + } + + public ExpressionBuilder or(Collection values, Function> valueProvider) { + if (values.isEmpty()) { + return this; + } + + matchers.add(OrMatcher.of(values.stream().map(valueProvider).toList())); + return this; + } + + public Matcher build() { + return AndMatcher.of(matchers); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/Matcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/Matcher.java new file mode 100644 index 00000000000..db3c02296b9 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/Matcher.java @@ -0,0 +1,19 @@ +package org.opentripplanner.transit.model.filter.expr; + +/** + * Generic matcher interface - this is the root of the matcher type hierarchy. + *

+ * @param Domain type to match. + */ +@FunctionalInterface +public interface Matcher { + boolean match(T entity); + + static Matcher everything() { + return e -> true; + } + + static Matcher nothing() { + return e -> false; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java new file mode 100644 index 00000000000..62da7af63f4 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java @@ -0,0 +1,58 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.opentripplanner.transit.model.filter.expr.BinaryOperator.OR; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Takes a list of matchers and provides a single interface. At least one of the matchers in the + * list must match for the composite matcher to return a match. + *

+ * @param The entity type the OrMatcher matches. + */ +public final class OrMatcher implements Matcher { + + private final Matcher[] matchers; + + private OrMatcher(List> matchers) { + this.matchers = matchers.toArray(Matcher[]::new); + } + + public static Matcher of(Matcher a, Matcher b) { + return of(List.of(a, b)); + } + + public static Matcher of(List> matchers) { + // Simplify if there is just one matcher in the list + if (matchers.size() == 1) { + return matchers.get(0); + } + // Collapse nested or matchers + var expr = new ArrayList>(); + for (Matcher it : matchers) { + if (it instanceof OrMatcher orMatcher) { + expr.addAll(Arrays.asList(orMatcher.matchers)); + } else { + expr.add(it); + } + } + return new OrMatcher<>(expr); + } + + @Override + public boolean match(T entity) { + for (var m : matchers) { + if (m.match(entity)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "(" + OR.arrayToString(matchers) + ')'; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java new file mode 100644 index 00000000000..f86e7a1ff77 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java @@ -0,0 +1,70 @@ +package org.opentripplanner.transit.model.filter.transit; + +import java.time.LocalDate; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; +import org.opentripplanner.transit.model.filter.expr.ContainsMatcher; +import org.opentripplanner.transit.model.filter.expr.EqualityMatcher; +import org.opentripplanner.transit.model.filter.expr.ExpressionBuilder; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.framework.AbstractTransitEntity; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.TripAlteration; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; + +/** + * A factory for creating matchers for TripOnServiceDate objects. + *

+ * This factory is used to create matchers for TripOnServiceDate objects based on a request. The + * resulting matcher can be used to filter a list of TripOnServiceDate objects. + */ +public class TripOnServiceDateMatcherFactory { + + public static Matcher of(TripOnServiceDateRequest request) { + ExpressionBuilder expr = ExpressionBuilder.of(); + + expr.or(request.operatingDays(), TripOnServiceDateMatcherFactory::operatingDay); + expr.or(request.authorities(), TripOnServiceDateMatcherFactory::authorityId); + expr.or(request.lines(), TripOnServiceDateMatcherFactory::routeId); + expr.or(request.serviceJourneys(), TripOnServiceDateMatcherFactory::serviceJourneyId); + expr.or(request.replacementFor(), TripOnServiceDateMatcherFactory::replacementFor); + expr.or(request.privateCodes(), TripOnServiceDateMatcherFactory::privateCode); + expr.or(request.alterations(), TripOnServiceDateMatcherFactory::alteration); + return expr.build(); + } + + static Matcher authorityId(FeedScopedId id) { + return new EqualityMatcher<>("agency", id, t -> t.getTrip().getRoute().getAgency().getId()); + } + + static Matcher routeId(FeedScopedId id) { + return new EqualityMatcher<>("route", id, t -> t.getTrip().getRoute().getId()); + } + + static Matcher serviceJourneyId(FeedScopedId id) { + return new EqualityMatcher<>("serviceJourney", id, t -> t.getTrip().getId()); + } + + static Matcher replacementFor(FeedScopedId id) { + return new ContainsMatcher<>( + "replacementForContains", + t -> t.getReplacementFor().stream().map(AbstractTransitEntity::getId).toList(), + new EqualityMatcher<>("replacementForIdEquals", id, (idToMatch -> idToMatch)) + ); + } + + static Matcher privateCode(String code) { + return new EqualityMatcher<>( + "privateCode", + code, + t -> t.getTrip().getNetexInternalPlanningCode() + ); + } + + static Matcher operatingDay(LocalDate date) { + return new EqualityMatcher<>("operatingDay", date, TripOnServiceDate::getServiceDate); + } + + static Matcher alteration(TripAlteration alteration) { + return new EqualityMatcher<>("alteration", alteration, TripOnServiceDate::getTripAlteration); + } +} diff --git a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index b3d68b6ffa5..0ce828a5939 100644 --- a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -34,8 +34,11 @@ import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.routing.stoptimes.ArrivalDeparture; import org.opentripplanner.routing.stoptimes.StopTimesHelper; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.filter.transit.TripOnServiceDateMatcherFactory; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -548,6 +551,23 @@ public TripOnServiceDate getTripOnServiceDateForTripAndDay( return transitModelIndex.getTripOnServiceDateForTripAndDay().get(tripIdAndServiceDate); } + /** + * Returns a list of TripOnServiceDates that match the filtering defined in the request. + * + * @param request - A TripOnServiceDateRequest object with filtering defined. + * @return - A list of TripOnServiceDates + */ + @Override + public List getTripOnServiceDates(TripOnServiceDateRequest request) { + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + return transitModelIndex + .getTripOnServiceDateForTripAndDay() + .values() + .stream() + .filter(matcher::match) + .collect(Collectors.toList()); + } + /** * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix * this when doing the issue #3030. diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 94870643f71..c0009f5c462 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -24,6 +24,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.routing.stoptimes.ArrivalDeparture; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; @@ -261,4 +262,12 @@ List stopTimesForPatternAtStop( Set getAllServiceCodes(); Map getServiceCodesRunningForDate(); + + /** + * Returns a list of TripOnServiceDates that match the filtering defined in the request. + * + * @param request - A TripOnServiceDateRequest object with filtering defined. + * @return - A list of TripOnServiceDates + */ + List getTripOnServiceDates(TripOnServiceDateRequest request); } diff --git a/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java b/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java index ae33e493bcc..e2eaab520bc 100644 --- a/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java @@ -1,6 +1,8 @@ package org.opentripplanner.framework.collection; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.type.Month; import java.time.Duration; @@ -15,6 +17,13 @@ class CollectionUtilsTest { public static final String NULL_STRING = ""; + @Test + void testIsEmpty() { + assertTrue(CollectionUtils.isEmpty(null)); + assertTrue(CollectionUtils.isEmpty(List.of())); + assertFalse(CollectionUtils.isEmpty(List.of(1))); + } + @Test void testToString() { assertEquals("", CollectionUtils.toString(null, NULL_STRING)); diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/AndMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/AndMatcherTest.java new file mode 100644 index 00000000000..c79260193ff --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/AndMatcherTest.java @@ -0,0 +1,39 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class AndMatcherTest { + + @Test + void testMatchSingleMatcher() { + var matcher = AndMatcher.of(List.of(new EqualityMatcher<>("int", 42, i -> i))); + assertTrue(matcher.match(42)); + assertFalse(matcher.match(43)); + } + + @Test + void testMatchMultiple() { + var matcher = AndMatcher.of( + List.of(new EqualityMatcher<>("int", 42, i -> i), new EqualityMatcher<>("int", 43, i -> i)) + ); + assertFalse(matcher.match(42)); + assertFalse(matcher.match(43)); + assertFalse(matcher.match(44)); + } + + @Test + void testMatchComposites() { + var matcher = AndMatcher.of( + List.of( + OrMatcher.of(List.of(new EqualityMatcher<>("int", 42, i -> i))), + OrMatcher.of(List.of(new EqualityMatcher<>("int", 43, i -> i))) + ) + ); + assertFalse(matcher.match(42)); + assertFalse(matcher.match(43)); + assertFalse(matcher.match(44)); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java new file mode 100644 index 00000000000..1709fc7bf86 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java @@ -0,0 +1,33 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ContainsMatcherTest { + + private static final Map> integerListMap = Map.of( + 1, + List.of("foo"), + 2, + List.of("bar"), + 3, + List.of("foo", "bar") + ); + + @Test + void testMatch() { + var matcher = new ContainsMatcher<>( + "contains", + integerListMap::get, + new EqualityMatcher<>("string", "foo", s -> s) + ); + + assertTrue(matcher.match(1)); + assertFalse(matcher.match(2)); + assertTrue(matcher.match(3)); + assertFalse(matcher.match(4)); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcherTest.java new file mode 100644 index 00000000000..31d208a768a --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcherTest.java @@ -0,0 +1,22 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class EqualityMatcherTest { + + @Test + void testMatchesPrimitive() { + var matcher = new EqualityMatcher<>("int", 42, i -> i); + assertTrue(matcher.match(42)); + assertFalse(matcher.match(43)); + } + + @Test + void testMatchesObject() { + var matcher = new EqualityMatcher<>("string", "foo", s -> s); + assertTrue(matcher.match("foo")); + assertFalse(matcher.match("bar")); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/OrMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/OrMatcherTest.java new file mode 100644 index 00000000000..415f64e40ed --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/OrMatcherTest.java @@ -0,0 +1,31 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class OrMatcherTest { + + @Test + void testMatch() { + var matcher = OrMatcher.of( + new EqualityMatcher<>("int", 42, i -> i), + new EqualityMatcher<>("int", 43, i -> i) + ); + assertTrue(matcher.match(42)); + assertTrue(matcher.match(43)); + assertFalse(matcher.match(44)); + } + + @Test + void testMatchComposites() { + var matcher = OrMatcher.of( + AndMatcher.of(List.of(new EqualityMatcher<>("int", 42, i -> i))), + AndMatcher.of(List.of(new EqualityMatcher<>("int", 43, i -> i))) + ); + assertTrue(matcher.match(42)); + assertTrue(matcher.match(43)); + assertFalse(matcher.match(44)); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java b/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java new file mode 100644 index 00000000000..b7cb7aa6698 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java @@ -0,0 +1,151 @@ +package org.opentripplanner.transit.model.filter.transit; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; + +class TripOnServiceDateMatcherFactoryTest { + + private TripOnServiceDate tripOnServiceDateRut; + private TripOnServiceDate tripOnServiceDateRut2; + private TripOnServiceDate tripOnServiceDateAkt; + + @BeforeEach + void setup() { + tripOnServiceDateRut = + TripOnServiceDate + .of(new FeedScopedId("RUT:route:trip:date", "123")) + .withTrip( + Trip + .of(new FeedScopedId("RUT:route:trip", "1")) + .withRoute( + Route + .of(new FeedScopedId("RUT:route", "2")) + .withAgency( + Agency + .of(new FeedScopedId("RUT", "3")) + .withName("RUT") + .withTimezone("Europe/Oslo") + .build() + ) + .withMode(TransitMode.BUS) + .withShortName("BUS") + .build() + ) + .build() + ) + .withServiceDate(LocalDate.of(2024, 2, 22)) + .build(); + + tripOnServiceDateRut2 = + TripOnServiceDate + .of(new FeedScopedId("RUT:route:trip:date", "123")) + .withTrip( + Trip + .of(new FeedScopedId("RUT:route:trip2", "1")) + .withRoute( + Route + .of(new FeedScopedId("RUT:route", "2")) + .withAgency( + Agency + .of(new FeedScopedId("RUT", "3")) + .withName("RUT") + .withTimezone("Europe/Oslo") + .build() + ) + .withMode(TransitMode.BUS) + .withShortName("BUS") + .build() + ) + .build() + ) + .withServiceDate(LocalDate.of(2024, 2, 22)) + .build(); + + tripOnServiceDateAkt = + TripOnServiceDate + .of(new FeedScopedId("AKT:route:trip:date", "123")) + .withTrip( + Trip + .of(new FeedScopedId("AKT:route:trip", "1")) + .withRoute( + Route + .of(new FeedScopedId("AKT:route", "2")) + .withAgency( + Agency + .of(new FeedScopedId("AKT", "3")) + .withName("AKT") + .withTimezone("Europe/Oslo") + .build() + ) + .withMode(TransitMode.BUS) + .withShortName("BUS") + .build() + ) + .build() + ) + .withServiceDate(LocalDate.of(2024, 2, 22)) + .build(); + } + + @Test + void testMatchOperatingDays() { + TripOnServiceDateRequest request = TripOnServiceDateRequest + .of() + .withOperatingDays(List.of(LocalDate.of(2024, 2, 22))) + .build(); + + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + + assertTrue(matcher.match(tripOnServiceDateRut)); + assertTrue(matcher.match(tripOnServiceDateRut2)); + assertTrue(matcher.match(tripOnServiceDateAkt)); + } + + @Test + void testMatchMultiple() { + TripOnServiceDateRequest request = TripOnServiceDateRequest + .of() + .withOperatingDays(List.of(LocalDate.of(2024, 2, 22))) + .withAuthorities(List.of(new FeedScopedId("RUT", "3"))) + .withLines(List.of(new FeedScopedId("RUT:route", "2"))) + .withServiceJourneys(List.of(new FeedScopedId("RUT:route:trip", "1"))) + .build(); + + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + + assertTrue(matcher.match(tripOnServiceDateRut)); + assertFalse(matcher.match(tripOnServiceDateRut2)); + assertFalse(matcher.match(tripOnServiceDateAkt)); + } + + @Test + void testMatchMultipleServiceJourneyMatchers() { + TripOnServiceDateRequest request = TripOnServiceDateRequest + .of() + .withOperatingDays(List.of(LocalDate.of(2024, 2, 22))) + .withAuthorities(List.of(new FeedScopedId("RUT", "3"))) + .withLines(List.of(new FeedScopedId("RUT:route", "2"))) + .withServiceJourneys( + List.of(new FeedScopedId("RUT:route:trip", "1"), new FeedScopedId("RUT:route:trip2", "1")) + ) + .build(); + + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + + assertTrue(matcher.match(tripOnServiceDateRut)); + assertTrue(matcher.match(tripOnServiceDateRut2)); + assertFalse(matcher.match(tripOnServiceDateAkt)); + } +}