Skip to content

Commit 1b8da58

Browse files
committed
Fix: Regression error in passThroughPoints in the Transmodel API
The list of ids inside passThroughPoints is allowed to be empty or null. We cannot change this - that would be a breaking change. So, when the via search enforced this, the API was not backward compatible anymore. This commit reverts the behavior and just ignores the passThroughPoints if the list of ids is null or empty. This bug was introduced in PR #6084.
1 parent 8e36ae2 commit 1b8da58

File tree

6 files changed

+144
-23
lines changed

6 files changed

+144
-23
lines changed

application/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapper.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import java.util.Collection;
77
import java.util.List;
88
import java.util.Map;
9+
import java.util.Objects;
10+
import javax.annotation.Nullable;
911
import org.opentripplanner.apis.transmodel.model.plan.TripQuery;
1012
import org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType;
1113
import org.opentripplanner.apis.transmodel.support.OneOfInputValidator;
@@ -30,6 +32,7 @@ static List<ViaLocation> toLegacyPassThroughLocations(
3032
return passThroughPoints
3133
.stream()
3234
.map(TripViaLocationMapper::mapLegacyPassThroughViaLocation)
35+
.filter(Objects::nonNull)
3336
.collect(toList());
3437
}
3538

@@ -65,19 +68,31 @@ private static PassThroughViaLocation mapPassThroughViaLocation(Map<String, Obje
6568

6669
private static List<FeedScopedId> mapStopLocationIds(Map<String, Object> map) {
6770
var c = (Collection<String>) map.get(ViaLocationInputType.FIELD_STOP_LOCATION_IDS);
71+
72+
// When coordinates are added, we need to accept null here...
73+
if (c == null) {
74+
throw new IllegalArgumentException(
75+
"'" + ViaLocationInputType.FIELD_STOP_LOCATION_IDS + "' is not set!"
76+
);
77+
}
6878
return c.stream().map(TransitIdMapper::mapIDToDomain).toList();
6979
}
7080

7181
/**
7282
* @deprecated Legacy passThrough, use via instead
7383
*/
7484
@Deprecated
85+
@Nullable
7586
private static ViaLocation mapLegacyPassThroughViaLocation(Map<String, Object> inputMap) {
7687
final String name = (String) inputMap.get("name");
77-
final List<FeedScopedId> stopLocationIds =
78-
((List<String>) inputMap.get("placeIds")).stream()
79-
.map(TransitIdMapper::mapIDToDomain)
80-
.toList();
88+
List<String> placeIds = (List<String>) inputMap.get("placeIds");
89+
if (placeIds == null || placeIds.isEmpty()) {
90+
return null;
91+
}
92+
final List<FeedScopedId> stopLocationIds = placeIds
93+
.stream()
94+
.map(TransitIdMapper::mapIDToDomain)
95+
.toList();
8196
return new PassThroughViaLocation(name, stopLocationIds);
8297
}
8398
}

