From c2be864def127ce07d23bd5804c9feb215b19d90 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Thu, 19 Oct 2023 15:25:31 +0200 Subject: [PATCH 1/3] Validate stop id in Transit leg reference --- .../model/plan/ScheduledTransitLeg.java | 4 +- .../legreference/LegReferenceSerializer.java | 27 +++--- .../ScheduledTransitLegReference.java | 78 +++++++++++++++- .../LegReferenceSerializerTest.java | 23 +++-- .../ScheduledTransitLegReferenceTest.java | 90 ++++++++++++++++--- .../stoptimes/AlternativeLegsTest.java | 22 ++++- 6 files changed, 204 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index dc1843b4d52..10158cdc13b 100644 --- a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -350,7 +350,9 @@ public LegReference getLegReference() { tripTimes.getTrip().getId(), serviceDate, boardStopPosInPattern, - alightStopPosInPattern + alightStopPosInPattern, + tripPattern.getStops().get(boardStopPosInPattern).getId(), + tripPattern.getStops().get(alightStopPosInPattern).getId() ); } diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java index eee47124f89..44e86f05eea 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java @@ -26,19 +26,6 @@ public class LegReferenceSerializer { private static final Logger LOG = LoggerFactory.getLogger(LegReferenceSerializer.class); - // TODO: This is for backwards compatibility. Change to use ISO_LOCAL_DATE after OTP v2.2 is released - private static final DateTimeFormatter LENIENT_ISO_LOCAL_DATE = new DateTimeFormatterBuilder() - .appendValue(YEAR, 4) - .optionalStart() - .appendLiteral('-') - .optionalEnd() - .appendValue(MONTH_OF_YEAR, 2) - .optionalStart() - .appendLiteral('-') - .optionalEnd() - .appendValue(DAY_OF_MONTH, 2) - .toFormatter(); - /** private constructor to prevent instantiating this utility class */ private LegReferenceSerializer() {} @@ -93,18 +80,28 @@ static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) out.writeUTF(s.serviceDate().toString()); out.writeInt(s.fromStopPositionInPattern()); out.writeInt(s.toStopPositionInPattern()); + out.writeUTF(s.fromStopId().toString()); + out.writeUTF(s.toStopId().toString()); } else { throw new IllegalArgumentException("Invalid LegReference type"); } } + /** + * Deserialize a leg reference. + * To remain backward-compatible, additional fields (stopId) are read optionally. + * TODO: Remove backward-compatible logic after OTP release 2.6 + * + */ static LegReference readScheduledTransitLeg(ObjectInputStream objectInputStream) throws IOException { return new ScheduledTransitLegReference( FeedScopedId.parse(objectInputStream.readUTF()), - LocalDate.parse(objectInputStream.readUTF(), LENIENT_ISO_LOCAL_DATE), + LocalDate.parse(objectInputStream.readUTF(), DateTimeFormatter.ISO_LOCAL_DATE), + objectInputStream.readInt(), objectInputStream.readInt(), - objectInputStream.readInt() + objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null, + objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null ); } diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java index e5e2ab695a4..41493416cad 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java @@ -9,6 +9,7 @@ import org.opentripplanner.routing.algorithm.mapping.AlertToLegMapper; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.TransitService; @@ -23,7 +24,10 @@ public record ScheduledTransitLegReference( FeedScopedId tripId, LocalDate serviceDate, int fromStopPositionInPattern, - int toStopPositionInPattern + int toStopPositionInPattern, + FeedScopedId fromStopId, + + FeedScopedId toStopId ) implements LegReference { private static final Logger LOG = LoggerFactory.getLogger(ScheduledTransitLegReference.class); @@ -35,6 +39,10 @@ public record ScheduledTransitLegReference( * rolled out, or because a realtime update has modified a trip), * it may not be possible to reconstruct the leg. * In this case the method returns null. + * The method checks that the referenced stop positions still refer to the same stop ids. + * As an exception, the reference is still considered valid if the referenced stop is different + * but belongs to the same parent station: this covers for example the case of a last-minute + * platform change in a train station that typically does not affect the validity of the leg. */ @Override @Nullable @@ -69,6 +77,18 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { return null; } + if ( + !matchReferencedStopInPattern( + tripPattern, + fromStopPositionInPattern, + fromStopId, + transitService + ) || + !matchReferencedStopInPattern(tripPattern, toStopPositionInPattern, toStopId, transitService) + ) { + return null; + } + Timetable timetable = transitService.getTimetableForTripPattern(tripPattern, serviceDate); TripTimes tripTimes = timetable.getTripTimes(trip); @@ -123,4 +143,60 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { return leg; } + + /** + * Return false if the stop id in the reference does not match the actual stop id in the trip + * pattern. + * Return true in the specific case where the stop ids differ, but belong to the same parent + * station. + * + */ + private boolean matchReferencedStopInPattern( + TripPattern tripPattern, + int stopPosition, + FeedScopedId stopId, + TransitService transitService + ) { + if (stopId == null) { + // this is a legacy reference, skip validation + // TODO: remove backward-compatible logic after OTP release 2.6 + return true; + } + + StopLocation stopLocationInPattern = tripPattern.getStops().get(stopPosition); + if (stopId.equals(stopLocationInPattern.getId())) { + return true; + } + StopLocation stopLocationInLegReference = transitService.getStopLocation(stopId); + if ( + stopLocationInLegReference == null || + stopLocationInPattern.getParentStation() == null || + !stopLocationInPattern + .getParentStation() + .equals(stopLocationInLegReference.getParentStation()) + ) { + LOG.info( + "Invalid transit leg reference:" + + " The referenced stop at position {} with id '{}' does not match" + + " the stop id '{}' in trip {} and service date {}", + stopPosition, + stopId, + stopLocationInPattern.getId(), + tripId, + serviceDate + ); + return false; + } + LOG.info( + "Transit leg reference with modified stop id within the same station: " + + "The referenced stop at position {} with id '{}' does not match\" +\n" + + " \" the stop id '{}' in trip {} and service date {}", + stopPosition, + stopId, + stopLocationInPattern.getId(), + tripId, + serviceDate + ); + return true; + } } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java index 0ef6b240a56..563136432ea 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java @@ -1,25 +1,38 @@ package org.opentripplanner.model.plan.legreference; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.time.LocalDate; import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.FeedScopedId; class LegReferenceSerializerTest { - private static final FeedScopedId TRIP_ID = new FeedScopedId("F", "Trip"); + private static final FeedScopedId TRIP_ID = TransitModelForTest.id("Trip"); private static final LocalDate SERVICE_DATE = LocalDate.of(2022, 1, 31); private static final int FROM_STOP_POS = 1; + + private static final FeedScopedId FROM_STOP_ID = TransitModelForTest.id("Boarding Stop"); private static final int TO_STOP_POS = 3; + private static final FeedScopedId TO_STOP_ID = TransitModelForTest.id("Alighting Stop"); private static final String ENCODED_TOKEN = - "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; + "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; + private static final String ENCODED_LEGACY_TOKEN = - "rO0ABXc0ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAIMjAyMjAxMzEAAAABAAAAAw=="; + "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; @Test void testScheduledTransitLegReferenceRoundTrip() { - var ref = new ScheduledTransitLegReference(TRIP_ID, SERVICE_DATE, 1, 3); + var ref = new ScheduledTransitLegReference( + TRIP_ID, + SERVICE_DATE, + FROM_STOP_POS, + TO_STOP_POS, + FROM_STOP_ID, + TO_STOP_ID + ); var out = LegReferenceSerializer.encode(ref); @@ -33,7 +46,7 @@ void testScheduledTransitLegReferenceRoundTrip() { @Test void testScheduledTransitLegReferenceDeserialize() { var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN); - + assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); assertEquals(SERVICE_DATE, ref.serviceDate()); assertEquals(FROM_STOP_POS, ref.fromStopPositionInPattern()); diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java index ed54ac1bd44..d44b2eb4857 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java @@ -18,6 +18,8 @@ import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.DefaultTransitService; @@ -30,19 +32,34 @@ class ScheduledTransitLegReferenceTest { private static final int SERVICE_CODE = 555; private static final LocalDate SERVICE_DATE = LocalDate.of(2023, 1, 1); private static final int NUMBER_OF_STOPS = 3; + public static FeedScopedId stopIdAtPosition0; + public static FeedScopedId stopIdAtPosition1; + + public static FeedScopedId stopIdAtPosition2; private static TransitService transitService; private static FeedScopedId tripId; + private static RegularStop stop4; @BeforeAll static void buildTransitService() { + Station parentStation = TransitModelForTest.station("PARENT_STATION").build(); + + RegularStop stop1 = TransitModelForTest.stopForTest("STOP1", 0, 0); + RegularStop stop2 = TransitModelForTest.stopForTest("STOP2", 0, 0); + RegularStop stop3 = TransitModelForTest.stopForTest("STOP3", 0, 0, parentStation); + stop4 = TransitModelForTest.stopForTest("STOP4", 0, 0, parentStation); + // build transit data TripPattern tripPattern = TransitModelForTest .tripPattern("1", TransitModelForTest.route(id("1")).build()) - .withStopPattern(TransitModelForTest.stopPattern(NUMBER_OF_STOPS)) + .withStopPattern(TransitModelForTest.stopPattern(stop1, stop2, stop3)) .build(); Timetable timetable = tripPattern.getScheduledTimetable(); Trip trip = TransitModelForTest.trip("1").build(); tripId = trip.getId(); + stopIdAtPosition0 = tripPattern.getStop(0).getId(); + stopIdAtPosition1 = tripPattern.getStop(1).getId(); + stopIdAtPosition2 = tripPattern.getStop(2).getId(); TripTimes tripTimes = new TripTimes( trip, TransitModelForTest.stopTimesEvery5Minutes(5, trip, PlanTestConstants.T11_00), @@ -52,7 +69,14 @@ static void buildTransitService() { timetable.addTripTimes(tripTimes); // build transit model - TransitModel transitModel = new TransitModel(StopModel.of().build(), new Deduplicator()); + StopModel stopModel = StopModel + .of() + .withRegularStop(stop1) + .withRegularStop(stop2) + .withRegularStop(stop3) + .withRegularStop(stop4) + .build(); + TransitModel transitModel = new TransitModel(stopModel, new Deduplicator()); transitModel.addTripPattern(tripPattern.getId(), tripPattern); transitModel.getServiceCodes().put(tripPattern.getId(), SERVICE_CODE); CalendarServiceData calendarServiceData = new CalendarServiceData(); @@ -66,20 +90,22 @@ static void buildTransitService() { @Test void getLegFromReference() { - int boardAtStop = 0; - int alightAtStop = 1; + int boardAtStopPos = 0; + int alightAtStopPos = 1; ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( tripId, SERVICE_DATE, - boardAtStop, - alightAtStop + boardAtStopPos, + alightAtStopPos, + stopIdAtPosition0, + stopIdAtPosition1 ); ScheduledTransitLeg leg = scheduledTransitLegReference.getLeg(transitService); assertNotNull(leg); assertEquals(tripId, leg.getTrip().getId()); assertEquals(SERVICE_DATE, leg.getServiceDate()); - assertEquals(boardAtStop, leg.getBoardStopPosInPattern()); - assertEquals(alightAtStop, leg.getAlightStopPosInPattern()); + assertEquals(boardAtStopPos, leg.getBoardStopPosInPattern()); + assertEquals(alightAtStopPos, leg.getAlightStopPosInPattern()); } @Test @@ -88,7 +114,9 @@ void getLegFromReferenceUnknownTrip() { FeedScopedId.ofNullable("XXX", "YYY"), SERVICE_DATE, 0, - 1 + 1, + stopIdAtPosition0, + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @@ -99,29 +127,63 @@ void getLegFromReferenceInvalidServiceDate() { tripId, LocalDate.EPOCH, 0, - 1 + 1, + stopIdAtPosition0, + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @Test - void getLegFromReferenceInvalidBoardingStop() { + void getLegFromReferenceOutOfRangeBoardingStop() { ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( tripId, SERVICE_DATE, NUMBER_OF_STOPS, - 1 + 1, + stopIdAtPosition0, + stopIdAtPosition1 + ); + assertNull(scheduledTransitLegReference.getLeg(transitService)); + } + + @Test + void getLegFromReferenceMismatchOnBoardingStop() { + ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( + tripId, + SERVICE_DATE, + 0, + 1, + TransitModelForTest.id("invalid stop id"), + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @Test - void getLegFromReferenceInvalidAlightingStop() { + void getLegFromReferenceMismatchOnAlightingStopSameParentStation() { + // this tests substitutes the actual alighting stop (stop3) by another stop (stop4) that + // belongs to the same parent station ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( tripId, SERVICE_DATE, 0, - NUMBER_OF_STOPS + 2, + stopIdAtPosition0, + stop4.getId() + ); + assertNotNull(scheduledTransitLegReference.getLeg(transitService)); + } + + @Test + void getLegFromReferenceOutOfRangeAlightingStop() { + ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( + tripId, + SERVICE_DATE, + 0, + NUMBER_OF_STOPS, + stopIdAtPosition0, + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } diff --git a/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java b/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java index db11e81dabc..a7369a98a8c 100644 --- a/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java +++ b/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java @@ -21,6 +21,12 @@ */ class AlternativeLegsTest extends GtfsTest { + private static final String FEED_ID = "FEED"; + private static final FeedScopedId STOP_ID_B = new FeedScopedId(FEED_ID, "B"); + private static final FeedScopedId STOP_ID_C = new FeedScopedId(FEED_ID, "C"); + private static final FeedScopedId STOP_ID_X = new FeedScopedId(FEED_ID, "X"); + private static final FeedScopedId STOP_ID_Y = new FeedScopedId(FEED_ID, "Y"); + @Override public String getFeedName() { return "gtfs/simple"; @@ -34,7 +40,9 @@ void testPreviousLegs() { new FeedScopedId(this.feedId.getId(), "1.2"), LocalDate.parse("2022-04-02"), 1, - 2 + 2, + STOP_ID_B, + STOP_ID_C ) .getLeg(transitService); @@ -70,7 +78,9 @@ void testNextLegs() { new FeedScopedId(this.feedId.getId(), "2.2"), LocalDate.parse("2022-04-02"), 0, - 1 + 1, + STOP_ID_B, + STOP_ID_C ) .getLeg(transitService); @@ -106,7 +116,9 @@ void testCircularRoutes() { new FeedScopedId(this.feedId.getId(), "19.1"), LocalDate.parse("2022-04-02"), 1, - 2 + 2, + STOP_ID_X, + STOP_ID_Y ) .getLeg(transitService); @@ -137,7 +149,9 @@ void testComplexCircularRoutes() { new FeedScopedId(this.feedId.getId(), "19.1"), LocalDate.parse("2022-04-02"), 1, - 7 + 7, + STOP_ID_X, + STOP_ID_B ) .getLeg(transitService); From 17937de169f5d766035a4c4c1f5a07a5819fe185 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 20 Oct 2023 12:41:47 +0200 Subject: [PATCH 2/3] Applied review suggestions --- .../model/plan/legreference/LegReferenceSerializer.java | 2 +- .../plan/legreference/ScheduledTransitLegReference.java | 2 +- .../plan/legreference/LegReferenceSerializerTest.java | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java index 44e86f05eea..8cfabeeb111 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java @@ -90,7 +90,7 @@ static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) /** * Deserialize a leg reference. * To remain backward-compatible, additional fields (stopId) are read optionally. - * TODO: Remove backward-compatible logic after OTP release 2.6 + * TODO: Remove backward-compatible logic after OTP release 2.5 * */ static LegReference readScheduledTransitLeg(ObjectInputStream objectInputStream) diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java index 41493416cad..ae81a906d12 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java @@ -159,7 +159,7 @@ private boolean matchReferencedStopInPattern( ) { if (stopId == null) { // this is a legacy reference, skip validation - // TODO: remove backward-compatible logic after OTP release 2.6 + // TODO: remove backward-compatible logic after OTP release 2.5 return true; } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java index 563136432ea..86d900913d5 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java @@ -17,9 +17,16 @@ class LegReferenceSerializerTest { private static final FeedScopedId FROM_STOP_ID = TransitModelForTest.id("Boarding Stop"); private static final int TO_STOP_POS = 3; private static final FeedScopedId TO_STOP_ID = TransitModelForTest.id("Alighting Stop"); + + /** + * Token based on the latest format, including stop ids. + */ private static final String ENCODED_TOKEN = "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; + /** + * Token based on the previous format, without stop ids. + */ private static final String ENCODED_LEGACY_TOKEN = "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; From 562609b560d952a8bf9ece8b1fcf1a8ec3f0b6da Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 20 Oct 2023 16:14:23 +0200 Subject: [PATCH 3/3] Add support for scheduled leg reference versioning --- .../legreference/LegReferenceSerializer.java | 46 ++++++++++++------- .../plan/legreference/LegReferenceType.java | 40 ++++++++++++---- .../LegReferenceSerializerTest.java | 14 +++--- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java index 8cfabeeb111..067037f54e1 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java @@ -1,18 +1,12 @@ package org.opentripplanner.model.plan.legreference; -import static java.time.temporal.ChronoField.DAY_OF_MONTH; -import static java.time.temporal.ChronoField.MONTH_OF_YEAR; -import static java.time.temporal.ChronoField.YEAR; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.text.ParseException; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; import java.util.Base64; import javax.annotation.Nullable; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -67,13 +61,25 @@ public static LegReference decode(String legReference) { var type = readEnum(in, LegReferenceType.class); return type.getDeserializer().read(in); - } catch (IOException | ParseException e) { + } catch (IOException e) { LOG.error("Unable to decode leg reference: '" + legReference + "'", e); return null; } } - static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) + static void writeScheduledTransitLegV1(LegReference ref, ObjectOutputStream out) + throws IOException { + if (ref instanceof ScheduledTransitLegReference s) { + out.writeUTF(s.tripId().toString()); + out.writeUTF(s.serviceDate().toString()); + out.writeInt(s.fromStopPositionInPattern()); + out.writeInt(s.toStopPositionInPattern()); + } else { + throw new IllegalArgumentException("Invalid LegReference type"); + } + } + + static void writeScheduledTransitLegV2(LegReference ref, ObjectOutputStream out) throws IOException { if (ref instanceof ScheduledTransitLegReference s) { out.writeUTF(s.tripId().toString()); @@ -87,21 +93,27 @@ static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) } } - /** - * Deserialize a leg reference. - * To remain backward-compatible, additional fields (stopId) are read optionally. - * TODO: Remove backward-compatible logic after OTP release 2.5 - * - */ - static LegReference readScheduledTransitLeg(ObjectInputStream objectInputStream) + static LegReference readScheduledTransitLegV1(ObjectInputStream objectInputStream) throws IOException { return new ScheduledTransitLegReference( FeedScopedId.parse(objectInputStream.readUTF()), LocalDate.parse(objectInputStream.readUTF(), DateTimeFormatter.ISO_LOCAL_DATE), objectInputStream.readInt(), objectInputStream.readInt(), - objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null, - objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null + null, + null + ); + } + + static LegReference readScheduledTransitLegV2(ObjectInputStream objectInputStream) + throws IOException { + return new ScheduledTransitLegReference( + FeedScopedId.parse(objectInputStream.readUTF()), + LocalDate.parse(objectInputStream.readUTF(), DateTimeFormatter.ISO_LOCAL_DATE), + objectInputStream.readInt(), + objectInputStream.readInt(), + FeedScopedId.parse(objectInputStream.readUTF()), + FeedScopedId.parse(objectInputStream.readUTF()) ); } diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java index 488eb594aa1..44cea2226d3 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java @@ -3,40 +3,60 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.text.ParseException; +import java.util.Arrays; +import java.util.Optional; /** * Enum for different types of LegReferences */ enum LegReferenceType { SCHEDULED_TRANSIT_LEG_V1( + 1, ScheduledTransitLegReference.class, - LegReferenceSerializer::writeScheduledTransitLeg, - LegReferenceSerializer::readScheduledTransitLeg + LegReferenceSerializer::writeScheduledTransitLegV1, + LegReferenceSerializer::readScheduledTransitLegV1 + ), + + SCHEDULED_TRANSIT_LEG_V2( + 2, + ScheduledTransitLegReference.class, + LegReferenceSerializer::writeScheduledTransitLegV2, + LegReferenceSerializer::readScheduledTransitLegV2 ); + private final int version; private final Class legReferenceClass; private final Writer serializer; private final Reader deserializer; LegReferenceType( + int version, Class legReferenceClass, Writer serializer, Reader deserializer ) { + this.version = version; this.legReferenceClass = legReferenceClass; this.serializer = serializer; this.deserializer = deserializer; } + /** + * Return the latest LegReferenceType version for a given leg reference class. + */ static LegReferenceType forClass(Class legReferenceClass) { - for (var type : LegReferenceType.values()) { - if (type.legReferenceClass.equals(legReferenceClass)) { - return type; - } - } - return null; + Optional latestVersion = Arrays + .stream(LegReferenceType.values()) + .filter(legReferenceType -> legReferenceType.legReferenceClass.equals(legReferenceClass)) + .reduce((legReferenceType, other) -> { + if (legReferenceType.version > other.version) { + return legReferenceType; + } + return other; + }); + + return latestVersion.orElse(null); } Writer getSerializer() { @@ -54,6 +74,6 @@ interface Writer { @FunctionalInterface interface Reader { - T read(ObjectInputStream in) throws IOException, ParseException; + T read(ObjectInputStream in) throws IOException; } } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java index 86d900913d5..655c2c24778 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java @@ -21,13 +21,13 @@ class LegReferenceSerializerTest { /** * Token based on the latest format, including stop ids. */ - private static final String ENCODED_TOKEN = - "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; + private static final String ENCODED_TOKEN_V2 = + "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjIABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; /** * Token based on the previous format, without stop ids. */ - private static final String ENCODED_LEGACY_TOKEN = + private static final String ENCODED_TOKEN_V1 = "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; @Test @@ -43,7 +43,7 @@ void testScheduledTransitLegReferenceRoundTrip() { var out = LegReferenceSerializer.encode(ref); - assertEquals(ENCODED_TOKEN, out); + assertEquals(ENCODED_TOKEN_V2, out); var ref2 = LegReferenceSerializer.decode(out); @@ -52,7 +52,7 @@ void testScheduledTransitLegReferenceRoundTrip() { @Test void testScheduledTransitLegReferenceDeserialize() { - var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN); + var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V2); assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); assertEquals(SERVICE_DATE, ref.serviceDate()); @@ -62,8 +62,8 @@ void testScheduledTransitLegReferenceDeserialize() { @Test void testScheduledTransitLegReferenceLegacyDeserialize() { - var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_LEGACY_TOKEN); - + var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V1); + assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); assertEquals(SERVICE_DATE, ref.serviceDate()); assertEquals(FROM_STOP_POS, ref.fromStopPositionInPattern());