|
2 | 2 |
|
3 | 3 | import java.time.ZonedDateTime; |
4 | 4 | import java.util.ArrayList; |
| 5 | +import java.util.Comparator; |
5 | 6 | import java.util.List; |
6 | | -import java.util.Optional; |
| 7 | +import org.opentripplanner.transit.model.framework.Result; |
7 | 8 | import org.opentripplanner.updater.spi.UpdateError.UpdateErrorType; |
8 | 9 | import org.opentripplanner.utils.lang.StringUtils; |
9 | 10 | import uk.org.siri.siri21.ArrivalBoardingActivityEnumeration; |
|
14 | 15 | import uk.org.siri.siri21.NaturalLanguageStringStructure; |
15 | 16 | import uk.org.siri.siri21.OccupancyEnumeration; |
16 | 17 | import uk.org.siri.siri21.RecordedCall; |
| 18 | +import uk.org.siri.siri21.StopPointRefStructure; |
17 | 19 |
|
18 | 20 | /** |
19 | 21 | * This class is a wrapper around either a {@link RecordedCall} or an {@link EstimatedCall}, making |
20 | 22 | * it possible to iterate over both of the types at once. |
| 23 | + * <p> |
| 24 | + * Instances are created via the {@link #of(EstimatedVehicleJourney)} factory which validates and |
| 25 | + * sorts calls during parsing, making invalid {@code CallWrapper} instances unrepresentable. |
21 | 26 | */ |
22 | 27 | public interface CallWrapper { |
23 | | - static CallWrapper of(EstimatedCall estimatedCall) { |
24 | | - return new EstimatedCallWrapper(estimatedCall); |
25 | | - } |
26 | | - |
27 | | - static CallWrapper of(RecordedCall recordedCall) { |
28 | | - return new RecordedCallWrapper(recordedCall); |
29 | | - } |
30 | | - |
31 | | - static List<CallWrapper> of(EstimatedVehicleJourney estimatedVehicleJourney) { |
| 28 | + /** |
| 29 | + * Parse and validate all calls from an {@link EstimatedVehicleJourney}. Each call must have a |
| 30 | + * non-empty stop point ref and exactly one of Order or VisitNumber. All calls must use the same |
| 31 | + * strategy (all Order or all VisitNumber). The returned list is sorted by sort order. |
| 32 | + * |
| 33 | + * @return a successful sorted list of calls, or a failure with the appropriate error type |
| 34 | + */ |
| 35 | + static Result<List<CallWrapper>, UpdateErrorType> of( |
| 36 | + EstimatedVehicleJourney estimatedVehicleJourney |
| 37 | + ) { |
32 | 38 | List<CallWrapper> result = new ArrayList<>(); |
| 39 | + boolean hasOrderCalls = false; |
| 40 | + boolean hasVisitNumberCalls = false; |
33 | 41 |
|
34 | 42 | if (estimatedVehicleJourney.getRecordedCalls() != null) { |
35 | | - for (var recordedCall : estimatedVehicleJourney.getRecordedCalls().getRecordedCalls()) { |
36 | | - result.add(new RecordedCallWrapper(recordedCall)); |
| 43 | + for (var call : estimatedVehicleJourney.getRecordedCalls().getRecordedCalls()) { |
| 44 | + var sortOrder = validateCall( |
| 45 | + call.getStopPointRef(), |
| 46 | + call.getOrder(), |
| 47 | + call.getVisitNumber() |
| 48 | + ); |
| 49 | + if (sortOrder.isFailure()) { |
| 50 | + return sortOrder.toFailureResult(); |
| 51 | + } |
| 52 | + hasOrderCalls |= call.getOrder() != null; |
| 53 | + hasVisitNumberCalls |= call.getVisitNumber() != null; |
| 54 | + result.add(new RecordedCallWrapper(call, sortOrder.successValue())); |
37 | 55 | } |
38 | 56 | } |
39 | 57 |
|
40 | 58 | if (estimatedVehicleJourney.getEstimatedCalls() != null) { |
41 | | - for (var estimatedCall : estimatedVehicleJourney.getEstimatedCalls().getEstimatedCalls()) { |
42 | | - result.add(new EstimatedCallWrapper(estimatedCall)); |
| 59 | + for (var call : estimatedVehicleJourney.getEstimatedCalls().getEstimatedCalls()) { |
| 60 | + var sortOrder = validateCall( |
| 61 | + call.getStopPointRef(), |
| 62 | + call.getOrder(), |
| 63 | + call.getVisitNumber() |
| 64 | + ); |
| 65 | + if (sortOrder.isFailure()) { |
| 66 | + return sortOrder.toFailureResult(); |
| 67 | + } |
| 68 | + hasOrderCalls |= call.getOrder() != null; |
| 69 | + hasVisitNumberCalls |= call.getVisitNumber() != null; |
| 70 | + result.add(new EstimatedCallWrapper(call, sortOrder.successValue())); |
43 | 71 | } |
44 | 72 | } |
45 | 73 |
|
46 | | - return List.copyOf(result); |
47 | | - } |
| 74 | + if (hasOrderCalls && hasVisitNumberCalls) { |
| 75 | + return Result.failure(UpdateErrorType.MIXED_CALL_ORDER_AND_VISIT_NUMBER); |
| 76 | + } |
48 | 77 |
|
49 | | - /** |
50 | | - * Validate that all calls have a non-empty stop point ref and either order or visit number (but |
51 | | - * not both). Also checks cross-call consistency: all calls must use the same strategy (all Order |
52 | | - * or all VisitNumber). A mix of Order and VisitNumber-only calls is rejected. |
53 | | - */ |
54 | | - static Optional<UpdateErrorType> validateAll(List<CallWrapper> calls) { |
55 | | - var perCallError = calls |
56 | | - .stream() |
57 | | - .map(CallWrapper::validate) |
58 | | - .flatMap(Optional::stream) |
59 | | - .findFirst(); |
60 | | - if (perCallError.isPresent()) { |
61 | | - return perCallError; |
62 | | - } |
63 | | - boolean anyHasOrder = calls.stream().anyMatch(CallWrapper::hasOrder); |
64 | | - boolean anyMissingOrder = calls.stream().anyMatch(c -> !c.hasOrder()); |
65 | | - if (anyHasOrder && anyMissingOrder) { |
66 | | - return Optional.of(UpdateErrorType.MIXED_CALL_ORDER_AND_VISIT_NUMBER); |
67 | | - } |
68 | | - return Optional.empty(); |
| 78 | + result.sort(Comparator.comparingInt(CallWrapper::getSortOrder)); |
| 79 | + |
| 80 | + return Result.success(List.copyOf(result)); |
69 | 81 | } |
70 | 82 |
|
71 | 83 | /** |
72 | | - * Validate that the call has a non-empty stop point ref and either order or visit number (but not |
73 | | - * both). |
| 84 | + * Validate a single call's stop point ref and resolve its sort order from Order/VisitNumber. |
74 | 85 | */ |
75 | | - default Optional<UpdateErrorType> validate() { |
76 | | - if (StringUtils.hasNoValueOrNullAsString(getStopPointRef())) { |
77 | | - return Optional.of(UpdateErrorType.EMPTY_STOP_POINT_REF); |
78 | | - } |
79 | | - if (!hasOrder() && !hasVisitNumber()) { |
80 | | - return Optional.of(UpdateErrorType.MISSING_CALL_ORDER); |
81 | | - } |
82 | | - if (hasOrder() && hasVisitNumber()) { |
83 | | - return Optional.of(UpdateErrorType.MIXED_CALL_ORDER_AND_VISIT_NUMBER); |
84 | | - } |
85 | | - return Optional.empty(); |
| 86 | + private static Result<Integer, UpdateErrorType> validateCall( |
| 87 | + StopPointRefStructure stopPointRef, |
| 88 | + java.math.BigInteger order, |
| 89 | + java.math.BigInteger visitNumber |
| 90 | + ) { |
| 91 | + var ref = stopPointRef != null ? stopPointRef.getValue() : null; |
| 92 | + if (StringUtils.hasNoValueOrNullAsString(ref)) { |
| 93 | + return Result.failure(UpdateErrorType.EMPTY_STOP_POINT_REF); |
| 94 | + } |
| 95 | + if (order == null && visitNumber == null) { |
| 96 | + return Result.failure(UpdateErrorType.MISSING_CALL_ORDER); |
| 97 | + } |
| 98 | + if (order != null && visitNumber != null) { |
| 99 | + return Result.failure(UpdateErrorType.MIXED_CALL_ORDER_AND_VISIT_NUMBER); |
| 100 | + } |
| 101 | + return Result.success(order != null ? order.intValueExact() : visitNumber.intValueExact()); |
86 | 102 | } |
87 | 103 |
|
88 | 104 | String getStopPointRef(); |
89 | 105 |
|
90 | | - boolean hasOrder(); |
91 | | - |
92 | | - boolean hasVisitNumber(); |
93 | | - |
94 | 106 | /** |
95 | | - * Return the sort order of this call. Prefers Order if present, falls back to VisitNumber. |
| 107 | + * Return the sort order of this call, resolved during parsing from either Order or VisitNumber. |
96 | 108 | */ |
97 | 109 | int getSortOrder(); |
98 | 110 |
|
@@ -128,37 +140,21 @@ default boolean hasDeparted() { |
128 | 140 | final class EstimatedCallWrapper implements CallWrapper { |
129 | 141 |
|
130 | 142 | private final EstimatedCall call; |
| 143 | + private final int sortOrder; |
131 | 144 |
|
132 | | - private EstimatedCallWrapper(EstimatedCall estimatedCall) { |
| 145 | + private EstimatedCallWrapper(EstimatedCall estimatedCall, int sortOrder) { |
133 | 146 | this.call = estimatedCall; |
| 147 | + this.sortOrder = sortOrder; |
134 | 148 | } |
135 | 149 |
|
136 | 150 | @Override |
137 | 151 | public String getStopPointRef() { |
138 | 152 | return call.getStopPointRef() != null ? call.getStopPointRef().getValue() : null; |
139 | 153 | } |
140 | 154 |
|
141 | | - @Override |
142 | | - public boolean hasOrder() { |
143 | | - return call.getOrder() != null; |
144 | | - } |
145 | | - |
146 | | - @Override |
147 | | - public boolean hasVisitNumber() { |
148 | | - return call.getVisitNumber() != null; |
149 | | - } |
150 | | - |
151 | | - /** |
152 | | - * Return the call order, either from the Order field or the VisitNumber field. |
153 | | - * Validation ensures that one of them is set. |
154 | | - * See {@link #validate()} |
155 | | - * @return |
156 | | - */ |
157 | 155 | @Override |
158 | 156 | public int getSortOrder() { |
159 | | - return call.getOrder() != null |
160 | | - ? call.getOrder().intValueExact() |
161 | | - : call.getVisitNumber().intValueExact(); |
| 157 | + return sortOrder; |
162 | 158 | } |
163 | 159 |
|
164 | 160 | @Override |
@@ -258,31 +254,21 @@ public boolean equals(Object obj) { |
258 | 254 | final class RecordedCallWrapper implements CallWrapper { |
259 | 255 |
|
260 | 256 | private final RecordedCall call; |
| 257 | + private final int sortOrder; |
261 | 258 |
|
262 | | - private RecordedCallWrapper(RecordedCall estimatedCall) { |
263 | | - this.call = estimatedCall; |
| 259 | + private RecordedCallWrapper(RecordedCall recordedCall, int sortOrder) { |
| 260 | + this.call = recordedCall; |
| 261 | + this.sortOrder = sortOrder; |
264 | 262 | } |
265 | 263 |
|
266 | 264 | @Override |
267 | 265 | public String getStopPointRef() { |
268 | 266 | return call.getStopPointRef() != null ? call.getStopPointRef().getValue() : null; |
269 | 267 | } |
270 | 268 |
|
271 | | - @Override |
272 | | - public boolean hasOrder() { |
273 | | - return call.getOrder() != null; |
274 | | - } |
275 | | - |
276 | | - @Override |
277 | | - public boolean hasVisitNumber() { |
278 | | - return call.getVisitNumber() != null; |
279 | | - } |
280 | | - |
281 | 269 | @Override |
282 | 270 | public int getSortOrder() { |
283 | | - return call.getOrder() != null |
284 | | - ? call.getOrder().intValueExact() |
285 | | - : call.getVisitNumber().intValueExact(); |
| 271 | + return sortOrder; |
286 | 272 | } |
287 | 273 |
|
288 | 274 | @Override |
|
0 commit comments