org.zeroturnaround
zt-zip
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java b/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java
index e1fb7fb4..fe6c22d7 100644
--- a/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java
@@ -1,8 +1,8 @@
package org.entur.netex.validation.validator.jaxb;
+import jakarta.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
-import javax.annotation.Nullable;
import org.entur.netex.index.api.NetexEntitiesIndex;
import org.entur.netex.validation.validator.DataLocation;
import org.entur.netex.validation.validator.ValidationContext;
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java b/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java
index 7e1f7c52..5b1862d6 100644
--- a/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java
@@ -1,6 +1,6 @@
package org.entur.netex.validation.validator.jaxb;
-import javax.annotation.Nullable;
+import jakarta.annotation.Nullable;
import org.entur.netex.index.api.NetexEntitiesIndex;
import org.entur.netex.validation.validator.model.QuayCoordinates;
import org.entur.netex.validation.validator.model.QuayId;
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java b/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java
index 15921381..8664559f 100644
--- a/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java
@@ -15,7 +15,7 @@
package org.entur.netex.validation.validator.jaxb;
-import javax.annotation.Nullable;
+import jakarta.annotation.Nullable;
import org.entur.netex.validation.validator.model.QuayCoordinates;
import org.entur.netex.validation.validator.model.QuayId;
import org.entur.netex.validation.validator.model.StopPlaceId;
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/AbstractStopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/AbstractStopTime.java
new file mode 100644
index 00000000..f6a532ef
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/AbstractStopTime.java
@@ -0,0 +1,83 @@
+package org.entur.netex.validation.validator.jaxb.model.stoptime;
+
+import java.math.BigInteger;
+import java.time.LocalTime;
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.rutebanken.netex.model.TimetabledPassingTime;
+
+/**
+ * Wrapper around {@link TimetabledPassingTime} that provides a simpler interface for passing times
+ * comparison. Passing times are exposed as seconds since midnight, taking into account the day
+ * offset.
+ *
+ * This class does not take Daylight Saving Time transitions into account, this is an error and
+ * should be fixed. See https://github.com/opentripplanner/OpenTripPlanner/issues/5109
+ */
+abstract sealed class AbstractStopTime
+ implements StopTime
+ permits FlexibleStopTime, RegularStopTime {
+
+ private final ScheduledStopPointId scheduledStopPointId;
+ private final TimetabledPassingTime timetabledPassingTime;
+
+ protected AbstractStopTime(
+ ScheduledStopPointId scheduledStopPointId,
+ TimetabledPassingTime timetabledPassingTime
+ ) {
+ this.scheduledStopPointId = scheduledStopPointId;
+ this.timetabledPassingTime = timetabledPassingTime;
+ }
+
+ @Override
+ public ScheduledStopPointId scheduledStopPointId() {
+ return scheduledStopPointId;
+ }
+
+ protected LocalTime arrivalTime() {
+ return timetabledPassingTime.getArrivalTime();
+ }
+
+ protected BigInteger arrivalDayOffset() {
+ return timetabledPassingTime.getArrivalDayOffset();
+ }
+
+ protected LocalTime latestArrivalTime() {
+ return timetabledPassingTime.getLatestArrivalTime();
+ }
+
+ protected BigInteger latestArrivalDayOffset() {
+ return timetabledPassingTime.getLatestArrivalDayOffset();
+ }
+
+ protected LocalTime departureTime() {
+ return timetabledPassingTime.getDepartureTime();
+ }
+
+ protected BigInteger departureDayOffset() {
+ return timetabledPassingTime.getDepartureDayOffset();
+ }
+
+ protected LocalTime earliestDepartureTime() {
+ return timetabledPassingTime.getEarliestDepartureTime();
+ }
+
+ protected BigInteger earliestDepartureDayOffset() {
+ return timetabledPassingTime.getEarliestDepartureDayOffset();
+ }
+
+ protected boolean isRegularStopFollowedByAreaStopValid(
+ FlexibleStopTime next
+ ) {
+ return (
+ normalizedDepartureTimeOrElseArrivalTime() <=
+ next.normalizedEarliestDepartureTime()
+ );
+ }
+
+ protected boolean isAreaStopFollowedByRegularStopValid(RegularStopTime next) {
+ return (
+ normalizedLatestArrivalTime() <=
+ next.normalizedArrivalTimeOrElseDepartureTime()
+ );
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/FlexibleStopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/FlexibleStopTime.java
new file mode 100644
index 00000000..16599655
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/FlexibleStopTime.java
@@ -0,0 +1,118 @@
+package org.entur.netex.validation.validator.jaxb.model.stoptime;
+
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.rutebanken.netex.model.TimetabledPassingTime;
+
+/**
+ * Wrapper around {@link TimetabledPassingTime} that provides a simpler interface
+ * for passing times comparison.
+ * Passing times are exposed as seconds since midnight, taking into account the day offset.
+ */
+final class FlexibleStopTime extends AbstractStopTime {
+
+ FlexibleStopTime(
+ ScheduledStopPointId scheduledStopPointId,
+ TimetabledPassingTime timetabledPassingTime
+ ) {
+ super(scheduledStopPointId, timetabledPassingTime);
+ }
+
+ @Override
+ public boolean isComplete() {
+ return hasLatestArrivalTime() && hasEarliestDepartureTime();
+ }
+
+ @Override
+ public boolean isConsistent() {
+ return (
+ !isComplete() ||
+ normalizedLatestArrivalTime() >= normalizedEarliestDepartureTime()
+ );
+ }
+
+ @Override
+ public boolean isStopTimesIncreasing(StopTime next) {
+ if (next instanceof RegularStopTime regularStopTime) {
+ return isAreaStopFollowedByRegularStopValid(regularStopTime);
+ }
+ return isAreaStopFollowedByAreaStopValid((FlexibleStopTime) next);
+ }
+
+ @Override
+ public int getStopTimeDiff(StopTime given) {
+ // TODO: This should be fixed. We need to take into account the type of given.
+ // Is it the same type as this, or not. See how we have done in
+ // isRegularStopFollowedByRegularStopValid, isAreaStopFollowedByAreaStopValid,
+ // isRegularStopFollowedByAreaStopValid, isAreaStopFollowedByRegularStopValid
+
+ if (given instanceof FlexibleStopTime) {
+ return isComplete()
+ ? normalizedEarliestDepartureTime() - normalizedLatestArrivalTime()
+ : 0;
+ }
+ return (
+ given.normalizedEarliestDepartureTime() -
+ normalizedArrivalTimeOrElseDepartureTime()
+ );
+ }
+
+ @Override
+ public int normalizedEarliestDepartureTime() {
+ return elapsedTimeSinceMidnight(
+ earliestDepartureTime(),
+ earliestDepartureDayOffset()
+ );
+ }
+
+ @Override
+ public int normalizedLatestArrivalTime() {
+ return elapsedTimeSinceMidnight(
+ latestArrivalTime(),
+ latestArrivalDayOffset()
+ );
+ }
+
+ @Override
+ public int normalizedDepartureTimeOrElseArrivalTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int normalizedArrivalTimeOrElseDepartureTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ private boolean hasLatestArrivalTime() {
+ return latestArrivalTime() != null;
+ }
+
+ private boolean hasEarliestDepartureTime() {
+ return earliestDepartureTime() != null;
+ }
+
+ private boolean isAreaStopFollowedByAreaStopValid(FlexibleStopTime next) {
+ int earliestDepartureTime = normalizedEarliestDepartureTime();
+ int nextEarliestDepartureTime = next.normalizedEarliestDepartureTime();
+ int latestArrivalTime = normalizedLatestArrivalTime();
+ int nextLatestArrivalTime = next.normalizedLatestArrivalTime();
+
+ return (
+ earliestDepartureTime <= nextEarliestDepartureTime &&
+ latestArrivalTime <= nextLatestArrivalTime
+ );
+ }
+
+ @Override
+ public boolean isArrivalInMinutesResolution() {
+ return hasLatestArrivalTime()
+ ? latestArrivalTime().getSecond() == 0
+ : earliestDepartureTime().getSecond() == 0;
+ }
+
+ @Override
+ public boolean isDepartureInMinutesResolution() {
+ return hasEarliestDepartureTime()
+ ? earliestDepartureTime().getSecond() == 0
+ : latestArrivalTime().getSecond() == 0;
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/RegularStopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/RegularStopTime.java
new file mode 100644
index 00000000..b634a94d
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/RegularStopTime.java
@@ -0,0 +1,131 @@
+package org.entur.netex.validation.validator.jaxb.model.stoptime;
+
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.rutebanken.netex.model.TimetabledPassingTime;
+
+/**
+ * Wrapper around {@link TimetabledPassingTime} that provides a simpler interface
+ * for passing times comparison.
+ * Passing times are exposed as seconds since midnight, taking into account the day offset.
+ */
+final class RegularStopTime extends AbstractStopTime {
+
+ RegularStopTime(
+ ScheduledStopPointId scheduledStopPointId,
+ TimetabledPassingTime timetabledPassingTime
+ ) {
+ super(scheduledStopPointId, timetabledPassingTime);
+ }
+
+ @Override
+ public boolean isComplete() {
+ return hasArrivalTime() || hasDepartureTime();
+ }
+
+ @Override
+ public boolean isConsistent() {
+ return (
+ arrivalTime() == null ||
+ departureTime() == null ||
+ normalizedDepartureTime() >= normalizedArrivalTime()
+ );
+ }
+
+ @Override
+ public boolean isStopTimesIncreasing(StopTime next) {
+ if (next instanceof RegularStopTime regularStopTime) {
+ return isRegularStopFollowedByRegularStopValid(regularStopTime);
+ }
+ return isRegularStopFollowedByAreaStopValid((FlexibleStopTime) next);
+ }
+
+ @Override
+ public int getStopTimeDiff(StopTime given) {
+ // TODO: This should be fixed. We need to take into account the type of given.
+ // Is it the same type as this, or not. See how we have done in
+ // isRegularStopFollowedByRegularStopValid, isAreaStopFollowedByAreaStopValid,
+ // isRegularStopFollowedByAreaStopValid, isAreaStopFollowedByRegularStopValid
+
+ if (given instanceof RegularStopTime) {
+ return (
+ given.normalizedArrivalTimeOrElseDepartureTime() -
+ normalizedDepartureTimeOrElseArrivalTime()
+ );
+ }
+ return (
+ given.normalizedLatestArrivalTime() -
+ normalizedDepartureTimeOrElseArrivalTime()
+ );
+ }
+
+ @Override
+ public int normalizedEarliestDepartureTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int normalizedLatestArrivalTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int normalizedDepartureTimeOrElseArrivalTime() {
+ return hasDepartureTime()
+ ? normalizedDepartureTime()
+ : normalizedArrivalTime();
+ }
+
+ @Override
+ public int normalizedArrivalTimeOrElseDepartureTime() {
+ return hasArrivalTime()
+ ? normalizedArrivalTime()
+ : normalizedDepartureTime();
+ }
+
+ /**
+ * Return the elapsed time in second between midnight and the departure time, taking into account
+ * the day offset.
+ */
+ private int normalizedDepartureTime() {
+ return elapsedTimeSinceMidnight(departureTime(), departureDayOffset());
+ }
+
+ /**
+ * Return the elapsed time in second between midnight and the arrival time, taking into account
+ * the day offset.
+ */
+ private int normalizedArrivalTime() {
+ return elapsedTimeSinceMidnight(arrivalTime(), arrivalDayOffset());
+ }
+
+ private boolean hasArrivalTime() {
+ return arrivalTime() != null;
+ }
+
+ private boolean hasDepartureTime() {
+ return departureTime() != null;
+ }
+
+ private boolean isRegularStopFollowedByRegularStopValid(
+ RegularStopTime next
+ ) {
+ return (
+ normalizedDepartureTimeOrElseArrivalTime() <=
+ next.normalizedArrivalTimeOrElseDepartureTime()
+ );
+ }
+
+ @Override
+ public boolean isArrivalInMinutesResolution() {
+ return hasArrivalTime()
+ ? arrivalTime().getSecond() == 0
+ : departureTime().getSecond() == 0;
+ }
+
+ @Override
+ public boolean isDepartureInMinutesResolution() {
+ return hasDepartureTime()
+ ? departureTime().getSecond() == 0
+ : arrivalTime().getSecond() == 0;
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/SortStopTimesUtil.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/SortStopTimesUtil.java
new file mode 100644
index 00000000..2b6f84bf
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/SortStopTimesUtil.java
@@ -0,0 +1,121 @@
+package org.entur.netex.validation.validator.jaxb.model.stoptime;
+
+import static java.util.Comparator.comparing;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.entur.netex.index.api.NetexEntitiesIndex;
+import org.entur.netex.validation.validator.jaxb.JAXBValidationContext;
+import org.entur.netex.validation.validator.jaxb.support.NetexUtils;
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.rutebanken.netex.model.EntityStructure;
+import org.rutebanken.netex.model.JourneyPattern;
+import org.rutebanken.netex.model.ServiceJourney;
+import org.rutebanken.netex.model.StopPointInJourneyPatternRefStructure;
+import org.rutebanken.netex.model.TimetabledPassingTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This utility class is used to sort the timetabled passing times of a service journey according to
+ * their order in the journey pattern.
+ * The order of the passing times is determined by the order of the stop points in the journey pattern.
+ * The passing times are sorted by their order in the journey pattern, and warped in a StopTime object.
+ */
+public final class SortStopTimesUtil {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(
+ SortStopTimesUtil.class
+ );
+
+ /**
+ * Prevent instantiation of this utility class.
+ */
+ private SortStopTimesUtil() {}
+
+ /**
+ * Sort the timetabled passing times according to their order in the journey pattern.
+ */
+ public static List getSortedStopTimes(
+ ServiceJourney serviceJourney,
+ JAXBValidationContext validationContext
+ ) {
+ JourneyPattern journeyPattern = validationContext.journeyPattern(
+ serviceJourney
+ );
+
+ if (journeyPattern == null) {
+ LOGGER.debug(
+ "No journey pattern ref found on service journey {}",
+ serviceJourney.getId()
+ );
+ return List.of();
+ }
+
+ Map stopPointIdToOrder = getStopPointIdsOrder(
+ journeyPattern
+ );
+
+ Map scheduledStopPointIdByStopPointId =
+ NetexUtils.scheduledStopPointIdByStopPointId(journeyPattern);
+
+ return validationContext
+ .timetabledPassingTimes(serviceJourney)
+ .stream()
+ .filter(SortStopTimesUtil::hasStopPointInJourneyPatternRef)
+ .sorted(
+ comparing(timetabledPassingTime ->
+ stopPointIdToOrder.get(NetexUtils.stopPointRef(timetabledPassingTime))
+ )
+ )
+ .map(timetabledPassingTime ->
+ StopTime.of(
+ scheduledStopPointIdByStopPointId.get(
+ NetexUtils.stopPointRef(timetabledPassingTime)
+ ),
+ timetabledPassingTime,
+ hasFlexibleStopPoint(
+ validationContext.getNetexEntitiesIndex(),
+ scheduledStopPointIdByStopPointId.get(
+ NetexUtils.stopPointRef(timetabledPassingTime)
+ )
+ )
+ )
+ )
+ .toList();
+ }
+
+ private static boolean hasStopPointInJourneyPatternRef(
+ TimetabledPassingTime timetabledPassingTime
+ ) {
+ return (
+ timetabledPassingTime
+ .getPointInJourneyPatternRef()
+ .getValue() instanceof StopPointInJourneyPatternRefStructure
+ );
+ }
+
+ private static Map getStopPointIdsOrder(
+ JourneyPattern journeyPattern
+ ) {
+ return NetexUtils
+ .stopPointsInJourneyPattern(journeyPattern)
+ .stream()
+ .collect(
+ Collectors.toMap(
+ EntityStructure::getId,
+ point -> point.getOrder().intValueExact()
+ )
+ );
+ }
+
+ private static boolean hasFlexibleStopPoint(
+ NetexEntitiesIndex netexEntitiesIndex,
+ ScheduledStopPointId scheduledStopPointId
+ ) {
+ return netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .containsKey(scheduledStopPointId.id());
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/StopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/StopTime.java
new file mode 100644
index 00000000..1eb7cc96
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/StopTime.java
@@ -0,0 +1,88 @@
+package org.entur.netex.validation.validator.jaxb.model.stoptime;
+
+import java.math.BigInteger;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.util.Objects;
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.rutebanken.netex.model.TimetabledPassingTime;
+
+public sealed interface StopTime permits AbstractStopTime {
+ static StopTime of(
+ ScheduledStopPointId scheduledStopPointId,
+ TimetabledPassingTime timetabledPassingTime,
+ boolean stopIsFlexibleArea
+ ) {
+ return stopIsFlexibleArea
+ ? new FlexibleStopTime(scheduledStopPointId, timetabledPassingTime)
+ : new RegularStopTime(scheduledStopPointId, timetabledPassingTime);
+ }
+
+ /**
+ * A passing time on a regular stop is complete if either arrival or departure time is present. A
+ * passing time on an area stop is complete if both earliest departure time and latest arrival
+ * time are present.
+ */
+ boolean isComplete();
+
+ /**
+ * A passing time on a regular stop is consistent if departure time is after arrival time. A
+ * passing time on an area stop is consistent if latest arrival time is after earliest departure
+ * time.
+ */
+ boolean isConsistent();
+
+ /**
+ * Return the elapsed time in second between midnight and the earliest departure time, taking into
+ * account the day offset. Only valid for area-stops, throw an exception if not.
+ */
+ int normalizedEarliestDepartureTime();
+
+ /**
+ * Return the elapsed time in second between midnight and the latest arrival time, taking into
+ * account the day offset. Only valid for area-stops, throw an exception if not.
+ */
+ int normalizedLatestArrivalTime();
+
+ /**
+ * Return the elapsed time in second between midnight and the departure time, taking into account
+ * the day offset. Fallback to arrival time if departure time is missing.
+ */
+ int normalizedDepartureTimeOrElseArrivalTime();
+
+ /**
+ * Return the elapsed time in second between midnight and the arrival time, taking into account
+ * the day offset. Fallback to departure time if arrival time is missing.
+ */
+ int normalizedArrivalTimeOrElseDepartureTime();
+
+ ScheduledStopPointId scheduledStopPointId();
+
+ /**
+ * Return {@code true} if this stop-time is before or equal to the given {@code next} stop time.
+ */
+ boolean isStopTimesIncreasing(StopTime next);
+
+ /**
+ * Return time between this and given time values with offset handling.
+ */
+ int getStopTimeDiff(StopTime given);
+
+ boolean isDepartureInMinutesResolution();
+
+ boolean isArrivalInMinutesResolution();
+
+ /**
+ * Return the elapsed time in second since midnight for a given local time, taking into account
+ * the day offset.
+ */
+ default int elapsedTimeSinceMidnight(LocalTime time, BigInteger dayOffset) {
+ Objects.requireNonNull(time);
+
+ int intOffsetValue = dayOffset != null ? dayOffset.intValueExact() : 0;
+ return (int) Duration
+ .between(LocalTime.MIDNIGHT, time)
+ .plus(Duration.ofDays(intOffsetValue))
+ .toSeconds();
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidator.java b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidator.java
new file mode 100644
index 00000000..7c73e5f2
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidator.java
@@ -0,0 +1,128 @@
+package org.entur.netex.validation.validator.jaxb.rules.servicejourney.passingtime;
+
+import jakarta.annotation.Nullable;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.entur.netex.validation.validator.Severity;
+import org.entur.netex.validation.validator.ValidationIssue;
+import org.entur.netex.validation.validator.ValidationRule;
+import org.entur.netex.validation.validator.jaxb.JAXBValidationContext;
+import org.entur.netex.validation.validator.jaxb.JAXBValidator;
+import org.entur.netex.validation.validator.jaxb.model.stoptime.SortStopTimesUtil;
+import org.entur.netex.validation.validator.jaxb.model.stoptime.StopTime;
+import org.rutebanken.netex.model.ServiceJourney;
+
+/**
+ * Validates that the passing times of a service journey are non-decreasing.
+ * This means that the time between each stop must be greater than or equal to zero.
+ */
+public class NonIncreasingPassingTimeValidator implements JAXBValidator {
+
+ static final ValidationRule RULE_NON_INCREASING_TIME = new ValidationRule(
+ "TIMETABLED_PASSING_TIME_NON_INCREASING_TIME",
+ "ServiceJourney has non-increasing TimetabledPassingTime",
+ "ServiceJourney has non-increasing TimetabledPassingTime at: %s",
+ Severity.ERROR
+ );
+
+ static final ValidationRule RULE_INCOMPLETE_TIME = new ValidationRule(
+ "TIMETABLED_PASSING_TIME_INCOMPLETE_TIME",
+ "ServiceJourney has incomplete TimetabledPassingTime",
+ "ServiceJourney has incomplete TimetabledPassingTime at: %s",
+ Severity.ERROR
+ );
+
+ static final ValidationRule RULE_INCONSISTENT_TIME = new ValidationRule(
+ "TIMETABLED_PASSING_TIME_INCONSISTENT_TIME",
+ "ServiceJourney has inconsistent TimetabledPassingTime",
+ "ServiceJourney has inconsistent TimetabledPassingTime at: %s",
+ Severity.ERROR
+ );
+
+ @Override
+ public List validate(
+ JAXBValidationContext validationContext
+ ) {
+ return validationContext
+ .serviceJourneys()
+ .stream()
+ .map(serviceJourney ->
+ validateServiceJourney(serviceJourney, validationContext)
+ )
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ @Override
+ public Set getRules() {
+ return Set.of(RULE_NON_INCREASING_TIME);
+ }
+
+ @Nullable
+ public ValidationIssue validateServiceJourney(
+ ServiceJourney serviceJourney,
+ JAXBValidationContext validationContext
+ ) {
+ List sortedTimetabledPassingTime =
+ SortStopTimesUtil.getSortedStopTimes(serviceJourney, validationContext);
+ var previousPassingTime = sortedTimetabledPassingTime.get(0);
+ ValidationIssue issueOnFirstStop = validateStopTime(
+ serviceJourney,
+ validationContext,
+ previousPassingTime
+ );
+ if (issueOnFirstStop != null) {
+ return issueOnFirstStop;
+ }
+
+ for (int i = 1; i < sortedTimetabledPassingTime.size(); i++) {
+ var currentPassingTime = sortedTimetabledPassingTime.get(i);
+
+ ValidationIssue issue = validateStopTime(
+ serviceJourney,
+ validationContext,
+ currentPassingTime
+ );
+ if (issue != null) {
+ return issue;
+ }
+
+ if (!previousPassingTime.isStopTimesIncreasing(currentPassingTime)) {
+ return new ValidationIssue(
+ RULE_NON_INCREASING_TIME,
+ validationContext.dataLocation(serviceJourney.getId()),
+ validationContext.stopPointName(
+ previousPassingTime.scheduledStopPointId()
+ )
+ );
+ }
+
+ previousPassingTime = currentPassingTime;
+ }
+ return null;
+ }
+
+ @Nullable
+ private static ValidationIssue validateStopTime(
+ ServiceJourney serviceJourney,
+ JAXBValidationContext validationContext,
+ StopTime stopTime
+ ) {
+ if (!stopTime.isComplete()) {
+ return new ValidationIssue(
+ RULE_INCOMPLETE_TIME,
+ validationContext.dataLocation(serviceJourney.getId()),
+ validationContext.stopPointName(stopTime.scheduledStopPointId())
+ );
+ }
+ if (!stopTime.isConsistent()) {
+ return new ValidationIssue(
+ RULE_INCONSISTENT_TIME,
+ validationContext.dataLocation(serviceJourney.getId()),
+ validationContext.stopPointName(stopTime.scheduledStopPointId())
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidator.java b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidator.java
new file mode 100644
index 00000000..40571f0a
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidator.java
@@ -0,0 +1,221 @@
+package org.entur.netex.validation.validator.jaxb.rules.servicejourney.transportmode;
+
+import static org.entur.netex.validation.validator.jaxb.support.NetexUtils.stopPointsInJourneyPattern;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.entur.netex.validation.validator.Severity;
+import org.entur.netex.validation.validator.ValidationIssue;
+import org.entur.netex.validation.validator.ValidationRule;
+import org.entur.netex.validation.validator.jaxb.JAXBValidationContext;
+import org.entur.netex.validation.validator.jaxb.JAXBValidator;
+import org.entur.netex.validation.validator.model.QuayId;
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.entur.netex.validation.validator.model.TransportModeAndSubMode;
+import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration;
+import org.rutebanken.netex.model.BusSubmodeEnumeration;
+import org.rutebanken.netex.model.JourneyPattern;
+import org.rutebanken.netex.model.ServiceJourney;
+import org.rutebanken.netex.model.StopPointInJourneyPattern;
+
+/**
+ * Validates that the transport mode and sub-mode of a service journey matches the quays it visits.
+ */
+public class MismatchedTransportModeSubModeValidator implements JAXBValidator {
+
+ static final ValidationRule RULE_INVALID_TRANSPORT_MODE = new ValidationRule(
+ "INVALID_TRANSPORT_MODE",
+ "Invalid transport mode",
+ "Invalid transport mode: The quay %s accepts %s, but the ServiceJourney %s is defined as %s",
+ Severity.ERROR
+ );
+
+ static final ValidationRule RULE_INVALID_TRANSPORT_SUB_MODE =
+ new ValidationRule(
+ "INVALID_TRANSPORT_SUB_MODE",
+ "Invalid transport sub-mode",
+ "Invalid transport sub-mode: The quay %s accepts %s, but the ServiceJourney %s is defined as %s",
+ Severity.ERROR
+ );
+
+ /**
+ * Iterate through all stop points of all service journeys and compare the transport mode of the associated quay with
+ * the transport mode of the service journey.
+ */
+ @Override
+ public List validate(
+ JAXBValidationContext validationContext
+ ) {
+ List issues = new ArrayList<>();
+
+ for (ServiceJourney serviceJourney : validationContext.serviceJourneys()) {
+ TransportModeAndSubMode serviceJourneyTransportMode =
+ validationContext.transportModeAndSubMode(serviceJourney);
+
+ // skip if neither the service journey nor the line have a transport mode.
+ // this should be validated separately.
+ if (serviceJourneyTransportMode == null) {
+ continue;
+ }
+
+ JourneyPattern journeyPattern = validationContext.journeyPattern(
+ serviceJourney
+ );
+ List stopPointInJourneyPatterns =
+ stopPointsInJourneyPattern(journeyPattern);
+
+ for (StopPointInJourneyPattern stopPointInJourneyPattern : stopPointInJourneyPatterns) {
+ QuayId quayId = validationContext.quayIdForScheduledStopPoint(
+ ScheduledStopPointId.of(stopPointInJourneyPattern)
+ );
+
+ // skip if the scheduled stop point is not mapped to a quay.
+ // this should be validated separately.
+ if (quayId == null) {
+ continue;
+ }
+ TransportModeAndSubMode quayTransportModeAndSubMode =
+ validationContext.transportModeAndSubModeForQuayId(quayId);
+
+ // skip if the quay does not have a transport mode.
+ // this can be caused by a data issue in the external stop register.
+ if (quayTransportModeAndSubMode == null) {
+ continue;
+ }
+
+ validationIssue(
+ quayId.id(),
+ serviceJourney.getId(),
+ quayTransportModeAndSubMode,
+ serviceJourneyTransportMode,
+ validationContext
+ )
+ .ifPresent(issues::add);
+ }
+ }
+ return issues;
+ }
+
+ /**
+ * Return a validation issue if the service journey transport mode/submode does not match
+ * the quay transport mode/submode.
+ */
+ private Optional validationIssue(
+ String quayId,
+ String serviceJourneyId,
+ TransportModeAndSubMode quayTransportModeAndSubMode,
+ TransportModeAndSubMode serviceJourneyTransportModeAndSubMode,
+ JAXBValidationContext validationContext
+ ) {
+ if (
+ quayTransportModeAndSubMode.mode() !=
+ serviceJourneyTransportModeAndSubMode.mode()
+ ) {
+ // Coach and bus are interchangeable.
+ if (
+ busServingCoachStop(
+ quayTransportModeAndSubMode,
+ serviceJourneyTransportModeAndSubMode
+ ) ||
+ coachServingBusStop(
+ quayTransportModeAndSubMode,
+ serviceJourneyTransportModeAndSubMode
+ )
+ ) {
+ return Optional.empty();
+ }
+
+ // Taxi can stop on bus and coach stops.
+ if (
+ taxiServingBusOrCoachStop(
+ quayTransportModeAndSubMode,
+ serviceJourneyTransportModeAndSubMode
+ )
+ ) {
+ return Optional.empty();
+ }
+
+ return Optional.of(
+ new ValidationIssue(
+ RULE_INVALID_TRANSPORT_MODE,
+ validationContext.dataLocation(serviceJourneyId),
+ quayId,
+ quayTransportModeAndSubMode.mode(),
+ serviceJourneyId,
+ serviceJourneyTransportModeAndSubMode.mode()
+ )
+ );
+ }
+
+ // Only rail replacement bus service can visit rail replacement bus stops.
+ if (
+ quayTransportModeAndSubMode.subMode() != null &&
+ BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS
+ .value()
+ .equals(quayTransportModeAndSubMode.subMode().name()) &&
+ !BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS
+ .value()
+ .equals(serviceJourneyTransportModeAndSubMode.subMode().name())
+ ) {
+ return Optional.of(
+ new ValidationIssue(
+ RULE_INVALID_TRANSPORT_SUB_MODE,
+ validationContext.dataLocation(serviceJourneyId),
+ quayId,
+ quayTransportModeAndSubMode.subMode(),
+ serviceJourneyId,
+ serviceJourneyTransportModeAndSubMode.subMode()
+ )
+ );
+ }
+
+ return Optional.empty();
+ }
+
+ private static boolean taxiServingBusOrCoachStop(
+ TransportModeAndSubMode quayTransportModeAndSubMode,
+ TransportModeAndSubMode serviceJourneyTransportModeAndSubMode
+ ) {
+ return (
+ serviceJourneyTransportModeAndSubMode.mode() ==
+ AllVehicleModesOfTransportEnumeration.TAXI &&
+ (
+ quayTransportModeAndSubMode.mode() ==
+ AllVehicleModesOfTransportEnumeration.BUS ||
+ quayTransportModeAndSubMode.mode() ==
+ AllVehicleModesOfTransportEnumeration.COACH
+ )
+ );
+ }
+
+ private static boolean coachServingBusStop(
+ TransportModeAndSubMode quayTransportModeAndSubMode,
+ TransportModeAndSubMode serviceJourneyTransportModeAndSubMode
+ ) {
+ return (
+ quayTransportModeAndSubMode.mode() ==
+ AllVehicleModesOfTransportEnumeration.BUS &&
+ serviceJourneyTransportModeAndSubMode.mode() ==
+ AllVehicleModesOfTransportEnumeration.COACH
+ );
+ }
+
+ private static boolean busServingCoachStop(
+ TransportModeAndSubMode quayTransportModeAndSubMode,
+ TransportModeAndSubMode serviceJourneyTransportModeAndSubMode
+ ) {
+ return (
+ quayTransportModeAndSubMode.mode() ==
+ AllVehicleModesOfTransportEnumeration.COACH &&
+ serviceJourneyTransportModeAndSubMode.mode() ==
+ AllVehicleModesOfTransportEnumeration.BUS
+ );
+ }
+
+ @Override
+ public Set getRules() {
+ return Set.of(RULE_INVALID_TRANSPORT_MODE, RULE_INVALID_TRANSPORT_SUB_MODE);
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java b/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java
index c6b35122..3e786e5c 100644
--- a/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java
@@ -1,7 +1,7 @@
package org.entur.netex.validation.validator.jaxb.support;
+import jakarta.annotation.Nullable;
import jakarta.xml.bind.JAXBElement;
-import javax.annotation.Nullable;
import org.rutebanken.netex.model.DatedServiceJourney;
import org.rutebanken.netex.model.DatedServiceJourneyRefStructure;
import org.rutebanken.netex.model.VersionOfObjectRefStructure;
diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/support/NetexUtils.java b/src/main/java/org/entur/netex/validation/validator/jaxb/support/NetexUtils.java
new file mode 100644
index 00000000..c7acd32f
--- /dev/null
+++ b/src/main/java/org/entur/netex/validation/validator/jaxb/support/NetexUtils.java
@@ -0,0 +1,94 @@
+package org.entur.netex.validation.validator.jaxb.support;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.rutebanken.netex.model.JourneyPattern;
+import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure;
+import org.rutebanken.netex.model.PointsInJourneyPattern_RelStructure;
+import org.rutebanken.netex.model.StopPointInJourneyPattern;
+import org.rutebanken.netex.model.TimetabledPassingTime;
+
+/**
+ * Utility methods for JAXB NeTEx entities.
+ */
+public class NetexUtils {
+
+ private NetexUtils() {}
+
+ /**
+ * Return the StopPointInJourneyPattern ID of a given TimeTabledPassingTime.
+ */
+ public static String stopPointRef(
+ TimetabledPassingTime timetabledPassingTime
+ ) {
+ return timetabledPassingTime
+ .getPointInJourneyPatternRef()
+ .getValue()
+ .getRef();
+ }
+
+ /**
+ * Return the mapping between stop point id and scheduled stop point id for the journey
+ * pattern.
+ */
+ public static Map scheduledStopPointIdByStopPointId(
+ JourneyPattern journeyPattern
+ ) {
+ return stopPointsInJourneyPattern(journeyPattern)
+ .stream()
+ .collect(
+ Collectors.toMap(
+ StopPointInJourneyPattern::getId,
+ ScheduledStopPointId::of
+ )
+ );
+ }
+
+ /**
+ * Find the stop points in journey pattern for the given journey pattern, sorted by order.
+ */
+ public static List stopPointsInJourneyPattern(
+ JourneyPattern journeyPattern
+ ) {
+ return Optional
+ .ofNullable(journeyPattern.getPointsInSequence())
+ .map(
+ PointsInJourneyPattern_RelStructure::getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern
+ )
+ .map(stopPointsInJourneyPattern ->
+ stopPointsInJourneyPattern
+ .stream()
+ .filter(StopPointInJourneyPattern.class::isInstance)
+ .map(StopPointInJourneyPattern.class::cast)
+ .sorted(
+ Comparator.comparing(
+ PointInLinkSequence_VersionedChildStructure::getOrder
+ )
+ )
+ )
+ .orElse(Stream.empty())
+ .toList();
+ }
+
+ /**
+ * Find the stop point in journey pattern for the
+ * given stop point in journey pattern reference.
+ */
+ public static StopPointInJourneyPattern stopPointInJourneyPattern(
+ String stopPointInJourneyPatternRef,
+ JourneyPattern journeyPattern
+ ) {
+ return stopPointsInJourneyPattern(journeyPattern)
+ .stream()
+ .filter(stopPointInJourneyPattern ->
+ stopPointInJourneyPattern.getId().equals(stopPointInJourneyPatternRef)
+ )
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java b/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java
index efc99e5c..68353ebf 100644
--- a/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java
+++ b/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java
@@ -1,7 +1,7 @@
package org.entur.netex.validation.validator.model;
+import jakarta.annotation.Nullable;
import java.util.Objects;
-import javax.annotation.Nullable;
import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration;
import org.rutebanken.netex.model.StopPlace;
import org.rutebanken.netex.model.TransportSubmodeStructure;
diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java
index aeb0e014..359ee4fd 100644
--- a/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java
+++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java
@@ -1,7 +1,6 @@
package org.entur.netex.validation.test.jaxb.support;
import jakarta.xml.bind.JAXBElement;
-import javax.annotation.Nonnull;
import javax.xml.namespace.QName;
import org.rutebanken.netex.model.VersionOfObjectRefStructure;
@@ -53,7 +52,7 @@ public static T createRef(
* @return the value wrapped in a JAXBElement
*/
@SuppressWarnings("unchecked")
- public static JAXBElement createJaxbElement(@Nonnull T value) {
+ public static JAXBElement createJaxbElement(T value) {
return new JAXBElement<>(
new QName("x"),
(Class) value.getClass(),
diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/NetexEntitiesTestFactory.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/NetexEntitiesTestFactory.java
new file mode 100644
index 00000000..114162e3
--- /dev/null
+++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/NetexEntitiesTestFactory.java
@@ -0,0 +1,1687 @@
+package org.entur.netex.validation.test.jaxb.support;
+
+import static org.entur.netex.validation.test.jaxb.support.JAXBUtils.createJaxbElement;
+
+import jakarta.xml.bind.JAXBElement;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.math.BigInteger;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import net.opengis.gml._3.AbstractRingPropertyType;
+import net.opengis.gml._3.DirectPositionListType;
+import net.opengis.gml._3.DirectPositionType;
+import net.opengis.gml._3.LineStringType;
+import net.opengis.gml._3.LinearRingType;
+import net.opengis.gml._3.ObjectFactory;
+import net.opengis.gml._3.PolygonType;
+import org.entur.netex.index.api.NetexEntitiesIndex;
+import org.entur.netex.index.impl.NetexEntitiesIndexImpl;
+import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration;
+import org.rutebanken.netex.model.DatedServiceJourney;
+import org.rutebanken.netex.model.DatedServiceJourneyRefStructure;
+import org.rutebanken.netex.model.DayType;
+import org.rutebanken.netex.model.DayTypeAssignment;
+import org.rutebanken.netex.model.DayTypeRefStructure;
+import org.rutebanken.netex.model.DayTypeRefs_RelStructure;
+import org.rutebanken.netex.model.DeadRun;
+import org.rutebanken.netex.model.DestinationDisplayRefStructure;
+import org.rutebanken.netex.model.EntityStructure;
+import org.rutebanken.netex.model.FlexibleArea;
+import org.rutebanken.netex.model.FlexibleLine;
+import org.rutebanken.netex.model.FlexibleLineTypeEnumeration;
+import org.rutebanken.netex.model.FlexibleStopPlace;
+import org.rutebanken.netex.model.FlexibleStopPlace_VersionStructure;
+import org.rutebanken.netex.model.JourneyPattern;
+import org.rutebanken.netex.model.JourneyPatternRefStructure;
+import org.rutebanken.netex.model.JourneyRefStructure;
+import org.rutebanken.netex.model.Line;
+import org.rutebanken.netex.model.LineRefStructure;
+import org.rutebanken.netex.model.Line_VersionStructure;
+import org.rutebanken.netex.model.LinkInJourneyPattern;
+import org.rutebanken.netex.model.LinkInLinkSequence_VersionedChildStructure;
+import org.rutebanken.netex.model.LinkSequenceProjection_VersionStructure;
+import org.rutebanken.netex.model.LinksInJourneyPattern_RelStructure;
+import org.rutebanken.netex.model.MultilingualString;
+import org.rutebanken.netex.model.OperatingDay;
+import org.rutebanken.netex.model.OperatingDayRefStructure;
+import org.rutebanken.netex.model.OperatingPeriodRefStructure;
+import org.rutebanken.netex.model.PassengerStopAssignment;
+import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure;
+import org.rutebanken.netex.model.PointsInJourneyPattern_RelStructure;
+import org.rutebanken.netex.model.Projections_RelStructure;
+import org.rutebanken.netex.model.QuayRefStructure;
+import org.rutebanken.netex.model.Route;
+import org.rutebanken.netex.model.RouteRefStructure;
+import org.rutebanken.netex.model.ScheduledStopPointRefStructure;
+import org.rutebanken.netex.model.ServiceAlterationEnumeration;
+import org.rutebanken.netex.model.ServiceJourney;
+import org.rutebanken.netex.model.ServiceJourneyInterchange;
+import org.rutebanken.netex.model.ServiceJourneyRefStructure;
+import org.rutebanken.netex.model.ServiceLink;
+import org.rutebanken.netex.model.ServiceLinkRefStructure;
+import org.rutebanken.netex.model.StopPlaceRefStructure;
+import org.rutebanken.netex.model.StopPointInJourneyPattern;
+import org.rutebanken.netex.model.StopPointInJourneyPatternRefStructure;
+import org.rutebanken.netex.model.TimetabledPassingTime;
+import org.rutebanken.netex.model.TimetabledPassingTimes_RelStructure;
+import org.rutebanken.netex.model.TransportSubmodeStructure;
+import org.rutebanken.netex.model.VehicleJourneyRefStructure;
+import org.rutebanken.netex.model.VersionOfObjectRefStructure;
+
+/**
+ * Create JAXB NeTEx test data.
+ */
+public class NetexEntitiesTestFactory {
+
+ private static final DayType EVERYDAY = new DayType()
+ .withId("EVERYDAY")
+ .withName(new MultilingualString().withValue("everyday"));
+
+ private CreateGenericLine extends Line_VersionStructure> line;
+
+ private CreateRoute route;
+
+ private final List journeyPatterns = new ArrayList<>();
+ private final List serviceJourneys = new ArrayList<>();
+ private final List datedServiceJourneys =
+ new ArrayList<>();
+
+ private final List interchanges =
+ new ArrayList<>();
+ private final List serviceLinks = new ArrayList<>();
+ private final List flexibleStopPlaces =
+ new ArrayList<>();
+ private final List deadRuns = new ArrayList<>();
+ private final List passengerStopAssignments =
+ new ArrayList<>();
+
+ public NetexEntitiesIndex create() {
+ NetexEntitiesIndex netexEntitiesIndex = new NetexEntitiesIndexImpl();
+
+ if (line != null && line instanceof CreateLine createLine) {
+ netexEntitiesIndex.getLineIndex().put(line.ref(), createLine.create());
+ }
+
+ if (line != null && line instanceof CreateFlexibleLine createFlexibleLine) {
+ netexEntitiesIndex
+ .getFlexibleLineIndex()
+ .put(line.ref(), createFlexibleLine.create());
+ }
+
+ if (route != null) {
+ netexEntitiesIndex.getRouteIndex().put(route.ref(), route.create());
+ }
+
+ fillIndexes(netexEntitiesIndex);
+ return netexEntitiesIndex;
+ }
+
+ private void fillIndexes(NetexEntitiesIndex netexEntitiesIndex) {
+ passengerStopAssignments
+ .stream()
+ .map(CreatePassengerStopAssignment::create)
+ .forEach(passengerStopAssignment -> {
+ // PassengerStopAssignmentsByStopPointRefIndex
+ netexEntitiesIndex
+ .getPassengerStopAssignmentsByStopPointRefIndex()
+ .put(
+ passengerStopAssignment
+ .getScheduledStopPointRef()
+ .getValue()
+ .getRef(),
+ passengerStopAssignment
+ );
+
+ // QuayIdByStopPointRefIndex
+ netexEntitiesIndex
+ .getQuayIdByStopPointRefIndex()
+ .put(
+ passengerStopAssignment
+ .getScheduledStopPointRef()
+ .getValue()
+ .getRef(),
+ passengerStopAssignment.getQuayRef().getValue().getRef()
+ );
+ });
+
+ journeyPatterns
+ .stream()
+ .map(CreateJourneyPattern::create)
+ .forEach(journeyPattern ->
+ netexEntitiesIndex
+ .getJourneyPatternIndex()
+ .put(journeyPattern.getId(), journeyPattern)
+ );
+
+ interchanges
+ .stream()
+ .map(CreateServiceJourneyInterchange::create)
+ .forEach(interchange ->
+ netexEntitiesIndex
+ .getServiceJourneyInterchangeIndex()
+ .put(interchange.getId(), interchange)
+ );
+
+ serviceJourneys
+ .stream()
+ .map(CreateServiceJourney::create)
+ .forEach(journey ->
+ netexEntitiesIndex
+ .getServiceJourneyIndex()
+ .put(journey.getId(), journey)
+ );
+
+ datedServiceJourneys
+ .stream()
+ .map(CreateDatedServiceJourney::create)
+ .forEach(journey ->
+ netexEntitiesIndex
+ .getDatedServiceJourneyIndex()
+ .put(journey.getId(), journey)
+ );
+
+ deadRuns
+ .stream()
+ .map(CreateDeadRun::create)
+ .forEach(deadRun ->
+ netexEntitiesIndex.getDeadRunIndex().put(deadRun.getId(), deadRun)
+ );
+
+ serviceLinks
+ .stream()
+ .map(CreateServiceLink::create)
+ .forEach(serviceLink ->
+ netexEntitiesIndex
+ .getServiceLinkIndex()
+ .put(serviceLink.getId(), serviceLink)
+ );
+ flexibleStopPlaces
+ .stream()
+ .map(CreateFlexibleStopPlace::create)
+ .forEach(flexibleStopPlace ->
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIndex()
+ .put(flexibleStopPlace.getId(), flexibleStopPlace)
+ );
+ }
+
+ /**
+ * Create a line with the given id
+ * The existing line will be overwritten.
+ *
+ * @param id the id of the line
+ * @return CreateLine
+ */
+ public CreateLine createLine(int id) {
+ line = new CreateLine(id);
+ return (CreateLine) line;
+ }
+
+ /**
+ * Create a line with id 1.
+ * The existing line will be overwritten.
+ *
+ * @return CreateLine
+ */
+ public CreateLine createLine() {
+ line = new CreateLine(1);
+ return (CreateLine) line;
+ }
+
+ /**
+ * Create a flexible line with the given id
+ * The existing line will be overwritten.
+ *
+ * @param id the id of the line
+ * @return CreateFlexibleLine
+ */
+ public CreateFlexibleLine createFlexibleLine(int id) {
+ line = new CreateFlexibleLine(id);
+ return (CreateFlexibleLine) line;
+ }
+
+ /**
+ * Create a flexible line with id 1.
+ * The existing line will be overwritten.
+ *
+ * @return CreateFlexibleLine
+ */
+ public CreateFlexibleLine createFlexibleLine() {
+ line = new CreateFlexibleLine(1);
+ return (CreateFlexibleLine) line;
+ }
+
+ /**
+ * Create a route with the given id
+ * The existing route will be overwritten.
+ *
+ * @param id the id of the route
+ * @return CreateRoute
+ */
+ public CreateRoute createRoute(int id) {
+ if (line == null) {
+ line = new CreateLine(1);
+ }
+
+ route = new CreateRoute(id, line);
+ return route;
+ }
+
+ /**
+ * Create a route with id 1.
+ * The existing route will be overwritten.
+ *
+ * @return CreateRoute
+ */
+ public CreateRoute createRoute() {
+ if (line == null) {
+ line = new CreateLine(1);
+ }
+
+ route = new CreateRoute(1, line);
+ return route;
+ }
+
+ /**
+ * Adds a new journey pattern with the given id
+ *
+ * @param id the id of the journey pattern
+ * @return CreateJourneyPattern
+ */
+ public CreateJourneyPattern createJourneyPattern(int id) {
+ CreateJourneyPattern createJourneyPattern = new CreateJourneyPattern(id);
+ journeyPatterns.add(createJourneyPattern);
+ return createJourneyPattern;
+ }
+
+ /**
+ * Adds a new journey pattern with id 1
+ *
+ * @return CreateJourneyPattern
+ */
+ public CreateJourneyPattern createJourneyPattern() {
+ CreateJourneyPattern createJourneyPattern = new CreateJourneyPattern(1);
+ journeyPatterns.add(createJourneyPattern);
+ return createJourneyPattern;
+ }
+
+ /**
+ * Adds a new service journey with the given id and the given journey pattern
+ * The line will be created if it does not exist, with id 1
+ *
+ * @param id the id of the journey pattern
+ * @param journeyPattern the journey pattern ref for the service journey
+ * @return CreateServiceJourney
+ */
+ public CreateServiceJourney createServiceJourney(
+ int id,
+ CreateJourneyPattern journeyPattern
+ ) {
+ if (line == null) {
+ line = new CreateLine(1);
+ }
+
+ CreateServiceJourney createServiceJourney = new CreateServiceJourney(
+ id,
+ line,
+ journeyPattern
+ );
+ serviceJourneys.add(createServiceJourney);
+ return createServiceJourney;
+ }
+
+ /**
+ * Adds a new service journey with id 1 and the given journey pattern
+ * The line will be created if it does not exist, with id 1
+ *
+ * @param journeyPattern the journey pattern ref for the service journey
+ * @return CreateServiceJourney
+ */
+ public CreateServiceJourney createServiceJourney(
+ CreateJourneyPattern journeyPattern
+ ) {
+ if (line == null) {
+ line = new CreateLine(1);
+ }
+
+ CreateServiceJourney createServiceJourney = new CreateServiceJourney(
+ 1,
+ line,
+ journeyPattern
+ );
+ serviceJourneys.add(createServiceJourney);
+ return createServiceJourney;
+ }
+
+ /**
+ * Adds a new dead run with the given id and the given journey pattern
+ * The line will be created if it does not exist, with id 1
+ *
+ * @param id the id of the journey pattern
+ * @param journeyPattern the journey pattern ref for the dead run
+ * @return CreateDeadRun
+ */
+ public CreateDeadRun createDeadRun(
+ int id,
+ CreateJourneyPattern journeyPattern
+ ) {
+ if (line == null) {
+ line = new CreateLine(1);
+ }
+ CreateDeadRun deadRun = new CreateDeadRun(id, line, journeyPattern);
+ deadRuns.add(deadRun);
+ return deadRun;
+ }
+
+ /**
+ * Adds a new dead run with id 1 and the given journey pattern
+ * The line will be created if it does not exist, with id 1
+ *
+ * @param journeyPattern the journey pattern ref for the dead run
+ * @return CreateDeadRun
+ */
+ public CreateDeadRun createDeadRun(CreateJourneyPattern journeyPattern) {
+ if (line == null) {
+ line = new CreateLine(1);
+ }
+ CreateDeadRun deadRun = new CreateDeadRun(1, line, journeyPattern);
+ deadRuns.add(deadRun);
+ return deadRun;
+ }
+
+ /**
+ * Adds a new dated service journey with the given id, service journey and operating day
+ *
+ * @param id the id of the dated service journey
+ * @param serviceJourneyRef the service journey ref for the dated service journey
+ * @param operatingDayRef the operating day ref for the dated service journey
+ * @return CreateDatedServiceJourney
+ */
+ public CreateDatedServiceJourney createDatedServiceJourney(
+ int id,
+ CreateServiceJourney serviceJourneyRef,
+ CreateOperatingDay operatingDayRef
+ ) {
+ CreateDatedServiceJourney createDatedServiceJourney =
+ new CreateDatedServiceJourney(id, serviceJourneyRef, operatingDayRef);
+ datedServiceJourneys.add(createDatedServiceJourney);
+ return createDatedServiceJourney;
+ }
+
+ /**
+ * Adds a new dated service journey with id 1, service journey and operating day
+ *
+ * @param serviceJourneyRef the service journey ref for the dated service journey
+ * @param operatingDayRef the operating day ref for the dated service journey
+ * @return CreateDatedServiceJourney
+ */
+ public CreateDatedServiceJourney createDatedServiceJourney(
+ CreateServiceJourney serviceJourneyRef,
+ CreateOperatingDay operatingDayRef
+ ) {
+ CreateDatedServiceJourney createDatedServiceJourney =
+ new CreateDatedServiceJourney(1, serviceJourneyRef, operatingDayRef);
+ datedServiceJourneys.add(createDatedServiceJourney);
+ return createDatedServiceJourney;
+ }
+
+ /**
+ * Adds numberOfServiceJourneys new service journeys with the given journey pattern.
+ * The line will be created if it does not exist, with id 1
+ * The service journeys will have ids from 1 to numberOfServiceJourneys
+ *
+ * @param createJourneyPattern the journey pattern ref for the service journeys
+ * @param numberOfServiceJourneys the number of service journeys to create
+ * @return List of CreateServiceJourney created
+ */
+ public List createServiceJourneys(
+ CreateJourneyPattern createJourneyPattern,
+ int numberOfServiceJourneys
+ ) {
+ if (line == null) {
+ line = new CreateLine(1);
+ }
+ List createServiceJourneys = IntStream
+ .rangeClosed(1, numberOfServiceJourneys)
+ .mapToObj(index ->
+ new CreateServiceJourney(index, line, createJourneyPattern)
+ )
+ .toList();
+ serviceJourneys.addAll(createServiceJourneys);
+ return createServiceJourneys;
+ }
+
+ /**
+ * Adds a new service journey interchange with the given id
+ *
+ * @param id the id of the service journey interchange
+ * @return CreateServiceJourneyInterchange
+ */
+ public CreateServiceJourneyInterchange createServiceJourneyInterchange(
+ int id
+ ) {
+ CreateServiceJourneyInterchange createServiceJourneyInterchange =
+ new CreateServiceJourneyInterchange(id);
+ interchanges.add(createServiceJourneyInterchange);
+ return createServiceJourneyInterchange;
+ }
+
+ /**
+ * Adds a new service journey interchange with id 1
+ *
+ * @return CreateServiceJourneyInterchange
+ */
+ public CreateServiceJourneyInterchange createServiceJourneyInterchange() {
+ CreateServiceJourneyInterchange createServiceJourneyInterchange =
+ new CreateServiceJourneyInterchange(1);
+ interchanges.add(createServiceJourneyInterchange);
+ return createServiceJourneyInterchange;
+ }
+
+ /**
+ * Adds a new service link with the given id
+ *
+ * @param id the id of the service link
+ * @return CreateServiceLink
+ */
+ public CreateServiceLink createServiceLink(
+ int id,
+ ScheduledStopPointRefStructure fromScheduledStopPointRef,
+ ScheduledStopPointRefStructure toScheduledStopPointRef
+ ) {
+ CreateServiceLink createServiceLink = new CreateServiceLink(id)
+ .withFromScheduledStopPointRef(fromScheduledStopPointRef)
+ .withToScheduledStopPointRef(toScheduledStopPointRef);
+ serviceLinks.add(createServiceLink);
+ return createServiceLink;
+ }
+
+ /**
+ * Adds a new service link with id 1
+ *
+ * @return CreateServiceLink
+ */
+ public CreateServiceLink createServiceLink(
+ ScheduledStopPointRefStructure fromScheduledStopPointRef,
+ ScheduledStopPointRefStructure toScheduledStopPointRef
+ ) {
+ CreateServiceLink createServiceLink = new CreateServiceLink(1)
+ .withFromScheduledStopPointRef(fromScheduledStopPointRef)
+ .withToScheduledStopPointRef(toScheduledStopPointRef);
+ serviceLinks.add(createServiceLink);
+ return createServiceLink;
+ }
+
+ /**
+ * Adds a new flexible stop place with the given id
+ *
+ * @param id the id of the flexible stop place
+ * @return CreateFlexibleStopPlace
+ */
+ public CreateFlexibleStopPlace createFlexibleStopPlace(int id) {
+ CreateFlexibleStopPlace createFlexibleStopPlace =
+ new CreateFlexibleStopPlace(id);
+ flexibleStopPlaces.add(createFlexibleStopPlace);
+ return createFlexibleStopPlace;
+ }
+
+ /**
+ * Adds a new flexible stop place with id 1
+ *
+ * @return CreateFlexibleStopPlace
+ */
+ public CreateFlexibleStopPlace createFlexibleStopPlace() {
+ CreateFlexibleStopPlace createFlexibleStopPlace =
+ new CreateFlexibleStopPlace(1);
+ flexibleStopPlaces.add(createFlexibleStopPlace);
+ return createFlexibleStopPlace;
+ }
+
+ /**
+ * Adds a new passenger stop assignment with the given id
+ *
+ * @param id the id of the passenger stop assignment
+ * @return CreatePassengerStopAssignment
+ */
+ public CreatePassengerStopAssignment createPassengerStopAssignment(int id) {
+ CreatePassengerStopAssignment createPassengerStopAssignment =
+ new CreatePassengerStopAssignment(id);
+ passengerStopAssignments.add(createPassengerStopAssignment);
+ return createPassengerStopAssignment;
+ }
+
+ /**
+ * Adds a new passenger stop assignment with id 1
+ *
+ * @return CreatePassengerStopAssignment
+ */
+ public CreatePassengerStopAssignment createPassengerStopAssignment() {
+ CreatePassengerStopAssignment createPassengerStopAssignment =
+ new CreatePassengerStopAssignment(1);
+ passengerStopAssignments.add(createPassengerStopAssignment);
+ return createPassengerStopAssignment;
+ }
+
+ /**
+ * Adds a new day type with the given id
+ *
+ * @param id the id of the day type
+ * @return CreateDayType
+ */
+ public CreateOperatingDay createOperatingDay(int id, LocalDate date) {
+ return new CreateOperatingDay(id, date);
+ }
+
+ /**
+ * Adds a new day type with id 1
+ *
+ * @return CreateDayType
+ */
+ public CreateOperatingDay createOperatingDay(LocalDate date) {
+ return new CreateOperatingDay(1, date);
+ }
+
+ /**
+ * Creates the new ScheduledStopPointRefStructure with the given id
+ *
+ * @param id the id of the ScheduledStopPoint
+ * @return ScheduledStopPointRefStructure
+ */
+ public static ScheduledStopPointRefStructure createScheduledStopPointRef(
+ int id
+ ) {
+ return new ScheduledStopPointRefStructure()
+ .withRef("TST:ScheduledStopPoint:" + id);
+ }
+
+ /**
+ * Creates the new QuayRefStructure with the given id
+ *
+ * @param id the id of the Quay
+ * @return QuayRefStructure
+ */
+ public static QuayRefStructure createQuayRef(int id) {
+ return new QuayRefStructure().withRef("TST:Quay:" + id);
+ }
+
+ /**
+ * Creates the new StopPlaceRefStructure with the given id
+ *
+ * @param id the id of the StopPlace
+ * @return StopPlaceRefStructure
+ */
+ public static StopPlaceRefStructure createStopPointRef(int id) {
+ return new StopPlaceRefStructure().withRef("TST:StopPoint:" + id);
+ }
+
+ /**
+ * Creates the new StopPlaceRefStructure with the given id
+ *
+ * @param id the id of the StopPlace
+ * @return StopPlaceRefStructure
+ */
+ public static ServiceLinkRefStructure createServiceLinkRef(int id) {
+ return new ServiceLinkRefStructure().withRef("TST:ServiceLink:" + id);
+ }
+
+ /**
+ * Creates the new VehicleJourneyRefStructure with the given id
+ *
+ * @param id the id of the VehicleJourney
+ * @return VehicleJourneyRefStructure
+ */
+ public static VehicleJourneyRefStructure createServiceJourneyRef(int id) {
+ return new VehicleJourneyRefStructure().withRef("TST:ServiceJourney:" + id);
+ }
+
+ /**
+ * Creates the new DatedServiceJourneyRefStructure with the given id
+ *
+ * @param id the id of the DatedServiceJourney
+ * @return DatedServiceJourneyRefStructure
+ */
+ public static DestinationDisplayRefStructure createDestinationDisplayRef(
+ int id
+ ) {
+ return new DestinationDisplayRefStructure()
+ .withRef("TST:DestinationDisplay:" + id);
+ }
+
+ /**
+ * This interface enables the CreateEntity classes to the reference object of their ids.
+ *
+ * @param
+ */
+ public interface CreateRef {
+ R refObject();
+ }
+
+ /**
+ * Abstract class for automatic handling of entity reference.
+ * It creates the reference using reflection, based on the integer id provided
+ * in the constructor
+ */
+ public abstract static class CreateEntity {
+
+ protected final int id;
+
+ public CreateEntity(int id) {
+ this.id = id;
+ }
+
+ public final String ref() {
+ Type type =
+ (
+ (ParameterizedType) getClass().getGenericSuperclass()
+ ).getActualTypeArguments()[0];
+ return "TST:" + ((Class>) type).getSimpleName() + ":" + id;
+ }
+
+ public abstract T create();
+ }
+
+ public static class CreateFlexibleArea extends CreateEntity {
+
+ private List coordinates;
+ private boolean withNullPolygon = false;
+
+ public CreateFlexibleArea(int id) {
+ super(id);
+ }
+
+ public CreateFlexibleArea withCoordinates(List coordinates) {
+ this.coordinates = coordinates;
+ return this;
+ }
+
+ public CreateFlexibleArea withNullPolygon(boolean withNullPolygon) {
+ this.withNullPolygon = withNullPolygon;
+ return this;
+ }
+
+ public FlexibleArea create() {
+ LinearRingType linearRing = new LinearRingType();
+ DirectPositionListType positionList = new DirectPositionListType()
+ .withValue(coordinates);
+ linearRing.withPosList(positionList);
+
+ FlexibleArea flexibleArea = new FlexibleArea()
+ .withId(ref())
+ .withName(new MultilingualString().withValue("FlexibleArea " + id));
+
+ if (withNullPolygon) {
+ return flexibleArea.withPolygon(null);
+ }
+
+ return flexibleArea.withPolygon(
+ new PolygonType()
+ .withExterior(
+ new AbstractRingPropertyType()
+ .withAbstractRing(
+ new ObjectFactory().createLinearRing(linearRing)
+ )
+ )
+ );
+ }
+ }
+
+ public static class CreateFlexibleStopPlace
+ extends CreateEntity {
+
+ private CreateFlexibleArea flexibleArea;
+
+ public CreateFlexibleStopPlace(int id) {
+ super(id);
+ }
+
+ /**
+ * Creates a new flexible area with the given id,
+ * if it does not already exist.
+ *
+ * @param id the id of the flexible area
+ * @return CreateFlexibleArea
+ */
+ public CreateFlexibleArea flexibleArea(int id) {
+ if (flexibleArea == null) {
+ flexibleArea = new CreateFlexibleArea(id);
+ }
+ return flexibleArea;
+ }
+
+ public FlexibleStopPlace create() {
+ return new FlexibleStopPlace()
+ .withId(ref())
+ .withName(new MultilingualString().withValue("FlexibleStopPlace " + id))
+ .withAreas(
+ new FlexibleStopPlace_VersionStructure.Areas()
+ .withFlexibleAreaOrFlexibleAreaRefOrHailAndRideArea(
+ Optional
+ .ofNullable(flexibleArea)
+ .map(CreateFlexibleArea::create)
+ .orElse(null)
+ )
+ );
+ }
+ }
+
+ public static class CreatePassengerStopAssignment
+ extends CreateEntity {
+
+ private ScheduledStopPointRefStructure scheduleStopPointRef;
+
+ private StopPlaceRefStructure stopPlaceRef;
+
+ private QuayRefStructure quayRef;
+
+ public CreatePassengerStopAssignment(int id) {
+ super(id);
+ }
+
+ public CreatePassengerStopAssignment withScheduledStopPointRef(
+ ScheduledStopPointRefStructure scheduledStopPointRef
+ ) {
+ this.scheduleStopPointRef = scheduledStopPointRef;
+ return this;
+ }
+
+ public CreatePassengerStopAssignment withStopPlaceRef(
+ StopPlaceRefStructure stopPlaceRef
+ ) {
+ this.stopPlaceRef = stopPlaceRef;
+ return this;
+ }
+
+ public CreatePassengerStopAssignment withQuayRef(QuayRefStructure QuayRef) {
+ this.quayRef = QuayRef;
+ return this;
+ }
+
+ public PassengerStopAssignment create() {
+ return new PassengerStopAssignment()
+ .withId(ref())
+ .withScheduledStopPointRef(createJaxbElement(scheduleStopPointRef))
+ .withQuayRef(createJaxbElement(quayRef))
+ .withStopPlaceRef(createJaxbElement(stopPlaceRef));
+ }
+ }
+
+ public static class CreateDatedServiceJourney
+ extends CreateEntity {
+
+ private final CreateOperatingDay operatingDayRef;
+ private final CreateServiceJourney serviceJourneyRef;
+ private CreateDatedServiceJourney datedServiceJourneyRef;
+ private ServiceAlterationEnumeration serviceAlteration;
+
+ public CreateDatedServiceJourney(
+ int id,
+ CreateServiceJourney serviceJourneyRef,
+ CreateOperatingDay operatingDayRef
+ ) {
+ super(id);
+ this.serviceJourneyRef = serviceJourneyRef;
+ this.operatingDayRef = operatingDayRef;
+ }
+
+ public CreateDatedServiceJourney withServiceAlteration(
+ ServiceAlterationEnumeration serviceAlteration
+ ) {
+ this.serviceAlteration = serviceAlteration;
+ return this;
+ }
+
+ public CreateDatedServiceJourney withDatedServiceJourneyRef(
+ CreateDatedServiceJourney datedServiceJourneyRef
+ ) {
+ this.datedServiceJourneyRef = datedServiceJourneyRef;
+ return this;
+ }
+
+ public DatedServiceJourney create() {
+ DatedServiceJourney datedServiceJourney = new DatedServiceJourney()
+ .withId(ref());
+
+ Collection> journeyRefs =
+ new ArrayList<>();
+ journeyRefs.add(
+ createJaxbElement(
+ new ServiceJourneyRefStructure().withRef(serviceJourneyRef.ref())
+ )
+ );
+ if (datedServiceJourneyRef != null) {
+ journeyRefs.add(
+ createJaxbElement(
+ new DatedServiceJourneyRefStructure()
+ .withRef(datedServiceJourneyRef.ref())
+ )
+ );
+ }
+
+ return datedServiceJourney
+ .withJourneyRef(journeyRefs)
+ .withOperatingDayRef(
+ new OperatingDayRefStructure().withRef(operatingDayRef.ref())
+ )
+ .withServiceAlteration(serviceAlteration);
+ }
+ }
+
+ public static class CreateOperatingDay extends CreateEntity {
+
+ private final LocalDate calendarDate;
+
+ public CreateOperatingDay(int id, LocalDate calendarDate) {
+ super(id);
+ this.calendarDate = calendarDate;
+ }
+
+ public OperatingDay create() {
+ return new OperatingDay()
+ .withId(ref())
+ .withCalendarDate(calendarDate.atStartOfDay());
+ }
+ }
+
+ public static class CreateDayType extends CreateEntity {
+
+ public CreateDayType(int id) {
+ super(id);
+ }
+
+ public DayType create() {
+ return new DayType().withId(ref());
+ }
+ }
+
+ public static class CreateDayTypeAssignment
+ extends CreateEntity {
+
+ private CreateDayType DayTypeRef;
+ private LocalDate date;
+ private CreateOperatingDay operatingDayRef;
+ // TODO: create CreateOperatingPeriod
+ private String operatingPeriodRef;
+
+ public CreateDayTypeAssignment(int id) {
+ super(id);
+ }
+
+ public CreateDayTypeAssignment withDate(LocalDate date) {
+ this.date = date;
+ this.operatingDayRef = null;
+ this.operatingPeriodRef = null;
+ return this;
+ }
+
+ public CreateDayTypeAssignment withOperatingDayRef(
+ CreateOperatingDay operatingDayRef
+ ) {
+ this.operatingDayRef = operatingDayRef;
+ this.date = null;
+ this.operatingPeriodRef = null;
+ return this;
+ }
+
+ public CreateDayTypeAssignment withOperatingPeriodRef(
+ String operatingPeriodRef
+ ) {
+ this.operatingPeriodRef = operatingPeriodRef;
+ this.date = null;
+ this.operatingDayRef = null;
+ return this;
+ }
+
+ public DayTypeAssignment create() {
+ DayTypeAssignment dayTypeAssignment = new DayTypeAssignment()
+ .withId(ref());
+
+ Optional
+ .ofNullable(date)
+ .ifPresent(d -> dayTypeAssignment.withDate(d.atStartOfDay()));
+
+ Optional
+ .ofNullable(operatingDayRef)
+ .ifPresent(ref ->
+ dayTypeAssignment.withOperatingDayRef(
+ new OperatingDayRefStructure().withRef(ref.ref())
+ )
+ );
+
+ Optional
+ .ofNullable(operatingPeriodRef)
+ .ifPresent(ref ->
+ dayTypeAssignment.withOperatingPeriodRef(
+ createJaxbElement(new OperatingPeriodRefStructure().withRef(ref))
+ )
+ );
+
+ return dayTypeAssignment;
+ }
+ }
+
+ public abstract static class CreateGenericLine<
+ T extends Line_VersionStructure
+ >
+ extends CreateEntity {
+
+ protected AllVehicleModesOfTransportEnumeration transportMode;
+ protected TransportSubmodeStructure transportSubmode;
+
+ public CreateGenericLine(int id) {
+ super(id);
+ }
+
+ public CreateGenericLine withTransportMode(
+ AllVehicleModesOfTransportEnumeration transportMode
+ ) {
+ this.transportMode = transportMode;
+ return this;
+ }
+
+ public CreateGenericLine withTransportSubmode(
+ TransportSubmodeStructure transportSubmode
+ ) {
+ this.transportSubmode = transportSubmode;
+ return this;
+ }
+ }
+
+ public static class CreateLine extends CreateGenericLine {
+
+ public CreateLine(int id) {
+ super(id);
+ }
+
+ public Line create() {
+ return new Line()
+ .withId(ref())
+ .withName(new MultilingualString().withValue("Line " + id))
+ .withTransportMode(transportMode)
+ .withTransportSubmode(transportSubmode);
+ }
+ }
+
+ public static class CreateFlexibleLine
+ extends CreateGenericLine {
+
+ private FlexibleLineTypeEnumeration flexibleLineType;
+
+ public CreateFlexibleLine(int id) {
+ super(id);
+ }
+
+ public CreateFlexibleLine withFlexibleLineType(
+ FlexibleLineTypeEnumeration flexibleLineType
+ ) {
+ this.flexibleLineType = flexibleLineType;
+ return this;
+ }
+
+ public FlexibleLine create() {
+ return new FlexibleLine()
+ .withId(ref())
+ .withFlexibleLineType(flexibleLineType)
+ .withName(new MultilingualString().withValue("FlexibleLine " + id))
+ .withTransportMode(transportMode)
+ .withTransportSubmode(transportSubmode);
+ }
+ }
+
+ public static class CreateRoute extends CreateEntity {
+
+ private final CreateGenericLine extends Line_VersionStructure> lineRef;
+
+ public CreateRoute(
+ int id,
+ CreateGenericLine extends Line_VersionStructure> lineRef
+ ) {
+ super(id);
+ this.lineRef = lineRef;
+ }
+
+ public Route create() {
+ return new Route()
+ .withId(ref())
+ .withLineRef(
+ createJaxbElement(new LineRefStructure().withRef(lineRef.ref()))
+ );
+ }
+ }
+
+ public static class CreateJourneyPattern
+ extends CreateEntity {
+
+ private CreateRoute routeRef;
+
+ private final List stopPointsInJourneyPatterns =
+ new ArrayList<>();
+
+ private final List serviceLinksInJourneyPatterns =
+ new ArrayList<>();
+
+ private boolean noServiceLinksInJourneyPattern = false;
+
+ public CreateJourneyPattern(int id) {
+ super(id);
+ }
+
+ public CreateJourneyPattern withRoute(CreateRoute routeRef) {
+ this.routeRef = routeRef;
+ return this;
+ }
+
+ public CreateJourneyPattern withNoServiceLinksInJourneyPattern() {
+ this.noServiceLinksInJourneyPattern = true;
+ return this;
+ }
+
+ /**
+ * Adds a new stop point in the journey pattern with the given id
+ *
+ * @param id the id of the stop point in the journey pattern
+ * @return CreateStopPointInJourneyPattern
+ */
+ public CreateStopPointInJourneyPattern createStopPointInJourneyPattern(
+ int id
+ ) {
+ CreateStopPointInJourneyPattern createStopPointInJourneyPattern =
+ new CreateStopPointInJourneyPattern(id)
+ .withOrder(id)
+ .withScheduledStopPointRef(createScheduledStopPointRef(id));
+ stopPointsInJourneyPatterns.add(createStopPointInJourneyPattern);
+ return createStopPointInJourneyPattern;
+ }
+
+ /**
+ * Adds a new service link in the journey pattern with the given id
+ *
+ * @param id the id of the service link in the journey pattern
+ * @return CreateLinkInJourneyPattern
+ */
+ public CreateLinkInJourneyPattern createServiceLinkInJourneyPattern(
+ int id
+ ) {
+ CreateLinkInJourneyPattern createLinkInJourneyPattern =
+ new CreateLinkInJourneyPattern(id);
+ serviceLinksInJourneyPatterns.add(createLinkInJourneyPattern);
+ return createLinkInJourneyPattern;
+ }
+
+ /**
+ * Adds numberOfStopPointInJourneyPattern new stop points in the journey pattern
+ * The stop points will have ids from 1 to numberOfStopPointInJourneyPattern
+ *
+ * @param numberOfStopPointInJourneyPattern the number of stop points to create
+ * @return List of CreateStopPointInJourneyPattern created
+ */
+ public List createStopPointsInJourneyPattern(
+ int numberOfStopPointInJourneyPattern
+ ) {
+ List stopPointsInJourneyPatterns =
+ IntStream
+ .rangeClosed(1, numberOfStopPointInJourneyPattern)
+ .mapToObj(index -> {
+ CreateStopPointInJourneyPattern createStopPointInJourneyPattern =
+ new CreateStopPointInJourneyPattern(index)
+ .withOrder(index)
+ .withScheduledStopPointRef(createScheduledStopPointRef(index))
+ .withForBoarding(index == 1) // first stop point
+ .withForAlighting(index == numberOfStopPointInJourneyPattern); // last stop point
+
+ // Setting destination display id for first and last stop point
+ if (index == 1 || index == numberOfStopPointInJourneyPattern) {
+ createStopPointInJourneyPattern.withDestinationDisplayId(
+ createDestinationDisplayRef(index)
+ );
+ }
+
+ return createStopPointInJourneyPattern;
+ })
+ .toList();
+
+ this.stopPointsInJourneyPatterns.addAll(stopPointsInJourneyPatterns);
+ return stopPointsInJourneyPatterns;
+ }
+
+ /**
+ * Adds numberOfServiceLinksInJourneyPattern new service links in the journey pattern
+ * The service links will have ids from 1 to numberOfServiceLinksInJourneyPattern
+ *
+ * @param numberOfServiceLinksInJourneyPattern the number of service links to create
+ * @return List of CreateLinkInJourneyPattern created
+ */
+ public List createServiceLinksInJourneyPattern(
+ int numberOfServiceLinksInJourneyPattern
+ ) {
+ List linksInJourneyPatterns = IntStream
+ .range(0, numberOfServiceLinksInJourneyPattern)
+ .mapToObj(index ->
+ new CreateLinkInJourneyPattern(index + 1)
+ .withOrder(index + 1)
+ .withServiceLinkRef(createServiceLinkRef(index + 1))
+ )
+ .toList();
+
+ serviceLinksInJourneyPatterns.addAll(linksInJourneyPatterns);
+ return linksInJourneyPatterns;
+ }
+
+ public JourneyPattern create() {
+ JourneyPattern journeyPattern = new JourneyPattern().withId(ref());
+
+ if (routeRef != null) {
+ journeyPattern.withRouteRef(
+ new RouteRefStructure().withRef(routeRef.ref())
+ );
+ }
+
+ journeyPattern.withPointsInSequence(
+ new PointsInJourneyPattern_RelStructure()
+ .withPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern(
+ this.stopPointsInJourneyPatterns.isEmpty()
+ ? List.of()
+ : this.stopPointsInJourneyPatterns.stream()
+ .map(CreateStopPointInJourneyPattern::create)
+ .map(PointInLinkSequence_VersionedChildStructure.class::cast)
+ .toList()
+ )
+ );
+
+ if (!noServiceLinksInJourneyPattern) {
+ journeyPattern.withLinksInSequence(
+ new LinksInJourneyPattern_RelStructure()
+ .withServiceLinkInJourneyPatternOrTimingLinkInJourneyPattern(
+ this.serviceLinksInJourneyPatterns.isEmpty()
+ ? List.of()
+ : this.serviceLinksInJourneyPatterns.stream()
+ .map(CreateLinkInJourneyPattern::create)
+ .map(LinkInLinkSequence_VersionedChildStructure.class::cast)
+ .toList()
+ )
+ );
+ }
+
+ return journeyPattern;
+ }
+ }
+
+ public static class CreateStopPointInJourneyPattern
+ extends CreateEntity {
+
+ private int order = 1;
+ private ScheduledStopPointRefStructure scheduledStopPointRef;
+ private DestinationDisplayRefStructure destinationDisplayRef;
+ private boolean forAlighting = false;
+ private boolean forBoarding = false;
+
+ public CreateStopPointInJourneyPattern(int id) {
+ super(id);
+ }
+
+ public CreateStopPointInJourneyPattern withOrder(int order) {
+ this.order = order;
+ return this;
+ }
+
+ public CreateStopPointInJourneyPattern withScheduledStopPointRef(
+ ScheduledStopPointRefStructure scheduledStopPointRef
+ ) {
+ this.scheduledStopPointRef = scheduledStopPointRef;
+ return this;
+ }
+
+ public CreateStopPointInJourneyPattern withDestinationDisplayId(
+ DestinationDisplayRefStructure destinationDisplayRef
+ ) {
+ this.destinationDisplayRef = destinationDisplayRef;
+ return this;
+ }
+
+ public CreateStopPointInJourneyPattern withForAlighting(
+ boolean forAlighting
+ ) {
+ this.forAlighting = forAlighting;
+ return this;
+ }
+
+ public CreateStopPointInJourneyPattern withForBoarding(
+ boolean forBoarding
+ ) {
+ this.forBoarding = forBoarding;
+ return this;
+ }
+
+ public StopPointInJourneyPattern create() {
+ StopPointInJourneyPattern stopPointInJourneyPattern =
+ new StopPointInJourneyPattern()
+ .withId(ref())
+ .withOrder(BigInteger.valueOf(order));
+
+ if (scheduledStopPointRef != null) {
+ stopPointInJourneyPattern.withScheduledStopPointRef(
+ createJaxbElement(scheduledStopPointRef)
+ );
+ }
+
+ if (destinationDisplayRef != null) {
+ stopPointInJourneyPattern.setDestinationDisplayRef(
+ createJaxbElement(destinationDisplayRef).getValue()
+ );
+ }
+
+ stopPointInJourneyPattern.withForAlighting(forAlighting);
+ stopPointInJourneyPattern.withForBoarding(forBoarding);
+
+ return stopPointInJourneyPattern;
+ }
+ }
+
+ public static class CreateLinkInJourneyPattern
+ extends CreateEntity {
+
+ private int order = 1;
+ private ServiceLinkRefStructure serviceLinkRef;
+
+ public CreateLinkInJourneyPattern(int id) {
+ super(id);
+ }
+
+ public CreateLinkInJourneyPattern withOrder(int order) {
+ this.order = order;
+ return this;
+ }
+
+ public CreateLinkInJourneyPattern withServiceLinkRef(
+ ServiceLinkRefStructure serviceLinkRef
+ ) {
+ this.serviceLinkRef = serviceLinkRef;
+ return this;
+ }
+
+ public LinkInJourneyPattern create() {
+ return new LinkInJourneyPattern()
+ .withId(ref())
+ .withOrder(BigInteger.valueOf(order))
+ .withServiceLinkRef(serviceLinkRef);
+ }
+ }
+
+ public static class CreateDeadRun extends CreateEntity {
+
+ private final CreateGenericLine extends Line_VersionStructure> lineRef;
+ private final CreateJourneyPattern journeyPattern;
+ private final List timetabledPassingTimes =
+ new ArrayList<>();
+
+ public CreateDeadRun(
+ int id,
+ CreateGenericLine extends Line_VersionStructure> lineRef,
+ CreateJourneyPattern journeyPattern
+ ) {
+ super(id);
+ this.lineRef = lineRef;
+ this.journeyPattern = journeyPattern;
+ }
+
+ /**
+ * Adds a new timetabled passing time with the given id
+ *
+ * @param id the id of the timetabled passing time
+ * @param createStopPointInJourneyPattern the stop point in the journey pattern ref for the timetabled passing time
+ * @return CreateTimetabledPassingTime
+ */
+ public CreateTimetabledPassingTime createTimetabledPassingTime(
+ int id,
+ CreateStopPointInJourneyPattern createStopPointInJourneyPattern
+ ) {
+ CreateTimetabledPassingTime createTimetabledPassingTime =
+ new CreateTimetabledPassingTime(id, createStopPointInJourneyPattern);
+ timetabledPassingTimes.add(createTimetabledPassingTime);
+ return createTimetabledPassingTime;
+ }
+
+ public DeadRun create() {
+ DeadRun deadRun = new DeadRun()
+ .withId(ref())
+ .withLineRef(
+ createJaxbElement(new LineRefStructure().withRef(lineRef.ref()))
+ )
+ .withDayTypes(createEveryDayRefs())
+ .withJourneyPatternRef(
+ createJaxbElement(
+ new JourneyPatternRefStructure().withRef(journeyPattern.ref())
+ )
+ );
+
+ deadRun.withPassingTimes(
+ new TimetabledPassingTimes_RelStructure()
+ .withTimetabledPassingTime(
+ timetabledPassingTimes
+ .stream()
+ .map(CreateTimetabledPassingTime::create)
+ .toList()
+ )
+ );
+
+ return deadRun;
+ }
+ }
+
+ public static class CreateServiceJourney
+ extends CreateEntity
+ implements CreateRef {
+
+ private final CreateGenericLine extends Line_VersionStructure> line;
+ private final CreateJourneyPattern journeyPattern;
+ private final List timetabledPassingTimes =
+ new ArrayList<>();
+ private AllVehicleModesOfTransportEnumeration transportMode;
+ private TransportSubmodeStructure transportSubmode;
+
+ public CreateServiceJourney(
+ int id,
+ CreateGenericLine extends Line_VersionStructure> line,
+ CreateJourneyPattern journeyPattern
+ ) {
+ super(id);
+ this.line = line;
+ this.journeyPattern = journeyPattern;
+ }
+
+ public VehicleJourneyRefStructure refObject() {
+ return NetexEntitiesTestFactory.createServiceJourneyRef(id);
+ }
+
+ /**
+ * Adds a new timetabled passing time with the given id
+ *
+ * @param id the id of the timetabled passing time
+ * @param createStopPointInJourneyPattern the stop point in the journey pattern ref for the timetabled passing time
+ * @return CreateTimetabledPassingTime
+ */
+ public CreateTimetabledPassingTime createTimetabledPassingTime(
+ int id,
+ CreateStopPointInJourneyPattern createStopPointInJourneyPattern
+ ) {
+ CreateTimetabledPassingTime createTimetabledPassingTime =
+ new CreateTimetabledPassingTime(id, createStopPointInJourneyPattern);
+ timetabledPassingTimes.add(createTimetabledPassingTime);
+ return createTimetabledPassingTime;
+ }
+
+ public CreateServiceJourney withTransportMode(
+ AllVehicleModesOfTransportEnumeration transportMode
+ ) {
+ this.transportMode = transportMode;
+ return this;
+ }
+
+ public CreateServiceJourney withTransportSubmode(
+ TransportSubmodeStructure transportSubmode
+ ) {
+ this.transportSubmode = transportSubmode;
+ return this;
+ }
+
+ public ServiceJourney create() {
+ ServiceJourney serviceJourney = new ServiceJourney()
+ .withId(ref())
+ .withLineRef(
+ createJaxbElement(new LineRefStructure().withRef(line.ref()))
+ )
+ .withDayTypes(createEveryDayRefs())
+ .withJourneyPatternRef(
+ createJaxbElement(
+ new JourneyPatternRefStructure().withRef(journeyPattern.ref())
+ )
+ );
+
+ serviceJourney.withPassingTimes(
+ new TimetabledPassingTimes_RelStructure()
+ .withTimetabledPassingTime(
+ timetabledPassingTimes
+ .stream()
+ .map(CreateTimetabledPassingTime::create)
+ .toList()
+ )
+ );
+
+ if (transportMode != null) {
+ serviceJourney.withTransportMode(transportMode);
+ }
+
+ if (transportSubmode != null) {
+ serviceJourney.withTransportSubmode(transportSubmode);
+ }
+
+ return serviceJourney;
+ }
+ }
+
+ public static class CreateServiceJourneyInterchange
+ extends CreateEntity {
+
+ private boolean guaranteed = true;
+ private Duration maximumWaitTime;
+ private ScheduledStopPointRefStructure fromPointRef;
+ private ScheduledStopPointRefStructure toPointRef;
+ private VehicleJourneyRefStructure fromJourneyRef;
+ private VehicleJourneyRefStructure toJourneyRef;
+
+ public CreateServiceJourneyInterchange(int id) {
+ super(id);
+ }
+
+ public CreateServiceJourneyInterchange withGuaranteed(boolean guaranteed) {
+ this.guaranteed = guaranteed;
+ return this;
+ }
+
+ public CreateServiceJourneyInterchange withMaximumWaitTime(
+ Duration maximumWaitTime
+ ) {
+ this.maximumWaitTime = maximumWaitTime;
+ return this;
+ }
+
+ public CreateServiceJourneyInterchange withFromPointRef(
+ ScheduledStopPointRefStructure fromPointRef
+ ) {
+ this.fromPointRef = fromPointRef;
+ return this;
+ }
+
+ public CreateServiceJourneyInterchange withToPointRef(
+ ScheduledStopPointRefStructure toPointRef
+ ) {
+ this.toPointRef = toPointRef;
+ return this;
+ }
+
+ public CreateServiceJourneyInterchange withFromJourneyRef(
+ VehicleJourneyRefStructure fromJourneyRef
+ ) {
+ this.fromJourneyRef = fromJourneyRef;
+ return this;
+ }
+
+ public CreateServiceJourneyInterchange withToJourneyRef(
+ VehicleJourneyRefStructure toJourneyRef
+ ) {
+ this.toJourneyRef = toJourneyRef;
+ return this;
+ }
+
+ public ServiceJourneyInterchange create() {
+ ServiceJourneyInterchange serviceJourneyInterchange =
+ new ServiceJourneyInterchange()
+ .withId(ref())
+ .withGuaranteed(guaranteed)
+ .withMaximumWaitTime(maximumWaitTime);
+
+ if (fromPointRef != null) {
+ serviceJourneyInterchange.withFromPointRef(fromPointRef);
+ }
+
+ if (toPointRef != null) {
+ serviceJourneyInterchange.withToPointRef(toPointRef);
+ }
+
+ if (fromJourneyRef != null) {
+ serviceJourneyInterchange.withFromJourneyRef(fromJourneyRef);
+ }
+
+ if (toJourneyRef != null) {
+ serviceJourneyInterchange.withToJourneyRef(toJourneyRef);
+ }
+
+ return serviceJourneyInterchange;
+ }
+ }
+
+ public static class CreateTimetabledPassingTime
+ extends CreateEntity {
+
+ private final CreateStopPointInJourneyPattern pointInJourneyPattern;
+ private LocalTime departureTime;
+ private LocalTime arrivalTime;
+ private LocalTime earliestDepartureTime;
+ private LocalTime latestArrivalTime;
+
+ public CreateTimetabledPassingTime(
+ int id,
+ CreateStopPointInJourneyPattern pointInJourneyPattern
+ ) {
+ super(id);
+ this.pointInJourneyPattern = pointInJourneyPattern;
+ }
+
+ public CreateTimetabledPassingTime withDepartureTime(
+ LocalTime departureTime
+ ) {
+ this.departureTime = departureTime;
+ return this;
+ }
+
+ public CreateTimetabledPassingTime withArrivalTime(LocalTime arrivalTime) {
+ this.arrivalTime = arrivalTime;
+ return this;
+ }
+
+ public CreateTimetabledPassingTime withEarliestDepartureTime(
+ LocalTime earliestDepartureTime
+ ) {
+ this.earliestDepartureTime = earliestDepartureTime;
+ return this;
+ }
+
+ public CreateTimetabledPassingTime withLatestArrivalTime(
+ LocalTime latestArrivalTime
+ ) {
+ this.latestArrivalTime = latestArrivalTime;
+ return this;
+ }
+
+ public TimetabledPassingTime create() {
+ return new TimetabledPassingTime()
+ .withId(ref())
+ .withDepartureTime(departureTime)
+ .withArrivalTime(arrivalTime)
+ .withEarliestDepartureTime(earliestDepartureTime)
+ .withLatestArrivalTime(latestArrivalTime)
+ .withPointInJourneyPatternRef(
+ createJaxbElement(
+ new StopPointInJourneyPatternRefStructure()
+ .withRef(pointInJourneyPattern.ref())
+ )
+ );
+ }
+ }
+
+ public static class CreateServiceLink extends CreateEntity {
+
+ private ScheduledStopPointRefStructure fromScheduledStopPointRef;
+ private ScheduledStopPointRefStructure toScheduledStopPointRef;
+ private LinkSequenceProjection_VersionStructure linkSequenceProjection_VersionStructure;
+
+ public CreateServiceLink(int id) {
+ super(id);
+ }
+
+ public CreateServiceLink withFromScheduledStopPointRef(
+ ScheduledStopPointRefStructure fromScheduledStopPointRef
+ ) {
+ this.fromScheduledStopPointRef = fromScheduledStopPointRef;
+ return this;
+ }
+
+ public CreateServiceLink withToScheduledStopPointRef(
+ ScheduledStopPointRefStructure toScheduledStopPointRef
+ ) {
+ this.toScheduledStopPointRef = toScheduledStopPointRef;
+ return this;
+ }
+
+ public CreateServiceLink withLineStringList(
+ List lineStringPositions
+ ) {
+ this.linkSequenceProjection_VersionStructure =
+ new LinkSequenceProjection_VersionStructure()
+ .withId("TST:ServiceLinkProjection:" + id)
+ .withLineString(
+ new LineStringType()
+ .withPosList(
+ new DirectPositionListType().withValue(lineStringPositions)
+ )
+ );
+ return this;
+ }
+
+ public CreateServiceLink withLineStringPositions(
+ List lineStringPositions
+ ) {
+ this.linkSequenceProjection_VersionStructure =
+ new LinkSequenceProjection_VersionStructure()
+ .withId("TST:ServiceLinkProjection:" + id)
+ .withLineString(
+ new LineStringType()
+ .withPosOrPointProperty(
+ lineStringPositions.toArray(Object[]::new)
+ )
+ );
+ return this;
+ }
+
+ public ServiceLink create() {
+ return new ServiceLink()
+ .withId(ref())
+ .withFromPointRef(fromScheduledStopPointRef)
+ .withToPointRef(toScheduledStopPointRef)
+ .withProjections(
+ new Projections_RelStructure()
+ .withProjectionRefOrProjection(
+ createJaxbElement(linkSequenceProjection_VersionStructure)
+ )
+ );
+ }
+ }
+
+ private static DayTypeRefs_RelStructure createEveryDayRefs() {
+ return new DayTypeRefs_RelStructure()
+ .withDayTypeRef(Collections.singleton(createEveryDayRef()));
+ }
+
+ private static JAXBElement createEveryDayRef() {
+ return createJaxbElement(
+ new DayTypeRefStructure().withRef(EVERYDAY.getId())
+ );
+ }
+}
diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/TestCommonDataRepository.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestCommonDataRepository.java
new file mode 100644
index 00000000..153de38f
--- /dev/null
+++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestCommonDataRepository.java
@@ -0,0 +1,65 @@
+package org.entur.netex.validation.test.jaxb.support;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.entur.netex.validation.validator.jaxb.CommonDataRepository;
+import org.entur.netex.validation.validator.model.FromToScheduledStopPointId;
+import org.entur.netex.validation.validator.model.QuayId;
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.entur.netex.validation.validator.model.ServiceLinkId;
+
+/**
+ * CommonRepository implementation for tests.
+ */
+public class TestCommonDataRepository implements CommonDataRepository {
+
+ private final Map quayForScheduledStopPoint;
+
+ TestCommonDataRepository(
+ Map quayForScheduledStopPoint
+ ) {
+ this.quayForScheduledStopPoint = quayForScheduledStopPoint;
+ }
+
+ /**
+ * Return a common data repository that maps ScheduledStopPoint #i to Quay #i
+ */
+ public static CommonDataRepository of(int numScheduledStopPoints) {
+ Map stopPointIdQuayIdMap = IntStream
+ .rangeClosed(1, numScheduledStopPoints)
+ .boxed()
+ .collect(
+ Collectors.toUnmodifiableMap(
+ index -> new ScheduledStopPointId("TST:ScheduledStopPoint:" + index),
+ index -> new QuayId("TST:Quay:" + index)
+ )
+ );
+
+ return new TestCommonDataRepository(stopPointIdQuayIdMap);
+ }
+
+ @Override
+ public boolean hasSharedScheduledStopPoints(String validationReportId) {
+ return !quayForScheduledStopPoint.isEmpty();
+ }
+
+ @Override
+ public QuayId quayIdForScheduledStopPoint(
+ ScheduledStopPointId scheduledStopPointId,
+ String validationReportId
+ ) {
+ if (scheduledStopPointId == null) {
+ return null;
+ }
+ return quayForScheduledStopPoint.get(scheduledStopPointId);
+ }
+
+ @Override
+ public FromToScheduledStopPointId fromToScheduledStopPointIdForServiceLink(
+ ServiceLinkId serviceLinkId,
+ String validationReportId
+ ) {
+ return null;
+ }
+}
diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/TestStopPlaceRepository.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestStopPlaceRepository.java
new file mode 100644
index 00000000..246913b2
--- /dev/null
+++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestStopPlaceRepository.java
@@ -0,0 +1,177 @@
+package org.entur.netex.validation.test.jaxb.support;
+
+import jakarta.annotation.Nullable;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.entur.netex.validation.validator.jaxb.StopPlaceRepository;
+import org.entur.netex.validation.validator.model.QuayCoordinates;
+import org.entur.netex.validation.validator.model.QuayId;
+import org.entur.netex.validation.validator.model.StopPlaceId;
+import org.entur.netex.validation.validator.model.TransportModeAndSubMode;
+import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration;
+import org.rutebanken.netex.model.BusSubmodeEnumeration;
+import org.rutebanken.netex.model.CoachSubmodeEnumeration;
+import org.rutebanken.netex.model.MultilingualString;
+import org.rutebanken.netex.model.Quay;
+import org.rutebanken.netex.model.RailSubmodeEnumeration;
+import org.rutebanken.netex.model.StopPlace;
+
+/**
+ * StopPlaceRepository implementation for tests.
+ */
+public class TestStopPlaceRepository implements StopPlaceRepository {
+
+ private final Map stopPlaces;
+ private final Map quays;
+ private final Map stopPlaceForQuay;
+
+ TestStopPlaceRepository(Map quayforStopPlace) {
+ this.quays =
+ quayforStopPlace
+ .values()
+ .stream()
+ .collect(
+ Collectors.toUnmodifiableMap(QuayId::ofValidId, Function.identity())
+ );
+ this.stopPlaces =
+ quayforStopPlace
+ .keySet()
+ .stream()
+ .collect(
+ Collectors.toUnmodifiableMap(
+ stopPlace -> new StopPlaceId(stopPlace.getId()),
+ Function.identity()
+ )
+ );
+ this.stopPlaceForQuay =
+ quayforStopPlace
+ .entrySet()
+ .stream()
+ .collect(
+ Collectors.toUnmodifiableMap(
+ entry -> QuayId.ofValidId(entry.getValue()),
+ Map.Entry::getKey
+ )
+ );
+ }
+
+ /**
+ * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode
+ * bus/local bus
+ */
+ public static StopPlaceRepository ofLocalBusStops(int numStops) {
+ return ofTransportMode(
+ numStops,
+ stopPlace ->
+ stopPlace
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ );
+ }
+
+ /**
+ * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode
+ * bus/rail replacement bus
+ */
+ public static StopPlaceRepository ofRailReplacementBusStops(int numStops) {
+ return ofTransportMode(
+ numStops,
+ stopPlace ->
+ stopPlace
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withBusSubmode(BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS)
+ );
+ }
+
+ /**
+ * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode
+ * coach/national coach
+ */
+
+ public static StopPlaceRepository ofNationalCoachStops(int numStops) {
+ return ofTransportMode(
+ numStops,
+ stopPlace ->
+ stopPlace
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.COACH)
+ .withCoachSubmode(CoachSubmodeEnumeration.NATIONAL_COACH)
+ );
+ }
+
+ /**
+ * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode
+ * rail/local
+ */
+ public static StopPlaceRepository ofLocalTrainStops(int numStops) {
+ return ofTransportMode(
+ numStops,
+ stopPlace ->
+ stopPlace
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL)
+ .withRailSubmode(RailSubmodeEnumeration.LOCAL)
+ );
+ }
+
+ /**
+ * Return a stop place repository containing numStops stop places and numStops quays where the transport modes and
+ * submodes are missing
+ */
+ public static StopPlaceRepository ofMissingTransportModeAndSubMode(
+ int numStops
+ ) {
+ return ofTransportMode(numStops, Function.identity());
+ }
+
+ private static StopPlaceRepository ofTransportMode(
+ int numStops,
+ Function setTransportMode
+ ) {
+ Map stopPlaceQuayMap = IntStream
+ .rangeClosed(1, numStops)
+ .boxed()
+ .collect(
+ Collectors.toUnmodifiableMap(
+ stopIndex ->
+ setTransportMode.apply(
+ new StopPlace()
+ .withId("TST:StopPlace:" + stopIndex)
+ .withName(
+ new MultilingualString().withValue("StopPlace " + stopIndex)
+ )
+ ),
+ quayIndex -> new Quay().withId("TST:Quay:" + quayIndex)
+ )
+ );
+ return new TestStopPlaceRepository(stopPlaceQuayMap);
+ }
+
+ @Override
+ public boolean hasStopPlaceId(StopPlaceId stopPlaceId) {
+ return stopPlaces.containsKey(stopPlaceId);
+ }
+
+ @Override
+ public boolean hasQuayId(QuayId quayId) {
+ return quays.containsKey(quayId);
+ }
+
+ @Nullable
+ @Override
+ public TransportModeAndSubMode getTransportModesForQuayId(QuayId quayId) {
+ return TransportModeAndSubMode.of(stopPlaceForQuay.get(quayId));
+ }
+
+ @Nullable
+ @Override
+ public QuayCoordinates getCoordinatesForQuayId(QuayId quayId) {
+ return QuayCoordinates.of(quays.get(quayId));
+ }
+
+ @Nullable
+ @Override
+ public String getStopPlaceNameForQuayId(QuayId quayId) {
+ return stopPlaceForQuay.get(quayId).getName().getValue();
+ }
+}
diff --git a/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidatorTest.java b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidatorTest.java
new file mode 100644
index 00000000..d6493519
--- /dev/null
+++ b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidatorTest.java
@@ -0,0 +1,390 @@
+package org.entur.netex.validation.validator.jaxb.rules.servicejourney.passingtime;
+
+import java.time.LocalTime;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+import org.entur.netex.index.api.NetexEntitiesIndex;
+import org.entur.netex.validation.test.jaxb.support.NetexEntitiesTestFactory;
+import org.entur.netex.validation.test.jaxb.support.TestCommonDataRepository;
+import org.entur.netex.validation.test.jaxb.support.TestStopPlaceRepository;
+import org.entur.netex.validation.validator.ValidationIssue;
+import org.entur.netex.validation.validator.ValidationRule;
+import org.entur.netex.validation.validator.jaxb.JAXBValidationContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.rutebanken.netex.model.ScheduledStopPointRefStructure;
+
+class NonIncreasingPassingTimeValidatorTest {
+
+ private static final String TEST_CODESPACE = "ENT";
+ private static final String TEST_LINE_XML_FILE = "line.xml";
+ private static final String VALIDATION_REPORT_ID = "Test1122";
+
+ private static final int NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN = 4;
+
+ private NonIncreasingPassingTimeValidator validator;
+ private NetexEntitiesTestFactory netexEntitiesTestFactory;
+ private List timetabledPassingTimes;
+ private List departureTimes;
+ private List scheduledStopPointRefs;
+
+ @BeforeEach
+ void setup() {
+ validator = new NonIncreasingPassingTimeValidator();
+
+ netexEntitiesTestFactory = new NetexEntitiesTestFactory();
+
+ NetexEntitiesTestFactory.CreateJourneyPattern createJourneyPattern =
+ netexEntitiesTestFactory.createJourneyPattern();
+
+ NetexEntitiesTestFactory.CreateServiceJourney createServiceJourney =
+ netexEntitiesTestFactory.createServiceJourney(createJourneyPattern);
+
+ scheduledStopPointRefs =
+ IntStream
+ .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN)
+ .mapToObj(NetexEntitiesTestFactory::createScheduledStopPointRef)
+ .toList();
+
+ List stopPointInJourneyPatterns =
+ IntStream
+ .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN)
+ .mapToObj(index ->
+ createJourneyPattern
+ .createStopPointInJourneyPattern(index)
+ .withScheduledStopPointRef(scheduledStopPointRefs.get(index - 1))
+ )
+ .toList();
+
+ departureTimes =
+ IntStream
+ .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN)
+ .mapToObj(index -> LocalTime.of(5, index * 5))
+ .toList();
+
+ timetabledPassingTimes =
+ IntStream
+ .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN)
+ .mapToObj(index ->
+ createServiceJourney
+ .createTimetabledPassingTime(
+ index,
+ stopPointInJourneyPatterns.get(index - 1)
+ )
+ .withDepartureTime(departureTimes.get(index - 1))
+ )
+ .toList();
+ }
+
+ @Test
+ void testValidateServiceJourneyWithRegularStop() {
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+ assertNoIssue(netexEntitiesIndex);
+ }
+
+ @Test
+ void testValidateServiceJourneyWithRegularStopMissingTime() {
+ // remove arrival time and departure time for the first passing time
+ timetabledPassingTimes.get(0).withDepartureTime(null).withArrivalTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME
+ );
+ }
+
+ @Test
+ void testValidateServiceJourneyWithRegularStopInconsistentTime() {
+ // set arrival time after departure time for the first passing time
+ timetabledPassingTimes
+ .get(0)
+ .withArrivalTime(departureTimes.get(0).plusMinutes(1));
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_INCONSISTENT_TIME
+ );
+ }
+
+ @Test
+ void testValidateServiceJourneyWithAreaStop() {
+ // remove arrival time and departure time and add flex window
+ timetabledPassingTimes
+ .get(0)
+ .withDepartureTime(null)
+ .withArrivalTime(null)
+ .withEarliestDepartureTime(LocalTime.MIDNIGHT)
+ .withLatestArrivalTime(LocalTime.MIDNIGHT.plusMinutes(1));
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ assertNoIssue(netexEntitiesIndex);
+ }
+
+ @Test
+ void testValidateServiceJourneyWithAreaStopMissingTimeWindow() {
+ // remove arrival time and departure time and add flex window
+ timetabledPassingTimes.get(0).withDepartureTime(null).withArrivalTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME
+ );
+ }
+
+ @Test
+ void testValidateServiceJourneyWithAreaStopInconsistentTimeWindow() {
+ // remove arrival time and departure time and add flex window
+ timetabledPassingTimes
+ .get(0)
+ .withDepartureTime(null)
+ .withArrivalTime(null)
+ .withEarliestDepartureTime(LocalTime.MIDNIGHT.plusMinutes(1))
+ .withLatestArrivalTime(LocalTime.MIDNIGHT);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_INCONSISTENT_TIME
+ );
+ }
+
+ @Test
+ void testValidateServiceJourneyWithRegularStopFollowedByRegularStopNonIncreasingTime() {
+ // remove arrival time and departure time and add flex window on second stop
+ timetabledPassingTimes
+ .get(1)
+ .withArrivalTime(departureTimes.get(0).minusMinutes(1));
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_NON_INCREASING_TIME
+ );
+ }
+
+ /**
+ * This test makes sure all passing times are complete and consistent, before it checks for
+ * increasing times.
+ */
+ @Test
+ void testValidateWithRegularStopFollowedByRegularStopWithMissingTime() {
+ // Set arrivalTime AFTER departure time (not valid)
+ timetabledPassingTimes.get(1).withArrivalTime(null).withDepartureTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME
+ );
+ }
+
+ @Test
+ void testValidateServiceJourneyWithRegularStopFollowedByStopArea() {
+ // remove arrival time and departure time and add flex window on second stop
+ timetabledPassingTimes
+ .get(1)
+ .withDepartureTime(null)
+ .withArrivalTime(null)
+ .withEarliestDepartureTime(departureTimes.get(1))
+ .withLatestArrivalTime(departureTimes.get(1).plusMinutes(1));
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(1).getRef(), "");
+
+ assertNoIssue(netexEntitiesIndex);
+ }
+
+ @Test
+ void testValidateServiceJourneyWithRegularStopFollowedByStopAreaNonIncreasingTime() {
+ // remove arrival time and departure time and add flex window with decreasing time on second stop
+ timetabledPassingTimes
+ .get(1)
+ .withEarliestDepartureTime(departureTimes.get(0).minusMinutes(1))
+ .withArrivalTime(null)
+ .withDepartureTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME
+ );
+ }
+
+ @Test
+ void testValidateServiceJourneyWithStopAreaFollowedByRegularStop() {
+ // remove arrival time and departure time and add flex window on first stop
+ timetabledPassingTimes
+ .get(0)
+ .withEarliestDepartureTime(departureTimes.get(0))
+ .withLatestArrivalTime(departureTimes.get(0))
+ .withArrivalTime(null)
+ .withDepartureTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ assertNoIssue(netexEntitiesIndex);
+ }
+
+ @Test
+ void testValidateServiceJourneyWithStopAreaFollowedByStopArea() {
+ timetabledPassingTimes
+ .get(0)
+ .withEarliestDepartureTime(departureTimes.get(0))
+ .withLatestArrivalTime(departureTimes.get(0))
+ .withArrivalTime(null)
+ .withDepartureTime(null);
+
+ timetabledPassingTimes
+ .get(1)
+ .withEarliestDepartureTime(departureTimes.get(1))
+ .withLatestArrivalTime(departureTimes.get(1).plusMinutes(1))
+ .withArrivalTime(null)
+ .withDepartureTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(1).getRef(), "");
+
+ assertNoIssue(netexEntitiesIndex);
+ }
+
+ @Test
+ void testValidateServiceJourneyWithStopAreaFollowedByStopAreaNonIncreasingTime() {
+ // remove arrival time and departure time and add flex window on first stop and second stop
+ // and add decreasing time on second stop
+ timetabledPassingTimes
+ .get(0)
+ .withEarliestDepartureTime(departureTimes.get(0))
+ .withLatestArrivalTime(departureTimes.get(0))
+ .withArrivalTime(null)
+ .withDepartureTime(null);
+
+ timetabledPassingTimes
+ .get(1)
+ .withEarliestDepartureTime(departureTimes.get(1).minusMinutes(1))
+ .withLatestArrivalTime(departureTimes.get(1).plusMinutes(1))
+ .withArrivalTime(null)
+ .withDepartureTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME
+ );
+ }
+
+ @Test
+ void testValidateServiceJourneyWithStopAreaFollowedByRegularStopNonIncreasingTime() {
+ // remove arrival time and departure time and add flex window on first stop
+ // and add decreasing time on second stop
+ timetabledPassingTimes
+ .get(0)
+ .withEarliestDepartureTime(departureTimes.get(0))
+ .withLatestArrivalTime(departureTimes.get(0))
+ .withArrivalTime(null)
+ .withDepartureTime(null);
+
+ timetabledPassingTimes
+ .get(1)
+ .withArrivalTime(departureTimes.get(0).minusMinutes(1))
+ .withDepartureTime(null);
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getFlexibleStopPlaceIdByStopPointRefIndex()
+ .put(scheduledStopPointRefs.get(0).getRef(), "");
+
+ assertIssue(
+ netexEntitiesIndex,
+ NonIncreasingPassingTimeValidator.RULE_NON_INCREASING_TIME
+ );
+ }
+
+ private void assertIssue(
+ NetexEntitiesIndex netexEntitiesIndex,
+ ValidationRule rule
+ ) {
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesIndex
+ );
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertEquals(1, validationIssues.size());
+ Assertions.assertEquals(rule, validationIssues.get(0).rule());
+ }
+
+ private void assertNoIssue(NetexEntitiesIndex netexEntitiesIndex) {
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesIndex
+ );
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ private static JAXBValidationContext createValidationContext(
+ NetexEntitiesIndex netexEntitiesIndex
+ ) {
+ return new JAXBValidationContext(
+ VALIDATION_REPORT_ID,
+ netexEntitiesIndex,
+ TestCommonDataRepository.of(NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN),
+ v ->
+ TestStopPlaceRepository.ofLocalBusStops(
+ NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN
+ ),
+ TEST_CODESPACE,
+ TEST_LINE_XML_FILE,
+ Map.of()
+ );
+ }
+}
diff --git a/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidatorTest.java b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidatorTest.java
new file mode 100644
index 00000000..52ed7dce
--- /dev/null
+++ b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidatorTest.java
@@ -0,0 +1,485 @@
+package org.entur.netex.validation.validator.jaxb.rules.servicejourney.transportmode;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import org.entur.netex.index.api.NetexEntitiesIndex;
+import org.entur.netex.validation.test.jaxb.support.NetexEntitiesTestFactory;
+import org.entur.netex.validation.test.jaxb.support.TestCommonDataRepository;
+import org.entur.netex.validation.test.jaxb.support.TestStopPlaceRepository;
+import org.entur.netex.validation.validator.ValidationIssue;
+import org.entur.netex.validation.validator.jaxb.CommonDataRepository;
+import org.entur.netex.validation.validator.jaxb.JAXBValidationContext;
+import org.entur.netex.validation.validator.jaxb.StopPlaceRepository;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration;
+import org.rutebanken.netex.model.BusSubmodeEnumeration;
+import org.rutebanken.netex.model.CoachSubmodeEnumeration;
+import org.rutebanken.netex.model.FlexibleLineTypeEnumeration;
+import org.rutebanken.netex.model.Line_VersionStructure;
+import org.rutebanken.netex.model.RailSubmodeEnumeration;
+import org.rutebanken.netex.model.TaxiSubmodeEnumeration;
+import org.rutebanken.netex.model.TransportSubmodeStructure;
+
+class MismatchedTransportModeSubModeValidatorTest {
+
+ private static final String TEST_REPORT_ID = "report id";
+ private static final String TEST_CODESPACE = "ENT";
+ private static final String TEST_FILENAME = "netex.xml";
+ private MismatchedTransportModeSubModeValidator validator;
+ private NetexEntitiesTestFactory netexEntitiesTestFactory;
+ private NetexEntitiesTestFactory.CreateGenericLine extends Line_VersionStructure> line;
+ private NetexEntitiesTestFactory.CreateServiceJourney serviceJourney;
+
+ @BeforeEach
+ void setUp() {
+ validator = new MismatchedTransportModeSubModeValidator();
+ netexEntitiesTestFactory = new NetexEntitiesTestFactory();
+ line = netexEntitiesTestFactory.createLine(1);
+ NetexEntitiesTestFactory.CreateRoute route =
+ netexEntitiesTestFactory.createRoute(1);
+ NetexEntitiesTestFactory.CreateJourneyPattern journeyPattern =
+ netexEntitiesTestFactory.createJourneyPattern(1).withRoute(route);
+ journeyPattern.createStopPointsInJourneyPattern(4);
+ serviceJourney =
+ netexEntitiesTestFactory.createServiceJourney(1, journeyPattern);
+ }
+
+ @Test
+ void transportModeOnLineMatchesWithStopPlace() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void stopAssignmentsWithValidModeDefinedInLineFileShouldBeConsidered() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ );
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getQuayIdByStopPointRefIndex()
+ .put("TST:ScheduledStopPoint:1", "TST:Quay:1");
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesIndex,
+ TestCommonDataRepository.of(0),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void stopAssignmentsWithInvalidModeDefinedInLineFileShouldBeConsidered() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withRailSubmode(RailSubmodeEnumeration.LOCAL)
+ );
+
+ NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create();
+
+ netexEntitiesIndex
+ .getQuayIdByStopPointRefIndex()
+ .put("TST:ScheduledStopPoint:1", "TST:Quay:1");
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesIndex,
+ TestCommonDataRepository.of(0),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+ Assertions.assertFalse(validationIssues.isEmpty());
+ }
+
+ @Test
+ void transportModeOverriddenOnServiceJourneyMatchesWithStopPlace() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withRailSubmode(RailSubmodeEnumeration.LOCAL)
+ );
+ serviceJourney
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void railReplacementBusStopsCanBeVisitedByRailReplacementBusService() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofRailReplacementBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void railReplacementBusStopsCanOnlyBeVisitedByRailReplacementBusService() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofRailReplacementBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertFalse(validationIssues.isEmpty());
+ }
+
+ @Test
+ void transportModeBusOnServiceJourneyShouldMatchWithTransportModeCoachOnStopPlace() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofNationalCoachStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void transportModeCoachOnServiceJourneyShouldMatchWithTransportModeBusOnStopPlace() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.COACH)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withCoachSubmode(CoachSubmodeEnumeration.NATIONAL_COACH)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void taxiCanStopOnBusStops() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void taxiCanStopOnCoachStops() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofNationalCoachStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void taxiCannotStopOnStopOtherThanBusOrCoach() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalTrainStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertFalse(validationIssues.isEmpty());
+ }
+
+ @Test
+ void validateOkWhenTransportModeNotFoundOnServiceJourneyNorLine() {
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void validateOkWhenTransportSubModeNotFoundOnServiceJourneyNorLine() {
+ line.withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI);
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void validateOkWhenTransportModeAndSubModeNotFoundOnQuay() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI)
+ );
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofMissingTransportModeAndSubMode(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void transportModeMissMatchShouldGenerateValidationIssue() {
+ line
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ netexEntitiesTestFactory.create(),
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalTrainStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertFalse(validationIssues.isEmpty());
+ }
+
+ @Test
+ void correctTransportModeOnFlexibleLineShouldBeValidated() {
+ NetexEntitiesIndex flexNetexEntitiesIndex =
+ createFlexNetexEntitiesIndex(createFlexibleLine ->
+ createFlexibleLine
+ .withFlexibleLineType(FlexibleLineTypeEnumeration.FIXED)
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS)
+ )
+ );
+
+ JAXBValidationContext validationContext = createValidationContext(
+ flexNetexEntitiesIndex,
+ TestCommonDataRepository.of(4),
+ TestStopPlaceRepository.ofLocalBusStops(4)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertTrue(validationIssues.isEmpty());
+ }
+
+ @Test
+ void incorrectTransportModeOnFlexibleLineShouldBeReported() {
+ NetexEntitiesIndex flexNetexEntitiesIndex =
+ createFlexNetexEntitiesIndex(createFlexibleLine ->
+ createFlexibleLine
+ .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL)
+ .withTransportSubmode(
+ new TransportSubmodeStructure()
+ .withRailSubmode(RailSubmodeEnumeration.LOCAL)
+ )
+ );
+
+ // create common data and stop place repositories where only the first two stops are mapped to fixed quays
+ // (the two following quays can be mapped to flexible areas)
+ JAXBValidationContext validationContext = createValidationContext(
+ flexNetexEntitiesIndex,
+ TestCommonDataRepository.of(2),
+ TestStopPlaceRepository.ofLocalBusStops(2)
+ );
+
+ List validationIssues = validator.validate(
+ validationContext
+ );
+
+ Assertions.assertEquals(2, validationIssues.size());
+ Assertions.assertTrue(
+ validationIssues
+ .stream()
+ .allMatch(validationIssue ->
+ validationIssue
+ .rule()
+ .equals(
+ MismatchedTransportModeSubModeValidator.RULE_INVALID_TRANSPORT_MODE
+ )
+ )
+ );
+ }
+
+ /**
+ * Create a NetexEntitiesIndex containing a flexible line.
+ */
+ private NetexEntitiesIndex createFlexNetexEntitiesIndex(
+ Consumer configureFlexibleLine
+ ) {
+ NetexEntitiesTestFactory netexEntitiesTestFactory =
+ new NetexEntitiesTestFactory();
+
+ NetexEntitiesTestFactory.CreateFlexibleLine createFlexibleLine =
+ netexEntitiesTestFactory
+ .createFlexibleLine()
+ .withFlexibleLineType(FlexibleLineTypeEnumeration.MIXED_FLEXIBLE);
+
+ configureFlexibleLine.accept(createFlexibleLine);
+
+ NetexEntitiesTestFactory.CreateRoute route =
+ netexEntitiesTestFactory.createRoute();
+
+ NetexEntitiesTestFactory.CreateJourneyPattern journeyPattern =
+ netexEntitiesTestFactory.createJourneyPattern().withRoute(route);
+ journeyPattern.createStopPointsInJourneyPattern(4);
+
+ netexEntitiesTestFactory.createServiceJourney(journeyPattern);
+
+ return netexEntitiesTestFactory.create();
+ }
+
+ private static JAXBValidationContext createValidationContext(
+ NetexEntitiesIndex netexEntitiesIndex,
+ CommonDataRepository commonDataRepository,
+ StopPlaceRepository stopPlaceRepository
+ ) {
+ return new JAXBValidationContext(
+ TEST_REPORT_ID,
+ netexEntitiesIndex,
+ commonDataRepository,
+ n -> stopPlaceRepository,
+ TEST_CODESPACE,
+ TEST_FILENAME,
+ Map.of()
+ );
+ }
+}
diff --git a/src/test/java/org/entur/netex/validation/validator/jaxb/support/NetexUtilsTest.java b/src/test/java/org/entur/netex/validation/validator/jaxb/support/NetexUtilsTest.java
new file mode 100644
index 00000000..88c1c3bf
--- /dev/null
+++ b/src/test/java/org/entur/netex/validation/validator/jaxb/support/NetexUtilsTest.java
@@ -0,0 +1,85 @@
+package org.entur.netex.validation.validator.jaxb.support;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.entur.netex.validation.test.jaxb.support.JAXBUtils;
+import org.entur.netex.validation.validator.model.ScheduledStopPointId;
+import org.junit.jupiter.api.Test;
+import org.rutebanken.netex.model.JourneyPattern;
+import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure;
+import org.rutebanken.netex.model.PointsInJourneyPattern_RelStructure;
+import org.rutebanken.netex.model.ScheduledStopPointRefStructure;
+import org.rutebanken.netex.model.StopPointInJourneyPattern;
+
+class NetexUtilsTest {
+
+ public static final String TEST_SCHEDULED_STOP_POINT_ID =
+ "TST:ScheduledStopPoint:1";
+ public static final String TEST_STOP_POINT_IN_JOURNEY_PATTERN_ID =
+ "TST:StopPointInJourneyPattern:1";
+
+ @Test
+ void testEmptyJourneyPattern() {
+ JourneyPattern journeyPattern = new JourneyPattern();
+ Map stringScheduledStopPointIdMap =
+ NetexUtils.scheduledStopPointIdByStopPointId(journeyPattern);
+ assertNotNull(stringScheduledStopPointIdMap);
+ }
+
+ @Test
+ void testScheduledStopPointIdByStopPointId() {
+ JourneyPattern journeyPattern = journeyPattern();
+ Map scheduledStopPointIdByStopPointId =
+ NetexUtils.scheduledStopPointIdByStopPointId(journeyPattern);
+ assertNotNull(scheduledStopPointIdByStopPointId);
+ assertEquals(
+ Map.of(
+ TEST_STOP_POINT_IN_JOURNEY_PATTERN_ID,
+ new ScheduledStopPointId(TEST_SCHEDULED_STOP_POINT_ID)
+ ),
+ scheduledStopPointIdByStopPointId
+ );
+ }
+
+ @Test
+ void testSStopPointId() {
+ JourneyPattern journeyPattern = journeyPattern();
+ StopPointInJourneyPattern stopPointInJourneyPattern =
+ NetexUtils.stopPointInJourneyPattern(
+ TEST_STOP_POINT_IN_JOURNEY_PATTERN_ID,
+ journeyPattern
+ );
+ assertNotNull(stopPointInJourneyPattern);
+ assertEquals(
+ TEST_STOP_POINT_IN_JOURNEY_PATTERN_ID,
+ stopPointInJourneyPattern.getId()
+ );
+ }
+
+ private static JourneyPattern journeyPattern() {
+ JourneyPattern journeyPattern = new JourneyPattern();
+ PointsInJourneyPattern_RelStructure pointsInJourneyPattern =
+ new PointsInJourneyPattern_RelStructure();
+ PointInLinkSequence_VersionedChildStructure point1 =
+ new StopPointInJourneyPattern()
+ .withId(TEST_STOP_POINT_IN_JOURNEY_PATTERN_ID)
+ .withScheduledStopPointRef(
+ JAXBUtils.createJaxbElement(
+ new ScheduledStopPointRefStructure()
+ .withRef(TEST_SCHEDULED_STOP_POINT_ID)
+ )
+ );
+ Collection points = List.of(
+ point1
+ );
+ pointsInJourneyPattern.withPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern(
+ points
+ );
+
+ journeyPattern.withPointsInSequence(pointsInJourneyPattern);
+ return journeyPattern;
+ }
+}