Skip to content

Commit 0f6be1a

Browse files
committed
Fix NeTEx graph builder crash on duplicate StopPointInJourneyPattern
Add validation to reject ServiceJourneys that reference JourneyPatterns containing duplicate StopPointInJourneyPattern IDs. This prevents IllegalStateException crashes when building lookup maps. The validator runs early in the validation pipeline to catch invalid data before it causes failures in downstream validators like ServiceJourneyNonIncreasingPassingTime. Fixes crash with error: java.lang.IllegalStateException: Duplicate key SKY:StopPointInJourneyPattern:... at ServiceJourneyInfo.scheduledStopPointIdByStopPointId()
1 parent dd8052c commit 0f6be1a

3 files changed

Lines changed: 149 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.opentripplanner.netex.validation;
2+
3+
import java.util.HashSet;
4+
import java.util.Set;
5+
import org.opentripplanner.graph_builder.issue.api.DataImportIssue;
6+
import org.opentripplanner.graph_builder.issue.api.Issue;
7+
import org.rutebanken.netex.model.EntityStructure;
8+
import org.rutebanken.netex.model.JourneyPattern_VersionStructure;
9+
import org.rutebanken.netex.model.ServiceJourney;
10+
11+
/**
12+
* Validates that a JourneyPattern does not contain duplicate StopPointInJourneyPattern IDs.
13+
* Duplicate stop point IDs in a journey pattern indicate invalid NeTEx data and will cause
14+
* failures when creating lookup maps.
15+
*/
16+
class JourneyPatternDuplicateStopPoints extends AbstractHMapValidationRule<String, ServiceJourney> {
17+
18+
private String duplicateStopPointId;
19+
private String journeyPatternId;
20+
21+
@Override
22+
public Status validate(ServiceJourney sj) {
23+
journeyPatternId = sj.getJourneyPatternRef().getValue().getRef();
24+
JourneyPattern_VersionStructure journeyPattern = index
25+
.getJourneyPatternsById()
26+
.lookup(journeyPatternId);
27+
28+
Set<String> seenIds = new HashSet<>();
29+
30+
for (var point : journeyPattern
31+
.getPointsInSequence()
32+
.getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern()) {
33+
String pointId = ((EntityStructure) point).getId();
34+
if (!seenIds.add(pointId)) {
35+
duplicateStopPointId = pointId;
36+
return Status.DISCARD;
37+
}
38+
}
39+
40+
return Status.OK;
41+
}
42+
43+
@Override
44+
public DataImportIssue logMessage(String key, ServiceJourney sj) {
45+
return Issue.issue(
46+
"JourneyPatternDuplicateStopPoints",
47+
"JourneyPattern contains duplicate StopPointInJourneyPattern. " +
48+
"ServiceJourney will be skipped. " +
49+
"ServiceJourney=%s, JourneyPattern=%s, Duplicate StopPointInJourneyPattern=%s",
50+
sj.getId(),
51+
journeyPatternId,
52+
duplicateStopPointId
53+
);
54+
}
55+
}

application/src/main/java/org/opentripplanner/netex/validation/Validator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ private void run() {
3333
validate(index.quayIdByStopPointRef, new PassengerStopAssignmentQuayNotFound());
3434
validate(index.serviceJourneyById, new JourneyPatternNotFoundInSJ());
3535
validate(index.serviceJourneyById, new JourneyPatternSJMismatch());
36+
validate(index.serviceJourneyById, new JourneyPatternDuplicateStopPoints());
3637
validate(index.serviceJourneyById, ServiceJourneyNonIncreasingPassingTime::new);
3738
validate(index.datedServiceJourneys, new DSJOperatingDayNotFound());
3839
validate(index.datedServiceJourneys, new DSJServiceJourneyNotFound());
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package org.opentripplanner.netex.validation;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.opentripplanner.netex.index.api.HMapValidationRule.Status.DISCARD;
5+
import static org.opentripplanner.netex.index.api.HMapValidationRule.Status.OK;
6+
7+
import java.math.BigInteger;
8+
import org.junit.jupiter.api.Test;
9+
import org.opentripplanner.netex.index.NetexEntityIndex;
10+
import org.opentripplanner.netex.mapping.MappingSupport;
11+
import org.rutebanken.netex.model.JourneyPatternRefStructure;
12+
import org.rutebanken.netex.model.PointsInJourneyPattern_RelStructure;
13+
import org.rutebanken.netex.model.ServiceJourney;
14+
import org.rutebanken.netex.model.ServiceJourneyPattern;
15+
import org.rutebanken.netex.model.StopPointInJourneyPattern;
16+
17+
class JourneyPatternDuplicateStopPointsTest {
18+
19+
private static final String PATTERN_ID = "pattern";
20+
private static final String JOURNEY_ID = "journey";
21+
22+
@Test
23+
void noDuplicates() {
24+
var pattern = createPattern("P-1", "P-2", "P-3");
25+
26+
var index = new NetexEntityIndex();
27+
index.journeyPatternsById.add(pattern);
28+
29+
var journey = createJourney(PATTERN_ID);
30+
31+
var rule = new JourneyPatternDuplicateStopPoints();
32+
rule.setup(index.readOnlyView());
33+
34+
assertEquals(OK, rule.validate(journey));
35+
}
36+
37+
@Test
38+
void duplicateStopPoints() {
39+
var pattern = createPattern("P-1", "P-2", "P-2");
40+
41+
var index = new NetexEntityIndex();
42+
index.journeyPatternsById.add(pattern);
43+
44+
var journey = createJourney(PATTERN_ID);
45+
46+
var rule = new JourneyPatternDuplicateStopPoints();
47+
rule.setup(index.readOnlyView());
48+
49+
assertEquals(DISCARD, rule.validate(journey));
50+
}
51+
52+
@Test
53+
void duplicateStopPointsAtDifferentPositions() {
54+
var pattern = createPattern("P-1", "P-2", "P-3", "P-1");
55+
56+
var index = new NetexEntityIndex();
57+
index.journeyPatternsById.add(pattern);
58+
59+
var journey = createJourney(PATTERN_ID);
60+
61+
var rule = new JourneyPatternDuplicateStopPoints();
62+
rule.setup(index.readOnlyView());
63+
64+
assertEquals(DISCARD, rule.validate(journey));
65+
}
66+
67+
private ServiceJourneyPattern createPattern(String... pointIds) {
68+
var pattern = new ServiceJourneyPattern();
69+
pattern.setId(PATTERN_ID);
70+
71+
var points = new PointsInJourneyPattern_RelStructure();
72+
int order = 1;
73+
for (String pointId : pointIds) {
74+
var point = new StopPointInJourneyPattern();
75+
point.setId(pointId);
76+
point.setOrder(BigInteger.valueOf(order++));
77+
points
78+
.getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern()
79+
.add(point);
80+
}
81+
82+
pattern.setPointsInSequence(points);
83+
return pattern;
84+
}
85+
86+
private ServiceJourney createJourney(String patternId) {
87+
var journey = new ServiceJourney();
88+
journey.setId(JOURNEY_ID);
89+
var ref = MappingSupport.createWrappedRef(patternId, JourneyPatternRefStructure.class);
90+
journey.withJourneyPatternRef(ref);
91+
return journey;
92+
}
93+
}

0 commit comments

Comments
 (0)