From 98d53d866c6c6ba183e9f78a759e150c4ef395f1 Mon Sep 17 00:00:00 2001 From: eibakke Date: Thu, 29 Feb 2024 16:01:28 +0100 Subject: [PATCH 1/6] Adds a matcher API for the transit service and makes use of it in the DatedServiceJourneyQuery. This is the first simple implementation of a filter using the unified matcher API. --- .../timetable/DatedServiceJourneyQuery.java | 72 +++------ .../api/request/TripOnServiceDateRequest.java | 73 +++++++++ .../TripOnServiceDateRequestBuilder.java | 66 ++++++++ .../transit/model/filter/expr/AndMatcher.java | 41 +++++ .../model/filter/expr/BinaryOperator.java | 37 +++++ .../model/filter/expr/ContainsMatcher.java | 36 +++++ .../model/filter/expr/EqualityMatcher.java | 31 ++++ .../transit/model/filter/expr/Matcher.java | 19 +++ .../transit/model/filter/expr/OrMatcher.java | 56 +++++++ .../TripOnServiceDateMatcherFactory.java | 150 ++++++++++++++++++ .../service/DefaultTransitService.java | 20 +++ .../transit/service/TransitService.java | 9 ++ .../model/filter/expr/AndMatcherTest.java | 39 +++++ .../filter/expr/ContainsMatcherTest.java | 29 ++++ .../filter/expr/EqualityMatcherTest.java | 22 +++ .../model/filter/expr/OrMatcherTest.java | 31 ++++ .../TripOnServiceDateMatcherFactoryTest.java | 148 +++++++++++++++++ 17 files changed, 826 insertions(+), 53 deletions(-) create mode 100644 src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java create mode 100644 src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/expr/BinaryOperator.java create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/expr/Matcher.java create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java create mode 100644 src/test/java/org/opentripplanner/transit/model/filter/expr/AndMatcherTest.java create mode 100644 src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java create mode 100644 src/test/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcherTest.java create mode 100644 src/test/java/org/opentripplanner/transit/model/filter/expr/OrMatcherTest.java create mode 100644 src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java 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..69209d4610d 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,38 @@ public static GraphQLFieldDefinition createQuery( .type(new GraphQLList(new GraphQLNonNull(Scalars.GraphQLString))) ) .dataFetcher(environment -> { - Stream stream = GqlUtil - .getTransitService(environment) - .getAllTripOnServiceDates() - .stream(); - + 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()) - ); + if (operatingDays == null || operatingDays.isEmpty()) { + throw new IllegalArgumentException("At least one operatingDay must be provided."); } - if (!serviceJourneys.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - serviceJourneys.contains(tripOnServiceDate.getTrip().getId()) - ); - } + TripOnServiceDateRequestBuilder tripOnServiceDateRequestBuilder = TripOnServiceDateRequest + .of(operatingDays) + .withAuthorities(authorities) + .withLines(lines) + .withServiceJourneys(serviceJourneys) + .withReplacementFor(replacementFor); if (privateCodes != null && !privateCodes.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - privateCodes.contains(tripOnServiceDate.getTrip().getNetexInternalPlanningCode()) - ); + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withPrivateCodes(privateCodes); } - // 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()) - ); + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withAlterations(alterations); } - 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/transit/api/request/TripOnServiceDateRequest.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java new file mode 100644 index 00000000000..6c9cc9e8718 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java @@ -0,0 +1,73 @@ +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; + +/* + * 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 authorities; + private final List lines; + private final List serviceJourneys; + private final List replacementFor; + private final List privateCodes; + private final List alterations; + private final List operatingDays; + + protected TripOnServiceDateRequest( + List authorities, + List lines, + List serviceJourneys, + List replacementFor, + List privateCodes, + List operatingDays, + List alterations + ) { + this.authorities = authorities; + this.lines = lines; + this.serviceJourneys = serviceJourneys; + this.replacementFor = replacementFor; + this.privateCodes = privateCodes; + this.alterations = alterations; + this.operatingDays = operatingDays; + } + + public static TripOnServiceDateRequestBuilder of(List operatingDays) { + return new TripOnServiceDateRequestBuilder(operatingDays); + } + + public List getAuthorities() { + return authorities; + } + + public List getLines() { + return lines; + } + + public List getServiceJourneys() { + return serviceJourneys; + } + + public List getReplacementFor() { + return replacementFor; + } + + public List getPrivateCodes() { + return privateCodes; + } + + public List getAlterations() { + return alterations; + } + + public List getOperatingDays() { + 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..4aa5dd076d8 --- /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 = List.of(); + private List lines = List.of(); + private List serviceJourneys = List.of(); + private List replacementFor = List.of(); + private List privateCodes = List.of(); + private List alterations = List.of(); + private final List operatingDays; + + protected TripOnServiceDateRequestBuilder(List operatingDays) { + if (operatingDays == null || operatingDays.isEmpty()) { + throw new IllegalArgumentException("operatingDays must have at least one date"); + } + this.operatingDays = operatingDays; + } + + 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( + authorities, + lines, + serviceJourneys, + replacementFor, + privateCodes, + operatingDays, + 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..8936993bb59 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java @@ -0,0 +1,41 @@ +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. + */ +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..79860e943b0 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java @@ -0,0 +1,36 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.Collection; +import java.util.function.Function; + +/** + * A matcher that checks if a collection contains a value. + *