application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaLocationInputType.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import graphql.language.StringValue;
77
import graphql.schema.GraphQLInputObjectType;
8+
import graphql.schema.GraphQLInputType;
89
import graphql.schema.GraphQLList;
910
import graphql.schema.GraphQLNonNull;
1011
import java.time.Duration;
@@ -84,7 +85,7 @@ be accepted. To visit a coordinate, the traveler must walk(bike or drive) to the
8485
b
8586
.name(FIELD_STOP_LOCATION_IDS)
8687
.description(DOC_STOP_LOCATION_IDS)
87-
.type(gqlListOfNonNullStrings())
88+
.type(requiredListOfNonNullStrings())
8889
)
8990
/*
9091
TODO: Add support for coordinates
@@ -101,7 +102,7 @@ be accepted. To visit a coordinate, the traveler must walk(bike or drive) to the
101102
b
102103
.name(FIELD_STOP_LOCATION_IDS)
103104
.description(DOC_STOP_LOCATION_IDS)
104-
.type(gqlListOfNonNullStrings())
105+
.type(requiredListOfNonNullStrings())
105106
)
106107
.build();
107108

@@ -119,7 +120,7 @@ be accepted. To visit a coordinate, the traveler must walk(bike or drive) to the
119120
)
120121
.build();
121122

122-
private static GraphQLList gqlListOfNonNullStrings() {
123-
return new GraphQLList(new GraphQLNonNull(GraphQLString));
123+
private static GraphQLInputType requiredListOfNonNullStrings() {
124+
return new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString)));
124125
}
125126
}

application/src/main/java/org/opentripplanner/routing/api/request/via/PassThroughViaLocation.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public PassThroughViaLocation(@Nullable String label, Collection<FeedScopedId> s
1616
super(label, stopLocationIds);
1717
if (stopLocationIds.isEmpty()) {
1818
throw new IllegalArgumentException(
19-
"A pass through via location must have at least one stop location. Label: " + label
19+
"A pass through via location must have at least one stop location." +
20+
(label == null ? "" : " Label: " + label)
2021
);
2122
}
2223
}

application/src/main/java/org/opentripplanner/routing/api/request/via/VisitViaLocation.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ public VisitViaLocation(
4141

4242
if (stopLocationIds().isEmpty() && coordinates().isEmpty()) {
4343
throw new IllegalArgumentException(
44-
"A via location must have at least one stop location or a coordinate. Label: " + label
44+
"A via location must have at least one stop location or a coordinate." +
45+
(label == null ? "" : " Label: " + label)
4546
);
4647
}
4748
}

application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,7 +2204,7 @@ input TripPassThroughViaLocationInput {
22042204
stop place or a group of stop places. It is enough to visit ONE of the locations
22052205
listed.
22062206
"""
2207-
stopLocationIds: [String!]
2207+
stopLocationIds: [String!]!
22082208
}
22092209

22102210
"""
@@ -2239,7 +2239,7 @@ input TripVisitViaLocationInput {
22392239
stop place or a group of stop places. It is enough to visit ONE of the locations
22402240
listed.
22412241
"""
2242-
stopLocationIds: [String!]
2242+
stopLocationIds: [String!]!
22432243
}
22442244

22452245
"Input format for specifying a location through either a place reference (id), coordinates or both. If both place and coordinates are provided the place ref will be used if found, coordinates will only be used if place is not known. The location also contain information about the minimum and maximum time the user is willing to stay at the via location."

application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapperTest.java

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@
2121

2222
class TripViaLocationMapperTest {
2323

24-
public static final String LABEL = "TestLabel";
25-
public static final Duration MIN_WAIT_TIME = Duration.ofMinutes(5);
26-
public static final List<String> LIST_IDS_INPUT = List.of("F:ID1", "F:ID2");
27-
public static final String EXPECTED_IDS_AS_STRING = "[F:ID1, F:ID2]";
24+
private static final String LABEL = "TestLabel";
25+
private static final Duration MIN_WAIT_TIME = Duration.ofMinutes(5);
26+
private static final List<String> LIST_IDS_INPUT = List.of("F:ID1", "F:ID2");
27+
private static final String EXPECTED_IDS_AS_STRING = "[F:ID1, F:ID2]";
28+
private static final String REASON_EMPTY_IDS_ALLOWED_PASS_THROUGH =
29+
"""
30+
Unfortunately the 'placeIds' is not required. Making it required would be a breaking change,
31+
so wee just ignore it."
32+
""";
2833

2934
@BeforeEach
3035
void setup() {
@@ -52,10 +57,7 @@ void testMapToVisitViaLocations() {
5257

5358
@Test
5459
void testMapToVisitViaLocationsWithBareMinimum() {
55-
Map<String, Object> input = Map.of(
56-
FIELD_VISIT,
57-
Map.of(FIELD_STOP_LOCATION_IDS, List.of("F:1"))
58-
);
60+
Map<String, Object> input = mapOf(FIELD_VISIT, mapOf(FIELD_STOP_LOCATION_IDS, List.of("F:1")));
5961
var result = TripViaLocationMapper.mapToViaLocations(List.of(input));
6062

6163
var via = result.getFirst();
@@ -66,9 +68,32 @@ void testMapToVisitViaLocationsWithBareMinimum() {
6668
assertFalse(via.isPassThroughLocation());
6769
}
6870

71+
@Test
72+
void testMapToVisitViaLocationsWithoutIds() {
73+
Map<String, Object> input = mapOf(FIELD_VISIT, mapOf(FIELD_STOP_LOCATION_IDS, null));
74+
var ex = assertThrows(
75+
IllegalArgumentException.class,
76+
() -> TripViaLocationMapper.mapToViaLocations(List.of(input))
77+
);
78+
assertEquals("'stopLocationIds' is not set!", ex.getMessage());
79+
}
80+
81+
@Test
82+
void testMapToVisitViaLocationsWithAnEmptyListOfIds() {
83+
Map<String, Object> input = mapOf(FIELD_VISIT, mapOf(FIELD_STOP_LOCATION_IDS, List.of()));
84+
var ex = assertThrows(
85+
IllegalArgumentException.class,
86+
() -> TripViaLocationMapper.mapToViaLocations(List.of(input))
87+
);
88+
assertEquals(
89+
"A via location must have at least one stop location or a coordinate.",
90+
ex.getMessage()
91+
);
92+
}
93+
6994
@Test
7095
void tetMapToPassThrough() {
71-
Map<String, Object> input = Map.of(FIELD_PASS_THROUGH, passThroughInput(LABEL, LIST_IDS_INPUT));
96+
Map<String, Object> input = mapOf(FIELD_PASS_THROUGH, passThroughInput(LABEL, LIST_IDS_INPUT));
7297
var result = TripViaLocationMapper.mapToViaLocations(List.of(input));
7398
var via = result.getFirst();
7499

@@ -83,9 +108,9 @@ void tetMapToPassThrough() {
83108

84109
@Test
85110
void tetMapToPassThroughWithBareMinimum() {
86-
Map<String, Object> input = Map.of(
111+
Map<String, Object> input = mapOf(
87112
FIELD_PASS_THROUGH,
88-
Map.of(FIELD_STOP_LOCATION_IDS, List.of("F:1"))
113+
mapOf(FIELD_STOP_LOCATION_IDS, List.of("F:1"))
89114
);
90115
var result = TripViaLocationMapper.mapToViaLocations(List.of(input));
91116
var via = result.getFirst();
@@ -95,6 +120,32 @@ void tetMapToPassThroughWithBareMinimum() {
95120
assertTrue(via.isPassThroughLocation());
96121
}
97122

123+
@Test
124+
void tetMapToPassThroughWithoutIds() {
125+
Map<String, Object> input = mapOf(FIELD_PASS_THROUGH, mapOf(FIELD_STOP_LOCATION_IDS, null));
126+
var ex = assertThrows(
127+
IllegalArgumentException.class,
128+
() -> TripViaLocationMapper.mapToViaLocations(List.of(input))
129+
);
130+
assertEquals("'stopLocationIds' is not set!", ex.getMessage());
131+
}
132+
133+
@Test
134+
void testMapToPassThroughWithAnEmptyListOfIds() {
135+
Map<String, Object> input = mapOf(
136+
FIELD_PASS_THROUGH,
137+
mapOf(FIELD_STOP_LOCATION_IDS, List.of())
138+
);
139+
var ex = assertThrows(
140+
IllegalArgumentException.class,
141+
() -> TripViaLocationMapper.mapToViaLocations(List.of(input))
142+
);
143+
assertEquals(
144+
"A pass through via location must have at least one stop location.",
145+
ex.getMessage()
146+
);
147+
}
148+
98149
@Test
99150
void testOneOf() {
100151
Map<String, Object> input = Map.ofEntries(
@@ -121,6 +172,48 @@ void testOneOf() {
121172
);
122173
}
123174

175+
@Test
176+
void testToLegacyPassThroughLocations() {
177+
Map<String, Object> input = Map.of("name", LABEL, "placeIds", LIST_IDS_INPUT);
178+
var result = TripViaLocationMapper.toLegacyPassThroughLocations(List.of(input));
179+
var via = result.getFirst();
180+
181+
assertEquals(LABEL, via.label());
182+
assertEquals(EXPECTED_IDS_AS_STRING, via.stopLocationIds().toString());
183+
assertTrue(via.isPassThroughLocation());
184+
assertEquals(
185+
"PassThroughViaLocation{label: TestLabel, stopLocationIds: [F:ID1, F:ID2]}",
186+
via.toString()
187+
);
188+
}
189+
190+
@Test
191+
void testToLegacyPassThroughLocationsWithBareMinimum() {
192+
Map<String, Object> input = mapOf("placeIds", LIST_IDS_INPUT);
193+
var result = TripViaLocationMapper.toLegacyPassThroughLocations(List.of(input));
194+
var via = result.getFirst();
195+
196+
assertNull(via.label());
197+
assertEquals(EXPECTED_IDS_AS_STRING, via.stopLocationIds().toString());
198+
assertTrue(via.isPassThroughLocation());
199+
assertEquals("PassThroughViaLocation{stopLocationIds: [F:ID1, F:ID2]}", via.toString());
200+
}
201+
202+
@Test
203+
void testToLegacyPassThroughLocationsWithoutIds() {
204+
var result = TripViaLocationMapper.toLegacyPassThroughLocations(
205+
List.of(mapOf("placeIds", null))
206+
);
207+
assertTrue(result.isEmpty(), REASON_EMPTY_IDS_ALLOWED_PASS_THROUGH);
208+
}
209+
210+
@Test
211+
void testToLegacyPassThroughLocationsWithEmptyList() {
212+
Map<String, Object> input = Map.ofEntries(entry("name", LABEL), entry("placeIds", List.of()));
213+
var result = TripViaLocationMapper.toLegacyPassThroughLocations(List.of(input));
214+
assertTrue(result.isEmpty(), REASON_EMPTY_IDS_ALLOWED_PASS_THROUGH);
215+
}
216+
124217
private Map<String, Object> visitInput(String label, Duration minWaitTime, List<String> ids) {
125218
var map = new HashMap<String, Object>();
126219
if (label != null) {
@@ -138,4 +231,14 @@ private Map<String, Object> visitInput(String label, Duration minWaitTime, List<
138231
private Map<String, Object> passThroughInput(String label, List<String> ids) {
139232
return visitInput(label, null, ids);
140233
}
234+
235+
/**
236+
* Create a new HashMap with the {@code key} and {@code value}, the value may be {@code null}.
237+
* The {@link Map#of(Object, Object)} does not support {@code null} values.
238+
*/
239+
private static Map<String, Object> mapOf(String key, Object value) {
240+
var map = new HashMap<String, Object>();
241+
map.put(key, value);
242+
return map;
243+
}
141244
}

0 commit comments

Comments
 (0)