-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reject SIRI messages with invalid stops and missing times #6499
Changes from all commits
02cf113
24adf92
f7abc81
cc97c9d
c1a93f4
47847cf
3a549a8
36ed324
20dbee1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,10 @@ | ||
package org.opentripplanner.updater.trip.siri; | ||
|
||
import static java.lang.Boolean.TRUE; | ||
import static org.opentripplanner.model.PickDrop.NONE; | ||
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.STOP_MISMATCH; | ||
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TOO_FEW_STOPS; | ||
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TOO_MANY_STOPS; | ||
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.UNKNOWN_STOP; | ||
|
||
import java.time.LocalDate; | ||
import java.time.ZoneId; | ||
|
@@ -102,16 +104,42 @@ public ModifiedTripBuilder( | |
public Result<TripUpdate, UpdateError> build() { | ||
RealTimeTripTimes newTimes = existingTripTimes.copyScheduledTimes(); | ||
|
||
var stopPattern = createStopPattern(pattern, calls, entityResolver); | ||
if (cancellation) { | ||
return cancelTrip(newTimes); | ||
} | ||
|
||
if (calls.size() < existingTripTimes.getNumStops()) { | ||
return UpdateError.result(existingTripTimes.getTrip().getId(), TOO_FEW_STOPS, dataSource); | ||
} | ||
|
||
if (calls.size() > existingTripTimes.getNumStops()) { | ||
return UpdateError.result(existingTripTimes.getTrip().getId(), TOO_MANY_STOPS, dataSource); | ||
} | ||
|
||
if (cancellation || stopPattern.isAllStopsNonRoutable()) { | ||
LOG.debug("Trip is cancelled"); | ||
newTimes.cancelTrip(); | ||
return Result.success( | ||
new TripUpdate(pattern.getStopPattern(), newTimes, serviceDate, dataSource) | ||
var result = createStopPattern(pattern, calls, entityResolver); | ||
if (result.isFailure()) { | ||
int invalidStopIndex = result.failureValue().stopIndex(); | ||
LOG.info( | ||
"Invalid SIRI-ET data for trip {} - {} at stop index {}", | ||
existingTripTimes.getTrip().getId(), | ||
result.failureValue().errorType(), | ||
invalidStopIndex | ||
); | ||
return Result.failure( | ||
new UpdateError( | ||
existingTripTimes.getTrip().getId(), | ||
result.failureValue().errorType(), | ||
invalidStopIndex, | ||
dataSource | ||
) | ||
); | ||
} | ||
|
||
StopPattern stopPattern = result.successValue(); | ||
if (stopPattern.isAllStopsNonRoutable()) { | ||
return cancelTrip(newTimes); | ||
} | ||
|
||
applyUpdates(newTimes); | ||
|
||
if (pattern.getStopPattern().equals(stopPattern)) { | ||
|
@@ -150,15 +178,25 @@ public Result<TripUpdate, UpdateError> build() { | |
return Result.success(new TripUpdate(stopPattern, newTimes, serviceDate, dataSource)); | ||
} | ||
|
||
/** | ||
* Full cancellation of a trip. | ||
*/ | ||
private Result<TripUpdate, UpdateError> cancelTrip(RealTimeTripTimes newTimes) { | ||
newTimes.cancelTrip(); | ||
return Result.success( | ||
new TripUpdate(pattern.getStopPattern(), newTimes, serviceDate, dataSource) | ||
); | ||
} | ||
|
||
/** | ||
* Applies real-time updates from the calls into newTimes. | ||
* Precondition: the number of calls is equal to the number of stops in the pattern (this is | ||
* verified before calling this method). | ||
*/ | ||
private void applyUpdates(RealTimeTripTimes newTimes) { | ||
ZonedDateTime startOfService = ServiceDateUtils.asStartOfService(serviceDate, zoneId); | ||
Set<CallWrapper> alreadyVisited = new HashSet<>(); | ||
|
||
int departureFromPreviousStop = 0; | ||
int lastDepartureDelay = 0; | ||
List<StopLocation> stopsInPattern = pattern.getStops(); | ||
for (int stopIndex = 0; stopIndex < stopsInPattern.size(); stopIndex++) { | ||
StopLocation stopInPattern = stopsInPattern.get(stopIndex); | ||
|
@@ -176,60 +214,37 @@ private void applyUpdates(RealTimeTripTimes newTimes) { | |
} | ||
} | ||
|
||
if (matchingCall != null) { | ||
TimetableHelper.applyUpdates( | ||
startOfService, | ||
newTimes, | ||
stopIndex, | ||
stopIndex == (stopsInPattern.size() - 1), | ||
predictionInaccurate, | ||
matchingCall, | ||
occupancy | ||
if (matchingCall == null) { | ||
throw new IllegalStateException( | ||
"The stop at index %d on the trip %s cannot be matched with any call. This implies a bug.".formatted( | ||
stopIndex, | ||
newTimes.getTrip().getId() | ||
) | ||
); | ||
|
||
alreadyVisited.add(matchingCall); | ||
|
||
lastDepartureDelay = newTimes.getDepartureDelay(stopIndex); | ||
} else { | ||
// No update found in calls | ||
if (pattern.isBoardAndAlightAt(stopIndex, NONE)) { | ||
// When newTimes contains stops without pickup/dropoff - set both arrival/departure to previous stop's departure | ||
// This necessary to accommodate the case when delay is reduced/eliminated between to stops with pickup/dropoff, and | ||
// multiple non-pickup/dropoff stops are in between. | ||
newTimes.updateArrivalTime(stopIndex, departureFromPreviousStop); | ||
newTimes.updateDepartureTime(stopIndex, departureFromPreviousStop); | ||
|
||
LOG.info( | ||
"Siri non-pickup/dropoff stop time interpolation for tripId: {}", | ||
newTimes.getTrip().getId() | ||
); | ||
} else { | ||
int arrivalDelay = lastDepartureDelay; | ||
int departureDelay = lastDepartureDelay; | ||
|
||
if (lastDepartureDelay == 0) { | ||
//No match has been found yet (i.e. still in RecordedCalls) - keep existing delays | ||
arrivalDelay = existingTripTimes.getArrivalDelay(stopIndex); | ||
departureDelay = existingTripTimes.getDepartureDelay(stopIndex); | ||
} | ||
|
||
newTimes.updateArrivalDelay(stopIndex, arrivalDelay); | ||
newTimes.updateDepartureDelay(stopIndex, departureDelay); | ||
|
||
LOG.info("Siri stop time interpolation for tripId: {}", newTimes.getTrip().getId()); | ||
} | ||
} | ||
|
||
departureFromPreviousStop = newTimes.getDepartureTime(stopIndex); | ||
TimetableHelper.applyUpdates( | ||
startOfService, | ||
newTimes, | ||
stopIndex, | ||
stopIndex == (stopsInPattern.size() - 1), | ||
predictionInaccurate, | ||
matchingCall, | ||
occupancy | ||
); | ||
|
||
alreadyVisited.add(matchingCall); | ||
} | ||
} | ||
|
||
/** | ||
* Creates a new StopPattern, based on an existing pattern, and list of calls. The stops can be | ||
* replaced with stops belonging to the same Station/StopPlace. The PickDrop values are updated | ||
* as well. | ||
* Precondition: the number of calls is equal to the number of stops in the pattern (this is | ||
* verified before calling this method). | ||
*/ | ||
static StopPattern createStopPattern( | ||
static Result<StopPattern, UpdateError> createStopPattern( | ||
TripPattern pattern, | ||
List<CallWrapper> calls, | ||
EntityResolver entityResolver | ||
|
@@ -242,16 +257,22 @@ static StopPattern createStopPattern( | |
for (int i = 0; i < numberOfStops; i++) { | ||
StopLocation stop = builder.stops.original(i); | ||
|
||
boolean matchFound = false; | ||
for (CallWrapper call : calls) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic in this method will still accept if there are too many calls in the ET update. Maybe fixing that is part of your other extra-call-PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have added validation rules for too few and too many calls. |
||
if (alreadyVisited.contains(call)) { | ||
continue; | ||
} | ||
|
||
//Current stop is being updated | ||
var callStop = entityResolver.resolveQuay(call.getStopPointRef()); | ||
if (callStop == null) { | ||
return Result.failure(new UpdateError(null, UNKNOWN_STOP, i)); | ||
} | ||
|
||
if (!stop.equals(callStop) && !stop.isPartOfSameStationAs(callStop)) { | ||
continue; | ||
} | ||
matchFound = true; | ||
|
||
// Used in lambda | ||
final int stopIndex = i; | ||
|
@@ -268,10 +289,13 @@ static StopPattern createStopPattern( | |
alreadyVisited.add(call); | ||
break; | ||
} | ||
if (!matchFound) { | ||
return Result.failure(new UpdateError(null, STOP_MISMATCH, i)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will fail with this error if there is a missing call as well. I guess that's not a big probeblem but it seems like an inaccurate error code for that case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have added validation rules for too few and too many calls. |
||
} | ||
} | ||
var newStopPattern = builder.build(); | ||
return (pattern.isModified() && pattern.getStopPattern().equals(newStopPattern)) | ||
? pattern.getStopPattern() | ||
: newStopPattern; | ||
? Result.success(pattern.getStopPattern()) | ||
: Result.success(newStopPattern); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -534,7 +534,7 @@ void testAddedTripFailOnUnknownStop() { | |
|
||
assertTrue(addedTrip.isFailure(), "Trip creation should fail"); | ||
assertEquals( | ||
UpdateError.UpdateErrorType.NO_VALID_STOPS, | ||
UpdateError.UpdateErrorType.UNKNOWN_STOP, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a test for UNKNOWN_STOP here but perhaps you could add one for regular, non-extra-journey updates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have added unit tests for unknown stop and stop mismatch |
||
addedTrip.failureValue().errorType(), | ||
"Trip creation should fail with call referring to unknown stop" | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a line of documentation in both
createStopPattern()
andapplyUpdates()
that the number of calls are guaranteed to be the same as the numberOfStops. I think that will make it easier to if we decide to refactor this code in the future.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done