+ * The collection is provided by a function that takes the entity being matched as an argument. + */ +public class ContainsMatcher implements Matcher { + + private final String typeName; + private final V value; + private final Function> valueProvider; + + public ContainsMatcher(String typeName, V value, Function> valueProvider) { + this.typeName = typeName; + this.value = value; + this.valueProvider = valueProvider; + } + + @Override + public boolean match(T entity) { + Collection values = valueProvider.apply(entity); + if (values == null) { + return false; + } + return values.contains(value); + } + + @Override + public String toString() { + return typeName + " contains " + value; + } +} 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..29a6a5bf758 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java @@ -0,0 +1,31 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.function.Function; + +/** + * A matcher that checks if a value is equal to another value. + *

+ * The value is provided by a function that takes the entity being matched as an argument. + */ +public class EqualityMatcher implements Matcher { + + private final String typeName; + private final V value; + private final Function valueProvider; + + 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/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..25a58b94e76 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java @@ -0,0 +1,56 @@ +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. + */ +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..bf87d9c0ff4 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java @@ -0,0 +1,150 @@ +package org.opentripplanner.transit.model.filter.transit; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; +import org.opentripplanner.transit.model.filter.expr.AndMatcher; +import org.opentripplanner.transit.model.filter.expr.ContainsMatcher; +import org.opentripplanner.transit.model.filter.expr.EqualityMatcher; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.filter.expr.OrMatcher; +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) { + List> matchers = new ArrayList<>(); + + matchers.add( + or( + request + .getOperatingDays() + .stream() + .map(TripOnServiceDateMatcherFactory::operatingDay) + .toList() + ) + ); + + if (!request.getAuthorities().isEmpty()) { + matchers.add( + or( + request + .getAuthorities() + .stream() + .map(TripOnServiceDateMatcherFactory::authorityId) + .toList() + ) + ); + } + + if (!request.getLines().isEmpty()) { + matchers.add( + or(request.getLines().stream().map(TripOnServiceDateMatcherFactory::routeId).toList()) + ); + } + + if (!request.getServiceJourneys().isEmpty()) { + matchers.add( + or( + request + .getServiceJourneys() + .stream() + .map(TripOnServiceDateMatcherFactory::serviceJourneyId) + .toList() + ) + ); + } + + if (!request.getReplacementFor().isEmpty()) { + matchers.add( + or( + request + .getReplacementFor() + .stream() + .map(TripOnServiceDateMatcherFactory::replacementFor) + .toList() + ) + ); + } + + if (!request.getPrivateCodes().isEmpty()) { + matchers.add( + or( + request + .getPrivateCodes() + .stream() + .map(TripOnServiceDateMatcherFactory::privateCode) + .toList() + ) + ); + } + + if (!request.getAlterations().isEmpty()) { + matchers.add( + or( + request + .getAlterations() + .stream() + .map(TripOnServiceDateMatcherFactory::alteration) + .toList() + ) + ); + } + + return and(matchers); + } + + 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<>( + "replacementFor", + id, + t -> t.getReplacementFor().stream().map(AbstractTransitEntity::getId).toList() + ); + } + + 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); + } + + static Matcher and(List> matchers) { + return AndMatcher.of(matchers); + } + + static Matcher or(List> matchers) { + return OrMatcher.of(matchers); + } +} 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/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..369bba7e3e9 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java @@ -0,0 +1,29 @@ +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<>("integer:string", "foo", i -> integerListMap.get(i)); + + 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..f29dcbd45ae --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java @@ -0,0 +1,148 @@ +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(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(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(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)); + } +} From df0b4b25af48869719bcf05fc38615d199d37083 Mon Sep 17 00:00:00 2001 From: eibakke Date: Mon, 4 Mar 2024 15:29:21 +0100 Subject: [PATCH 2/6] Adds an expression builder for building up a list of matchers simply and in a logically consistent manner. Also does List.copyOf instead of simple reassignment between TripOnServiceDateRequestBuilder and TripOnServiceDateRequest. --- .../api/request/TripOnServiceDateRequest.java | 14 +-- .../model/filter/expr/ExpressionBuilder.java | 37 +++++++ .../TripOnServiceDateMatcherFactory.java | 102 ++---------------- 3 files changed, 55 insertions(+), 98 deletions(-) create mode 100644 src/main/java/org/opentripplanner/transit/model/filter/expr/ExpressionBuilder.java diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java index 6c9cc9e8718..a7a3a53dac6 100644 --- a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java @@ -30,13 +30,13 @@ protected TripOnServiceDateRequest( List operatingDays, List alterations ) { - this.authorities = authorities; - this.lines = lines; - this.serviceJourneys = serviceJourneys; - this.replacementFor = replacementFor; - this.privateCodes = privateCodes; - this.alterations = alterations; - this.operatingDays = operatingDays; + this.authorities = List.copyOf(authorities); + this.lines = List.copyOf(lines); + this.serviceJourneys = List.copyOf(serviceJourneys); + this.replacementFor = List.copyOf(replacementFor); + this.privateCodes = List.copyOf(privateCodes); + this.alterations = List.copyOf(alterations); + this.operatingDays = List.copyOf(operatingDays); } public static TripOnServiceDateRequestBuilder of(List operatingDays) { 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/transit/TripOnServiceDateMatcherFactory.java b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java index bf87d9c0ff4..ba23d679261 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java @@ -1,14 +1,11 @@ package org.opentripplanner.transit.model.filter.transit; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; -import org.opentripplanner.transit.model.filter.expr.AndMatcher; 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.filter.expr.OrMatcher; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.TripAlteration; @@ -23,85 +20,16 @@ public class TripOnServiceDateMatcherFactory { public static Matcher of(TripOnServiceDateRequest request) { - List> matchers = new ArrayList<>(); - - matchers.add( - or( - request - .getOperatingDays() - .stream() - .map(TripOnServiceDateMatcherFactory::operatingDay) - .toList() - ) - ); - - if (!request.getAuthorities().isEmpty()) { - matchers.add( - or( - request - .getAuthorities() - .stream() - .map(TripOnServiceDateMatcherFactory::authorityId) - .toList() - ) - ); - } - - if (!request.getLines().isEmpty()) { - matchers.add( - or(request.getLines().stream().map(TripOnServiceDateMatcherFactory::routeId).toList()) - ); - } - - if (!request.getServiceJourneys().isEmpty()) { - matchers.add( - or( - request - .getServiceJourneys() - .stream() - .map(TripOnServiceDateMatcherFactory::serviceJourneyId) - .toList() - ) - ); - } - - if (!request.getReplacementFor().isEmpty()) { - matchers.add( - or( - request - .getReplacementFor() - .stream() - .map(TripOnServiceDateMatcherFactory::replacementFor) - .toList() - ) - ); - } - - if (!request.getPrivateCodes().isEmpty()) { - matchers.add( - or( - request - .getPrivateCodes() - .stream() - .map(TripOnServiceDateMatcherFactory::privateCode) - .toList() - ) - ); - } - - if (!request.getAlterations().isEmpty()) { - matchers.add( - or( - request - .getAlterations() - .stream() - .map(TripOnServiceDateMatcherFactory::alteration) - .toList() - ) - ); - } - - return and(matchers); + ExpressionBuilder expr = ExpressionBuilder.of(); + + expr.or(request.getOperatingDays(), TripOnServiceDateMatcherFactory::operatingDay); + expr.or(request.getAuthorities(), TripOnServiceDateMatcherFactory::authorityId); + expr.or(request.getLines(), TripOnServiceDateMatcherFactory::routeId); + expr.or(request.getServiceJourneys(), TripOnServiceDateMatcherFactory::serviceJourneyId); + expr.or(request.getReplacementFor(), TripOnServiceDateMatcherFactory::replacementFor); + expr.or(request.getPrivateCodes(), TripOnServiceDateMatcherFactory::privateCode); + expr.or(request.getAlterations(), TripOnServiceDateMatcherFactory::alteration); + return expr.build(); } static Matcher authorityId(FeedScopedId id) { @@ -139,12 +67,4 @@ static Matcher operatingDay(LocalDate date) { static Matcher alteration(TripAlteration alteration) { return new EqualityMatcher<>("alteration", alteration, TripOnServiceDate::getTripAlteration); } - - static Matcher and(List> matchers) { - return AndMatcher.of(matchers); - } - - static Matcher or(List> matchers) { - return OrMatcher.of(matchers); - } } From b782f55ab0b8d19e971453d4538c9927a982ceed Mon Sep 17 00:00:00 2001 From: eibakke Date: Wed, 6 Mar 2024 12:08:59 +0100 Subject: [PATCH 3/6] Adds convenience function for null and empty check for collections and removes get prefix from getters. --- .../model/timetable/DatedServiceJourneyQuery.java | 7 ++++--- .../api/request/TripOnServiceDateRequest.java | 14 +++++++------- .../transit/TripOnServiceDateMatcherFactory.java | 14 +++++++------- 3 files changed, 18 insertions(+), 17 deletions(-) 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 69209d4610d..d8cfc591392 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 @@ -13,6 +13,7 @@ import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.support.GqlUtil; +import org.opentripplanner.framework.collection.CollectionUtils; import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.api.request.TripOnServiceDateRequestBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -101,7 +102,7 @@ public static GraphQLFieldDefinition createQuery( var operatingDays = environment.>getArgument("operatingDays"); var alterations = environment.>getArgument("alterations"); - if (operatingDays == null || operatingDays.isEmpty()) { + if (CollectionUtils.isEmpty(operatingDays)) { throw new IllegalArgumentException("At least one operatingDay must be provided."); } @@ -112,12 +113,12 @@ public static GraphQLFieldDefinition createQuery( .withServiceJourneys(serviceJourneys) .withReplacementFor(replacementFor); - if (privateCodes != null && !privateCodes.isEmpty()) { + if (!CollectionUtils.isEmpty(privateCodes)) { tripOnServiceDateRequestBuilder = tripOnServiceDateRequestBuilder.withPrivateCodes(privateCodes); } - if (alterations != null && !alterations.isEmpty()) { + if (!CollectionUtils.isEmpty(alterations)) { tripOnServiceDateRequestBuilder = tripOnServiceDateRequestBuilder.withAlterations(alterations); } diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java index a7a3a53dac6..5fe947b2da1 100644 --- a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java @@ -43,31 +43,31 @@ public static TripOnServiceDateRequestBuilder of(List operatingDays) return new TripOnServiceDateRequestBuilder(operatingDays); } - public List getAuthorities() { + public List authorities() { return authorities; } - public List getLines() { + public List lines() { return lines; } - public List getServiceJourneys() { + public List serviceJourneys() { return serviceJourneys; } - public List getReplacementFor() { + public List replacementFor() { return replacementFor; } - public List getPrivateCodes() { + public List privateCodes() { return privateCodes; } - public List getAlterations() { + public List alterations() { return alterations; } - public List getOperatingDays() { + public List operatingDays() { return operatingDays; } } 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 index ba23d679261..e20ccb3b3f9 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java @@ -22,13 +22,13 @@ public class TripOnServiceDateMatcherFactory { public static Matcher of(TripOnServiceDateRequest request) { ExpressionBuilder expr = ExpressionBuilder.of(); - expr.or(request.getOperatingDays(), TripOnServiceDateMatcherFactory::operatingDay); - expr.or(request.getAuthorities(), TripOnServiceDateMatcherFactory::authorityId); - expr.or(request.getLines(), TripOnServiceDateMatcherFactory::routeId); - expr.or(request.getServiceJourneys(), TripOnServiceDateMatcherFactory::serviceJourneyId); - expr.or(request.getReplacementFor(), TripOnServiceDateMatcherFactory::replacementFor); - expr.or(request.getPrivateCodes(), TripOnServiceDateMatcherFactory::privateCode); - expr.or(request.getAlterations(), TripOnServiceDateMatcherFactory::alteration); + 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(); } From 0a0f475d93f2433e4989d3379de808b72453f0b8 Mon Sep 17 00:00:00 2001 From: eibakke Date: Wed, 20 Mar 2024 11:35:23 +0100 Subject: [PATCH 4/6] Addresses minor comments in code review. --- .../model/timetable/DatedServiceJourneyQuery.java | 3 ++- .../framework/collection/CollectionUtils.java | 3 +++ .../api/request/TripOnServiceDateRequest.java | 13 ++++++++----- .../request/TripOnServiceDateRequestBuilder.java | 12 ++++++------ .../transit/model/filter/expr/ContainsMatcher.java | 10 +++++----- .../transit/model/filter/expr/EqualityMatcher.java | 4 ++-- .../framework/collection/CollectionUtilsTest.java | 9 +++++++++ .../TripOnServiceDateMatcherFactoryTest.java | 9 ++++++--- 8 files changed, 41 insertions(+), 22 deletions(-) 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 d8cfc591392..c0e1657d689 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 @@ -107,7 +107,8 @@ public static GraphQLFieldDefinition createQuery( } TripOnServiceDateRequestBuilder tripOnServiceDateRequestBuilder = TripOnServiceDateRequest - .of(operatingDays) + .of() + .withOperatingDays(operatingDays) .withAuthorities(authorities) .withLines(lines) .withServiceJourneys(serviceJourneys) 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/transit/api/request/TripOnServiceDateRequest.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java index 5fe947b2da1..551ee656ced 100644 --- a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java @@ -13,34 +13,37 @@ */ 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; - private final List operatingDays; protected TripOnServiceDateRequest( + List operatingDays, List authorities, List lines, List serviceJourneys, List replacementFor, List privateCodes, - List operatingDays, List alterations ) { + if (operatingDays == null || operatingDays.isEmpty()) { + throw new IllegalArgumentException("operatingDays must have at least one date"); + } + this.operatingDays = List.copyOf(operatingDays); this.authorities = List.copyOf(authorities); this.lines = List.copyOf(lines); this.serviceJourneys = List.copyOf(serviceJourneys); this.replacementFor = List.copyOf(replacementFor); this.privateCodes = List.copyOf(privateCodes); this.alterations = List.copyOf(alterations); - this.operatingDays = List.copyOf(operatingDays); } - public static TripOnServiceDateRequestBuilder of(List operatingDays) { - return new TripOnServiceDateRequestBuilder(operatingDays); + public static TripOnServiceDateRequestBuilder of() { + return new TripOnServiceDateRequestBuilder(); } public List authorities() { diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java index 4aa5dd076d8..a1e2707b099 100644 --- a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java @@ -13,13 +13,13 @@ public class TripOnServiceDateRequestBuilder { private List replacementFor = List.of(); private List privateCodes = List.of(); private List alterations = List.of(); - private final List operatingDays; + private List operatingDays; - protected TripOnServiceDateRequestBuilder(List operatingDays) { - if (operatingDays == null || operatingDays.isEmpty()) { - throw new IllegalArgumentException("operatingDays must have at least one date"); - } + protected TripOnServiceDateRequestBuilder() {} + + public TripOnServiceDateRequestBuilder withOperatingDays(List operatingDays) { this.operatingDays = operatingDays; + return this; } public TripOnServiceDateRequestBuilder withAuthorities(List authorities) { @@ -54,12 +54,12 @@ public TripOnServiceDateRequestBuilder withAlterations(List alte public TripOnServiceDateRequest build() { return new TripOnServiceDateRequest( + operatingDays, authorities, lines, serviceJourneys, replacementFor, privateCodes, - operatingDays, alterations ); } 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 index 79860e943b0..3c93ec0c580 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java @@ -8,13 +8,13 @@ *

* The collection is provided by a function that takes the entity being matched as an argument. */ -public class ContainsMatcher implements Matcher { +public class ContainsMatcher implements Matcher { private final String typeName; - private final V value; - private final Function> valueProvider; + private final S value; + private final Function> valueProvider; - public ContainsMatcher(String typeName, V value, Function> valueProvider) { + public ContainsMatcher(String typeName, S value, Function> valueProvider) { this.typeName = typeName; this.value = value; this.valueProvider = valueProvider; @@ -22,7 +22,7 @@ public ContainsMatcher(String typeName, V value, Function> valu @Override public boolean match(T entity) { - Collection values = valueProvider.apply(entity); + Collection values = valueProvider.apply(entity); if (values == null) { return false; } 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 index 29a6a5bf758..154f3bad04c 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java @@ -3,9 +3,9 @@ import java.util.function.Function; /** - * A matcher that checks if a value is equal to another value. + * A matcher that checks if a value is equal to another value derived from the matched entities. *

- * The value is provided by a function that takes the entity being matched as an argument. + * The derived entity value is provided by a function that takes the entity being matched as an argument. */ public class EqualityMatcher implements Matcher { 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/transit/TripOnServiceDateMatcherFactoryTest.java b/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java index f29dcbd45ae..b7cb7aa6698 100644 --- a/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java +++ b/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java @@ -102,7 +102,8 @@ void setup() { @Test void testMatchOperatingDays() { TripOnServiceDateRequest request = TripOnServiceDateRequest - .of(List.of(LocalDate.of(2024, 2, 22))) + .of() + .withOperatingDays(List.of(LocalDate.of(2024, 2, 22))) .build(); Matcher matcher = TripOnServiceDateMatcherFactory.of(request); @@ -115,7 +116,8 @@ void testMatchOperatingDays() { @Test void testMatchMultiple() { TripOnServiceDateRequest request = TripOnServiceDateRequest - .of(List.of(LocalDate.of(2024, 2, 22))) + .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"))) @@ -131,7 +133,8 @@ void testMatchMultiple() { @Test void testMatchMultipleServiceJourneyMatchers() { TripOnServiceDateRequest request = TripOnServiceDateRequest - .of(List.of(LocalDate.of(2024, 2, 22))) + .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( From 6e2f083d18f8d3e59263eb5d2eac7d6eefeee213 Mon Sep 17 00:00:00 2001 From: eibakke Date: Mon, 25 Mar 2024 11:29:06 +0100 Subject: [PATCH 5/6] Makes ContainsMatcher way more configurable and performant using suggestions from code review. Also improves documentation generally. --- .../transit/model/filter/expr/AndMatcher.java | 2 + .../model/filter/expr/ContainsMatcher.java | 55 +++++++++++++------ .../model/filter/expr/EqualityMatcher.java | 11 +++- .../transit/model/filter/expr/OrMatcher.java | 2 + .../TripOnServiceDateMatcherFactory.java | 6 +- .../filter/expr/ContainsMatcherTest.java | 6 +- 6 files changed, 59 insertions(+), 23 deletions(-) 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 index 8936993bb59..74f38efa8b7 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java @@ -7,6 +7,8 @@ /** * 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 { 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 index 3c93ec0c580..ed3731897ec 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java @@ -1,36 +1,55 @@ package org.opentripplanner.transit.model.filter.expr; -import java.util.Collection; import java.util.function.Function; /** - * A matcher that checks if a collection contains a value. - *

- * The collection is provided by a function that takes the entity being matched as an argument. + * 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 { +public class ContainsMatcher implements Matcher { - private final String typeName; - private final S value; - private final Function> valueProvider; + private final String relationshipName; + private final Function> valuesProvider; + private final Matcher valueMatcher; - public ContainsMatcher(String typeName, S value, Function> valueProvider) { - this.typeName = typeName; - this.value = value; - this.valueProvider = valueProvider; + /** + * @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; } - @Override - public boolean match(T entity) { - Collection values = valueProvider.apply(entity); - if (values == null) { + public boolean match(S entity) { + if (valuesProvider.apply(entity) == null) { return false; } - return values.contains(value); + for (T it : valuesProvider.apply(entity)) { + if (valueMatcher.match(it)) { + return true; + } + } + return false; } @Override public String toString() { - return typeName + " contains " + value; + 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 index 154f3bad04c..1380131e07a 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java @@ -4,8 +4,11 @@ /** * 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 { @@ -13,6 +16,12 @@ public class EqualityMatcher implements Matcher { 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; 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 index 25a58b94e76..62da7af63f4 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java @@ -9,6 +9,8 @@ /** * 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 { 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 index e20ccb3b3f9..f86e7a1ff77 100644 --- a/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java +++ b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java @@ -46,9 +46,9 @@ static Matcher serviceJourneyId(FeedScopedId id) { static Matcher replacementFor(FeedScopedId id) { return new ContainsMatcher<>( - "replacementFor", - id, - t -> t.getReplacementFor().stream().map(AbstractTransitEntity::getId).toList() + "replacementForContains", + t -> t.getReplacementFor().stream().map(AbstractTransitEntity::getId).toList(), + new EqualityMatcher<>("replacementForIdEquals", id, (idToMatch -> idToMatch)) ); } 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 index 369bba7e3e9..1709fc7bf86 100644 --- a/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java @@ -19,7 +19,11 @@ class ContainsMatcherTest { @Test void testMatch() { - var matcher = new ContainsMatcher<>("integer:string", "foo", i -> integerListMap.get(i)); + var matcher = new ContainsMatcher<>( + "contains", + integerListMap::get, + new EqualityMatcher<>("string", "foo", s -> s) + ); assertTrue(matcher.match(1)); assertFalse(matcher.match(2)); From 153fe567fae9f2c45a7c2d6a05ab2cef885f5b80 Mon Sep 17 00:00:00 2001 From: eibakke Date: Thu, 5 Sep 2024 14:16:04 +0200 Subject: [PATCH 6/6] Addresses comments in PR. --- .../timetable/DatedServiceJourneyQuery.java | 20 +++++++------------ .../framework/collection/ListUtils.java | 9 +++++++++ .../api/request/TripOnServiceDateRequest.java | 15 +++++++------- .../TripOnServiceDateRequestBuilder.java | 12 +++++------ 4 files changed, 30 insertions(+), 26 deletions(-) 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 c0e1657d689..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 @@ -13,7 +13,6 @@ import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.support.GqlUtil; -import org.opentripplanner.framework.collection.CollectionUtils; import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.api.request.TripOnServiceDateRequestBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -94,6 +93,9 @@ public static GraphQLFieldDefinition createQuery( .type(new GraphQLList(new GraphQLNonNull(Scalars.GraphQLString))) ) .dataFetcher(environment -> { + // 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")); @@ -102,10 +104,6 @@ public static GraphQLFieldDefinition createQuery( var operatingDays = environment.>getArgument("operatingDays"); var alterations = environment.>getArgument("alterations"); - if (CollectionUtils.isEmpty(operatingDays)) { - throw new IllegalArgumentException("At least one operatingDay must be provided."); - } - TripOnServiceDateRequestBuilder tripOnServiceDateRequestBuilder = TripOnServiceDateRequest .of() .withOperatingDays(operatingDays) @@ -114,15 +112,11 @@ public static GraphQLFieldDefinition createQuery( .withServiceJourneys(serviceJourneys) .withReplacementFor(replacementFor); - if (!CollectionUtils.isEmpty(privateCodes)) { - tripOnServiceDateRequestBuilder = - tripOnServiceDateRequestBuilder.withPrivateCodes(privateCodes); - } + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withPrivateCodes(privateCodes); - if (!CollectionUtils.isEmpty(alterations)) { - tripOnServiceDateRequestBuilder = - tripOnServiceDateRequestBuilder.withAlterations(alterations); - } + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withAlterations(alterations); return GqlUtil .getTransitService(environment) 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 index 551ee656ced..6735dc1db29 100644 --- a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java @@ -2,6 +2,7 @@ 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; @@ -33,13 +34,13 @@ protected TripOnServiceDateRequest( if (operatingDays == null || operatingDays.isEmpty()) { throw new IllegalArgumentException("operatingDays must have at least one date"); } - this.operatingDays = List.copyOf(operatingDays); - this.authorities = List.copyOf(authorities); - this.lines = List.copyOf(lines); - this.serviceJourneys = List.copyOf(serviceJourneys); - this.replacementFor = List.copyOf(replacementFor); - this.privateCodes = List.copyOf(privateCodes); - this.alterations = List.copyOf(alterations); + 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() { diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java index a1e2707b099..7aa2644fdc9 100644 --- a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java @@ -7,12 +7,12 @@ public class TripOnServiceDateRequestBuilder { - private List authorities = List.of(); - private List lines = List.of(); - private List serviceJourneys = List.of(); - private List replacementFor = List.of(); - private List privateCodes = List.of(); - private List alterations = List.of(); + private List authorities; + private List lines; + private List serviceJourneys; + private List replacementFor; + private List privateCodes; + private List alterations; private List operatingDays; protected TripOnServiceDateRequestBuilder() {}