diff --git a/src/main/java/no/entur/antu/config/NetexDataConfig.java b/src/main/java/no/entur/antu/config/NetexDataConfig.java index 48ccb3c9..51adf847 100644 --- a/src/main/java/no/entur/antu/config/NetexDataConfig.java +++ b/src/main/java/no/entur/antu/config/NetexDataConfig.java @@ -1,7 +1,10 @@ package no.entur.antu.config; +import static no.entur.antu.config.cache.CacheConfig.ACTIVE_DATES_CACHE; import static no.entur.antu.config.cache.CacheConfig.LINE_INFO_CACHE; +import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_DAY_TYPES_CACHE; import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_INTERCHANGE_INFO_CACHE; +import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_OPERATING_DAYS_CACHE; import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_STOPS_CACHE; import java.util.List; @@ -25,6 +28,15 @@ NetexDataRepository netexDataRepository( @Qualifier( SERVICE_JOURNEY_STOPS_CACHE ) Map>> serviceJourneyStopsCache, + @Qualifier( + SERVICE_JOURNEY_DAY_TYPES_CACHE + ) Map> serviceJourneyDayTypesCache, + @Qualifier( + ACTIVE_DATES_CACHE + ) Map> activeDatesCache, + @Qualifier( + SERVICE_JOURNEY_OPERATING_DAYS_CACHE + ) Map> serviceJourneyOperatingDaysCache, @Qualifier( SERVICE_JOURNEY_INTERCHANGE_INFO_CACHE ) Map> serviceJourneyInterchangeInfoCache @@ -33,6 +45,9 @@ NetexDataRepository netexDataRepository( redissonClient, lineInfoCache, serviceJourneyStopsCache, + serviceJourneyDayTypesCache, + activeDatesCache, + serviceJourneyOperatingDaysCache, serviceJourneyInterchangeInfoCache ); } diff --git a/src/main/java/no/entur/antu/config/TimetableDataValidatorConfig.java b/src/main/java/no/entur/antu/config/TimetableDataValidatorConfig.java index 926cb7e7..599044c2 100644 --- a/src/main/java/no/entur/antu/config/TimetableDataValidatorConfig.java +++ b/src/main/java/no/entur/antu/config/TimetableDataValidatorConfig.java @@ -18,15 +18,19 @@ import java.util.List; import java.util.Set; +import no.entur.antu.netexdata.collectors.DatedServiceJourneysCollector; import no.entur.antu.netexdata.collectors.LineInfoCollector; +import no.entur.antu.netexdata.collectors.ServiceJourneyDayTypesCollector; import no.entur.antu.netexdata.collectors.ServiceJourneyInterchangeInfoCollector; import no.entur.antu.netexdata.collectors.ServiceJourneyStopsCollector; +import no.entur.antu.netexdata.collectors.activedatecollector.ActiveDatesCollector; import no.entur.antu.organisation.OrganisationRepository; import no.entur.antu.validation.validator.id.NetexIdValidator; import no.entur.antu.validation.validator.interchange.distance.UnexpectedInterchangeDistanceValidator; import no.entur.antu.validation.validator.interchange.duplicate.DuplicateInterchangesValidator; import no.entur.antu.validation.validator.interchange.mandatoryfields.MandatoryFieldsValidator; import no.entur.antu.validation.validator.interchange.stoppoints.StopPointsInVehicleJourneyValidator; +import no.entur.antu.validation.validator.interchange.waittime.UnexpectedWaitTimeAndActiveDatesValidator; import no.entur.antu.validation.validator.journeypattern.stoppoint.distance.UnexpectedDistanceBetweenStopPointsValidator; import no.entur.antu.validation.validator.journeypattern.stoppoint.identicalstoppoints.IdenticalStopPointsValidator; import no.entur.antu.validation.validator.journeypattern.stoppoint.samequayref.SameQuayRefValidator; @@ -41,7 +45,10 @@ import no.entur.antu.validation.validator.servicelink.distance.UnexpectedDistanceInServiceLinkValidator; import no.entur.antu.validation.validator.servicelink.stoppoints.MismatchedStopPointsValidator; import no.entur.antu.validation.validator.xpath.EnturTimetableDataValidationTreeFactory; -import org.entur.netex.validation.validator.*; +import org.entur.netex.validation.validator.DatasetValidator; +import org.entur.netex.validation.validator.NetexValidatorsRunner; +import org.entur.netex.validation.validator.ValidationReportEntryFactory; +import org.entur.netex.validation.validator.XPathValidator; import org.entur.netex.validation.validator.id.NetexIdUniquenessValidator; import org.entur.netex.validation.validator.id.NetexReferenceValidator; import org.entur.netex.validation.validator.id.ReferenceToValidEntityTypeValidator; @@ -180,6 +187,19 @@ public DuplicateLineNameValidator duplicateLineNameValidator( ); } + @Bean + public UnexpectedWaitTimeAndActiveDatesValidator unexpectedWaitTimeValidator( + @Qualifier( + "validationReportEntryFactory" + ) ValidationReportEntryFactory validationReportEntryFactory, + NetexDataRepository netexDataRepository + ) { + return new UnexpectedWaitTimeAndActiveDatesValidator( + validationReportEntryFactory, + netexDataRepository + ); + } + @Bean public NetexValidatorsRunner timetableDataValidatorsRunner( @Qualifier( @@ -214,10 +234,14 @@ public NetexValidatorsRunner timetableDataValidatorsRunner( StopPointsInVehicleJourneyValidator stopPointsInVehicleJourneyValidator, DuplicateLineNameValidator duplicateLineNameValidator, MissingReplacementValidator missingReplacementValidator, + UnexpectedWaitTimeAndActiveDatesValidator unexpectedWaitTimeAndActiveDatesValidator, LineInfoCollector lineInfoCollector, ServiceJourneyStopsCollector serviceJourneyStopsCollector, ServiceJourneyInterchangeInfoCollector serviceJourneyInterchangeInfoCollector, - CommonDataRepositoryLoader commonDataRepository, + ActiveDatesCollector activeDatesCollector, + ServiceJourneyDayTypesCollector serviceJourneyDayTypesCollector, + DatedServiceJourneysCollector datedServiceJourneysCollector, + CommonDataRepositoryLoader commonDataRepositoryLoader, NetexDataRepository netexDataRepository, StopPlaceRepository stopPlaceRepository ) { @@ -254,13 +278,17 @@ public NetexValidatorsRunner timetableDataValidatorsRunner( List netexTimetableDatasetValidators = List.of( duplicateLineNameValidator, - stopPointsInVehicleJourneyValidator + stopPointsInVehicleJourneyValidator, + unexpectedWaitTimeAndActiveDatesValidator ); List commonDataCollectors = List.of( lineInfoCollector, serviceJourneyInterchangeInfoCollector, - serviceJourneyStopsCollector + serviceJourneyStopsCollector, + activeDatesCollector, + serviceJourneyDayTypesCollector, + datedServiceJourneysCollector ); return NetexValidatorsRunner @@ -271,7 +299,7 @@ public NetexValidatorsRunner timetableDataValidatorsRunner( .withJaxbValidators(jaxbValidators) .withDatasetValidators(netexTimetableDatasetValidators) .withNetexDataCollectors(commonDataCollectors) - .withCommonDataRepository(commonDataRepository) + .withCommonDataRepository(commonDataRepositoryLoader) .withNetexDataRepository(netexDataRepository) .withStopPlaceRepository(stopPlaceRepository) .withValidationReportEntryFactory(validationReportEntryFactory) diff --git a/src/main/java/no/entur/antu/config/cache/CacheConfig.java b/src/main/java/no/entur/antu/config/cache/CacheConfig.java index 7f88d9ef..0eab98a4 100644 --- a/src/main/java/no/entur/antu/config/cache/CacheConfig.java +++ b/src/main/java/no/entur/antu/config/cache/CacheConfig.java @@ -40,8 +40,13 @@ public class CacheConfig { public static final String LINE_INFO_CACHE = "linesInfoCache"; public static final String SERVICE_JOURNEY_INTERCHANGE_INFO_CACHE = "serviceJourneyInterchangeInfoCache"; + public static final String SERVICE_JOURNEY_DAY_TYPES_CACHE = + "serviceJourneyDayTypesCache"; public static final String SERVICE_JOURNEY_STOPS_CACHE = "serviceJourneyStopsCache"; + public static final String SERVICE_JOURNEY_OPERATING_DAYS_CACHE = + "serviceJourneyOperatingDaysCache"; + public static final String ACTIVE_DATES_CACHE = "activeDatesCache"; public static final String QUAY_ID_NOT_FOUND_CACHE = "quayIdNotFoundCache"; private static final Kryo5Codec DEFAULT_CODEC = new Kryo5Codec(); @@ -172,6 +177,17 @@ public Map> serviceJourneyInterchangeInfoCache( ); } + @Bean(name = SERVICE_JOURNEY_DAY_TYPES_CACHE) + public Map> serviceJourneyDayTypesCache( + RedissonClient redissonClient + ) { + return getOrCreateReportScopedCache( + redissonClient, + SERVICE_JOURNEY_DAY_TYPES_CACHE, + new CompositeCodec(new StringCodec(), new StringCodec()) + ); + } + @Bean(name = SERVICE_JOURNEY_STOPS_CACHE) public Map>> serviceJourneyStopsCache( RedissonClient redissonClient @@ -183,6 +199,28 @@ public Map>> serviceJourneyStopsCache( ); } + @Bean(name = ACTIVE_DATES_CACHE) + public Map> activeDatesCache( + RedissonClient redissonClient + ) { + return getOrCreateReportScopedCache( + redissonClient, + ACTIVE_DATES_CACHE, + new CompositeCodec(new StringCodec(), new StringCodec()) + ); + } + + @Bean(name = SERVICE_JOURNEY_OPERATING_DAYS_CACHE) + public Map> serviceJourneyOperatingDaysCache( + RedissonClient redissonClient + ) { + return getOrCreateReportScopedCache( + redissonClient, + SERVICE_JOURNEY_OPERATING_DAYS_CACHE, + new CompositeCodec(new StringCodec(), new StringCodec()) + ); + } + @Bean public NetexIdRepository netexIdRepository( RedissonClient redissonClient, diff --git a/src/main/java/no/entur/antu/config/cache/NetexDataCollectorConfig.java b/src/main/java/no/entur/antu/config/cache/NetexDataCollectorConfig.java index 5e8e9316..c8787e86 100644 --- a/src/main/java/no/entur/antu/config/cache/NetexDataCollectorConfig.java +++ b/src/main/java/no/entur/antu/config/cache/NetexDataCollectorConfig.java @@ -1,14 +1,20 @@ package no.entur.antu.config.cache; +import static no.entur.antu.config.cache.CacheConfig.ACTIVE_DATES_CACHE; import static no.entur.antu.config.cache.CacheConfig.LINE_INFO_CACHE; +import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_DAY_TYPES_CACHE; import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_INTERCHANGE_INFO_CACHE; +import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_OPERATING_DAYS_CACHE; import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_STOPS_CACHE; import java.util.List; import java.util.Map; +import no.entur.antu.netexdata.collectors.DatedServiceJourneysCollector; import no.entur.antu.netexdata.collectors.LineInfoCollector; +import no.entur.antu.netexdata.collectors.ServiceJourneyDayTypesCollector; import no.entur.antu.netexdata.collectors.ServiceJourneyInterchangeInfoCollector; import no.entur.antu.netexdata.collectors.ServiceJourneyStopsCollector; +import no.entur.antu.netexdata.collectors.activedatecollector.ActiveDatesCollector; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @@ -25,6 +31,29 @@ public LineInfoCollector lineInfoScraper( return new LineInfoCollector(redissonClient, lineInfoCache); } + @Bean + public ActiveDatesCollector activeDatesCollector( + RedissonClient redissonClient, + @Qualifier( + ACTIVE_DATES_CACHE + ) Map> activeDatesCache + ) { + return new ActiveDatesCollector(redissonClient, activeDatesCache); + } + + @Bean + public DatedServiceJourneysCollector datedServiceJourneysCollector( + RedissonClient redissonClient, + @Qualifier( + SERVICE_JOURNEY_OPERATING_DAYS_CACHE + ) Map> serviceJourneyOperatingDaysCache + ) { + return new DatedServiceJourneysCollector( + redissonClient, + serviceJourneyOperatingDaysCache + ); + } + @Bean public ServiceJourneyInterchangeInfoCollector serviceJourneyInterchangeInfoCollector( RedissonClient redissonClient, @@ -38,6 +67,19 @@ public ServiceJourneyInterchangeInfoCollector serviceJourneyInterchangeInfoColle ); } + @Bean + public ServiceJourneyDayTypesCollector serviceJourneyDayTypesCollector( + RedissonClient redissonClient, + @Qualifier( + SERVICE_JOURNEY_DAY_TYPES_CACHE + ) Map> serviceJourneyDayTypesCache + ) { + return new ServiceJourneyDayTypesCollector( + redissonClient, + serviceJourneyDayTypesCache + ); + } + @Bean public ServiceJourneyStopsCollector serviceJourneyStopsCollector( RedissonClient redissonClient, diff --git a/src/main/java/no/entur/antu/netexdata/DefaultNetexDataRepository.java b/src/main/java/no/entur/antu/netexdata/DefaultNetexDataRepository.java index e7125cb7..1e260ac1 100644 --- a/src/main/java/no/entur/antu/netexdata/DefaultNetexDataRepository.java +++ b/src/main/java/no/entur/antu/netexdata/DefaultNetexDataRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import no.entur.antu.exception.AntuException; import org.entur.netex.validation.validator.model.ActiveDates; import org.entur.netex.validation.validator.model.ActiveDatesId; @@ -23,15 +24,24 @@ public class DefaultNetexDataRepository implements NetexDataRepositoryLoader { private final Map> lineInfoCache; private final Map>> serviceJourneyStopsCache; + private final Map> serviceJourneyDayTypesCache; + private final Map> activeDatesCache; + private final Map> serviceJourneyOperatingDaysCache; private final Map> serviceJourneyInterchangeInfoCache; public DefaultNetexDataRepository( Map> lineInfoCache, Map>> serviceJourneyStopsCache, + Map> serviceJourneyDayTypesCache, + Map> activeDatesCache, + Map> serviceJourneyOperatingDaysCache, Map> serviceJourneyInterchangeInfoCache ) { this.lineInfoCache = lineInfoCache; this.serviceJourneyStopsCache = serviceJourneyStopsCache; + this.serviceJourneyDayTypesCache = serviceJourneyDayTypesCache; + this.activeDatesCache = activeDatesCache; + this.serviceJourneyOperatingDaysCache = serviceJourneyOperatingDaysCache; this.serviceJourneyInterchangeInfoCache = serviceJourneyInterchangeInfoCache; } @@ -67,40 +77,79 @@ public Map> serviceJourneyStops( ); } - @Override - public List serviceJourneyInterchangeInfos( + public Map> serviceJourneyDayTypes( String validationReportId ) { - return Optional - .ofNullable(serviceJourneyInterchangeInfoCache) - .map(Map::entrySet) + return serviceJourneyDayTypesCache + .keySet() .stream() - .flatMap(Set::stream) - .filter(entry -> entry.getKey().startsWith(validationReportId)) - .flatMap(entry -> entry.getValue().stream()) - .map(ServiceJourneyInterchangeInfo::fromString) - .toList(); + .filter(k -> k.startsWith(validationReportId)) + .map(serviceJourneyDayTypesCache::get) + .flatMap(m -> m.entrySet().stream()) + .collect( + Collectors.toMap( + entry -> ServiceJourneyId.ofValidId(entry.getKey()), + entry -> + Stream.of(entry.getValue().split(",")).map(DayTypeId::new).toList(), + (p, n) -> n + ) + ); } @Override - public Map> serviceJourneyDayTypes( + public Map> serviceJourneyOperatingDays( String validationReportId ) { - throw new UnsupportedOperationException(); + return serviceJourneyOperatingDaysCache + .keySet() + .stream() + .filter(k -> k.startsWith(validationReportId)) + .map(serviceJourneyOperatingDaysCache::get) + .flatMap(m -> m.entrySet().stream()) + .collect( + Collectors.toMap( + entry -> ServiceJourneyId.ofValidId(entry.getKey()), + entry -> + Stream + .of(entry.getValue().split(",")) + .map(OperatingDayId::new) + .toList(), + (p, n) -> n + ) + ); } - @Override public Map activeDates( String validationReportId ) { - throw new UnsupportedOperationException(); + return activeDatesCache + .keySet() + .stream() + .filter(k -> k.startsWith(validationReportId)) + .map(activeDatesCache::get) + .flatMap(m -> m.entrySet().stream()) + .collect( + Collectors.toMap( + entry -> ActiveDatesId.of(entry.getKey()), + entry -> ActiveDates.fromString(entry.getValue()), + (p, n) -> n + ) + ); } @Override - public Map> serviceJourneyOperatingDays( + public List serviceJourneyInterchangeInfos( String validationReportId ) { - throw new UnsupportedOperationException(); + return Optional + .ofNullable(serviceJourneyInterchangeInfoCache) + .map(Map::entrySet) + .stream() + .flatMap(Set::stream) + .filter(entry -> entry.getKey().startsWith(validationReportId)) + .flatMap(entry -> entry.getValue().stream()) + .map(ServiceJourneyInterchangeInfo::fromString) + .toList(); } @Override diff --git a/src/main/java/no/entur/antu/netexdata/RedisNetexDataRepository.java b/src/main/java/no/entur/antu/netexdata/RedisNetexDataRepository.java index 1ec4efc6..89fabdc0 100644 --- a/src/main/java/no/entur/antu/netexdata/RedisNetexDataRepository.java +++ b/src/main/java/no/entur/antu/netexdata/RedisNetexDataRepository.java @@ -12,11 +12,17 @@ public RedisNetexDataRepository( RedissonClient redissonClient, Map> lineInfoCache, Map>> serviceJourneyStopsCache, + Map> serviceJourneyDayTypesCache, + Map> activeDatesCache, + Map> serviceJourneyOperatingDaysCache, Map> serviceJourneyInterchangeInfoCache ) { super( lineInfoCache, serviceJourneyStopsCache, + serviceJourneyDayTypesCache, + activeDatesCache, + serviceJourneyOperatingDaysCache, serviceJourneyInterchangeInfoCache ); this.redissonClient = redissonClient; diff --git a/src/main/java/no/entur/antu/netexdata/collectors/DatedServiceJourneysCollector.java b/src/main/java/no/entur/antu/netexdata/collectors/DatedServiceJourneysCollector.java new file mode 100644 index 00000000..22440111 --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/DatedServiceJourneysCollector.java @@ -0,0 +1,139 @@ +package no.entur.antu.netexdata.collectors; + +import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_OPERATING_DAYS_CACHE; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import jakarta.xml.bind.JAXBElement; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.entur.netex.validation.validator.jaxb.NetexDataCollector; +import org.redisson.api.RLock; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; +import org.rutebanken.netex.model.DatedServiceJourney; +import org.rutebanken.netex.model.ServiceJourneyRefStructure; + +public class DatedServiceJourneysCollector extends NetexDataCollector { + + private final RedissonClient redissonClient; + private final Map> serviceJourneyOperatingDaysCache; + + public DatedServiceJourneysCollector( + RedissonClient redissonClient, + Map> serviceJourneyOperatingDaysCache + ) { + this.redissonClient = redissonClient; + this.serviceJourneyOperatingDaysCache = serviceJourneyOperatingDaysCache; + } + + @Override + protected void collectDataFromLineFile( + JAXBValidationContext validationContext + ) { + Multimap serviceJourneyOperatingDays = validationContext + .datedServiceJourneys() + .stream() + .map(DatedServiceJourneysCollector::operatingDaysRefsPerServiceJourney) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toMultimap(Map.Entry::getKey, Map.Entry::getValue)); + + addServiceJourneyOperatingDays( + validationContext.getValidationReportId(), + validationContext.getFileName(), + serviceJourneyOperatingDays + .asMap() + .entrySet() + .stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> String.join(",", entry.getValue()) + ) + ) + ); + } + + @Override + protected void collectDataFromCommonFile( + JAXBValidationContext validationContext + ) { + // No service journeys and journey patterns in common files + } + + /** + * List of operating days references per service journey. + * There is only one serviceJourneyRef per datedServiceJourney. + */ + private static Optional> operatingDaysRefsPerServiceJourney( + DatedServiceJourney datedServiceJourney + ) { + return datedServiceJourney + .getJourneyRef() + .stream() + .map(JAXBElement::getValue) + .filter(ServiceJourneyRefStructure.class::isInstance) + .map(ServiceJourneyRefStructure.class::cast) + .filter(serviceJourneyRef -> serviceJourneyRef.getRef() != null) + .map(serviceJourneyRef -> + Map.entry( + serviceJourneyRef.getRef(), + datedServiceJourney.getOperatingDayRef().getRef() + ) + ) + .findFirst(); + } + + private void addServiceJourneyOperatingDays( + String validationReportId, + String filename, + Map serviceJourneyOperatingDays + ) { + RLock lock = redissonClient.getLock(validationReportId); + try { + lock.lock(); + + String keyName = + validationReportId + + "_" + + SERVICE_JOURNEY_OPERATING_DAYS_CACHE + + "_" + + filename; + + RMap serviceJourneyOperatingDaysForFile = + redissonClient.getMap(keyName); + serviceJourneyOperatingDaysForFile.putAll(serviceJourneyOperatingDays); + serviceJourneyOperatingDaysCache.put( + keyName, + serviceJourneyOperatingDaysForFile + ); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + static Collector> toMultimap( + Function keyMapper, + Function valueMapper + ) { + return Collector.of( + ArrayListMultimap::create, // Supplier: Create a new Multimap + (multimap, assignment) -> + multimap.put( + keyMapper.apply(assignment), + valueMapper.apply(assignment) + ), // Accumulator + (m1, m2) -> { // Combiner + m1.putAll(m2); + return m1; + } + ); + } +} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/LineInfoCollector.java b/src/main/java/no/entur/antu/netexdata/collectors/LineInfoCollector.java index 1719feb9..53507ddc 100644 --- a/src/main/java/no/entur/antu/netexdata/collectors/LineInfoCollector.java +++ b/src/main/java/no/entur/antu/netexdata/collectors/LineInfoCollector.java @@ -35,7 +35,7 @@ protected void collectDataFromLineFile( @Override protected void collectDataFromCommonFile( - JAXBValidationContext validationContext + JAXBValidationContext jaxbValidationContext ) { // No Lines in common files } diff --git a/src/main/java/no/entur/antu/netexdata/collectors/ServiceJourneyDayTypesCollector.java b/src/main/java/no/entur/antu/netexdata/collectors/ServiceJourneyDayTypesCollector.java new file mode 100644 index 00000000..416f27fa --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/ServiceJourneyDayTypesCollector.java @@ -0,0 +1,97 @@ +package no.entur.antu.netexdata.collectors; + +import static no.entur.antu.config.cache.CacheConfig.SERVICE_JOURNEY_DAY_TYPES_CACHE; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import no.entur.antu.validation.validator.support.NetexUtils; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.entur.netex.validation.validator.jaxb.NetexDataCollector; +import org.entur.netex.validation.validator.model.DayTypeId; +import org.redisson.api.RLock; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; + +public class ServiceJourneyDayTypesCollector extends NetexDataCollector { + + private final RedissonClient redissonClient; + private final Map> serviceJourneyDayTypesCache; + + public ServiceJourneyDayTypesCollector( + RedissonClient redissonClient, + Map> serviceJourneyDayTypesCache + ) { + this.redissonClient = redissonClient; + this.serviceJourneyDayTypesCache = serviceJourneyDayTypesCache; + } + + @Override + protected void collectDataFromLineFile( + JAXBValidationContext validationContext + ) { + Map serviceJourneyDayTypes = NetexUtils + .validServiceJourneys(validationContext) + .stream() + .map(serviceJourney -> + Map.entry( + serviceJourney.getId(), + DayTypeId + .of(serviceJourney) + .stream() + .map(DayTypeId::toString) + .toList() + ) + ) + .filter(entry -> !entry.getValue().isEmpty()) + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> String.join(",", entry.getValue()) + ) + ); + + if (!serviceJourneyDayTypes.isEmpty()) { + addServiceJourneyDayTypes( + validationContext.getValidationReportId(), + validationContext.getFileName(), + serviceJourneyDayTypes + ); + } + } + + @Override + protected void collectDataFromCommonFile( + JAXBValidationContext validationContext + ) { + // No service journeys and journey patterns in common files + } + + private void addServiceJourneyDayTypes( + String validationReportId, + String filename, + Map serviceJourneyDayTypes + ) { + RLock lock = redissonClient.getLock(validationReportId); + try { + lock.lock(); + + String keyName = + validationReportId + + "_" + + SERVICE_JOURNEY_DAY_TYPES_CACHE + + "_" + + filename; + + RMap serviceJourneyDayTypesMap = redissonClient.getMap( + keyName + ); + serviceJourneyDayTypesMap.putAll(serviceJourneyDayTypes); + serviceJourneyDayTypesCache.put(keyName, serviceJourneyDayTypesMap); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } +} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/ServiceJourneyStopsCollector.java b/src/main/java/no/entur/antu/netexdata/collectors/ServiceJourneyStopsCollector.java index caa29eb3..03a9222d 100644 --- a/src/main/java/no/entur/antu/netexdata/collectors/ServiceJourneyStopsCollector.java +++ b/src/main/java/no/entur/antu/netexdata/collectors/ServiceJourneyStopsCollector.java @@ -13,9 +13,7 @@ import org.redisson.api.RLock; import org.redisson.api.RMap; import org.redisson.api.RedissonClient; -import org.springframework.stereotype.Component; -@Component public class ServiceJourneyStopsCollector extends NetexDataCollector { private final RedissonClient redissonClient; @@ -38,8 +36,8 @@ protected void collectDataFromLineFile( // service journeys in them? // if (validationContext.serviceJourneyInterchanges().findAny().isPresent()) {} - Map> serviceJourneyStops = validationContext - .serviceJourneys() + Map> serviceJourneyStops = NetexUtils + .validServiceJourneys(validationContext) .stream() .map(serviceJourney -> { Map scheduledStopPointIdMap = diff --git a/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/ActiveDatesCollector.java b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/ActiveDatesCollector.java new file mode 100644 index 00000000..f8bae013 --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/ActiveDatesCollector.java @@ -0,0 +1,178 @@ +package no.entur.antu.netexdata.collectors.activedatecollector; + +import static no.entur.antu.config.cache.CacheConfig.ACTIVE_DATES_CACHE; +import static no.entur.antu.netexdata.collectors.activedatecollector.calender.CalendarUtilities.getValidityForFrameOrDefault; + +import jakarta.xml.bind.JAXBElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import no.entur.antu.netexdata.collectors.activedatecollector.calender.ActiveDatesBuilder; +import no.entur.antu.netexdata.collectors.activedatecollector.calender.ServiceCalendarFrameObject; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.entur.netex.validation.validator.jaxb.NetexDataCollector; +import org.redisson.api.RLock; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; +import org.rutebanken.netex.model.CompositeFrame; +import org.rutebanken.netex.model.ServiceCalendarFrame; +import org.rutebanken.netex.model.ValidBetween; + +public class ActiveDatesCollector extends NetexDataCollector { + + private final RedissonClient redissonClient; + private final Map> activeDatesCache; + + public ActiveDatesCollector( + RedissonClient redissonClient, + Map> activeDatesCache + ) { + this.redissonClient = redissonClient; + this.activeDatesCache = activeDatesCache; + } + + @Override + protected void collectDataFromLineFile( + JAXBValidationContext validationContext + ) { + collectData(validationContext); + } + + @Override + protected void collectDataFromCommonFile( + JAXBValidationContext validationContext + ) { + collectData(validationContext); + } + + private void collectData(JAXBValidationContext validationContext) { + ActiveDatesBuilder activeDatesBuilder = new ActiveDatesBuilder(); + + // TODO: Is it possible that the file has ServiceCalendarFrames has ServiceCalendarFrames outside the compositeFrame, + // while compositeFrame also exists? + List serviceCalendarFrameObjects = + new ArrayList<>(); + if (validationContext.hasCompositeFrames()) { + serviceCalendarFrameObjects = + validationContext + .compositeFrames() + .stream() + .map(ActiveDatesCollector::getServiceCalendarFrameObjects) + .flatMap(List::stream) + .toList(); + } else if (validationContext.hasServiceCalendarFrames()) { + serviceCalendarFrameObjects = + validationContext + .serviceCalendarFrames() + .stream() + .map(ActiveDatesCollector::getServiceCalendarFrameObjects) + .toList(); + } + + Map activeDatesPerDayTypes = serviceCalendarFrameObjects + .stream() + .map(activeDatesBuilder::buildPerDayType) + .flatMap(map -> map.entrySet().stream()) + .filter(entry -> entry.getValue().isValid()) + .collect( + Collectors.toMap( + entry -> entry.getKey().toString(), + entry -> entry.getValue().toString(), + (v1, v2) -> v1 + ) + ); + + Map activeDatesPerOperationDays = + serviceCalendarFrameObjects + .stream() + .map(activeDatesBuilder::buildPerOperatingDay) + .flatMap(map -> map.entrySet().stream()) + .filter(entry -> entry.getValue().isValid()) + .collect( + Collectors.toMap( + entry -> entry.getKey().toString(), + entry -> entry.getValue().toString(), + (v1, v2) -> v1 + ) + ); + + Map activeDates = Stream + .concat( + activeDatesPerDayTypes.entrySet().stream(), + activeDatesPerOperationDays.entrySet().stream() + ) + .collect( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1) + ); + + if (!activeDates.isEmpty()) { + addActiveDates( + validationContext.getValidationReportId(), + validationContext.getFileName(), + activeDates + ); + } + } + + private void addActiveDates( + String validationReportId, + String filename, + Map activeDates + ) { + RLock lock = redissonClient.getLock(validationReportId); + try { + lock.lock(); + + String keyName = + validationReportId + "_" + ACTIVE_DATES_CACHE + "_" + filename; + + RMap activeDatesForFile = redissonClient.getMap(keyName); + activeDatesForFile.putAll(activeDates); + activeDatesCache.put(keyName, activeDatesForFile); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + private static List getServiceCalendarFrameObjects( + CompositeFrame compositeFrame + ) { + // When grouping Frames into a CompositeFrame, ValidityCondition must be the same for all its frames. + // That is, ValidityCondition is not set per frame, but is implicitly controlled from the CompositeFrame. + ValidBetween validityForCompositeFrame = getValidityForFrameOrDefault( + compositeFrame, + null + ); + return compositeFrame + .getFrames() + .getCommonFrame() + .stream() + .map(JAXBElement::getValue) + .filter(ServiceCalendarFrame.class::isInstance) + .map(ServiceCalendarFrame.class::cast) + .map(serviceCalendarFrame -> + ServiceCalendarFrameObject.ofNullable( + serviceCalendarFrame, + validityForCompositeFrame + ) + ) + .toList(); + } + + private static ServiceCalendarFrameObject getServiceCalendarFrameObjects( + ServiceCalendarFrame serviceCalendarFrame + ) { + ValidBetween validityForServiceCalendarFrame = getValidityForFrameOrDefault( + serviceCalendarFrame, + null + ); + return ServiceCalendarFrameObject.ofNullable( + serviceCalendarFrame, + validityForServiceCalendarFrame + ); + } +} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ActiveDatesBuilder.java b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ActiveDatesBuilder.java new file mode 100644 index 00000000..eaf487ac --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ActiveDatesBuilder.java @@ -0,0 +1,356 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static no.entur.antu.netexdata.collectors.activedatecollector.calender.CalendarUtilities.getOrDefault; +import static no.entur.antu.netexdata.collectors.activedatecollector.calender.CalendarUtilities.isWithinValidRange; + +import jakarta.xml.bind.JAXBElement; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import org.entur.netex.validation.validator.model.ActiveDates; +import org.entur.netex.validation.validator.model.DayTypeId; +import org.entur.netex.validation.validator.model.OperatingDayId; +import org.rutebanken.netex.model.DayOfWeekEnumeration; +import org.rutebanken.netex.model.DayType; +import org.rutebanken.netex.model.DayTypeAssignment; +import org.rutebanken.netex.model.OperatingDay; +import org.rutebanken.netex.model.OperatingDay_VersionStructure; +import org.rutebanken.netex.model.PropertyOfDay; +import org.rutebanken.netex.model.ValidBetween; +import org.rutebanken.netex.model.VersionOfObjectRefStructure; + +public class ActiveDatesBuilder { + + private final Map activeDatesForDayTypeRef = + new HashMap<>(); + private final List excludedDates = new ArrayList<>(); + private int intDayTypes = 0; + + public Map buildPerDayType( + ServiceCalendarFrameObject serviceCalendarFrameObject + ) { + // Creating the DayTypes for ServiceCalendarFrame + serviceCalendarFrameObject + .calendarData() + .dayTypes() + .forEach((dayTypeRef, dayType) -> { + activeDatesForDayTypeRef.put( + dayTypeRef, + new ActiveDates(new ArrayList<>()) + ); + addDayType(dayType); + }); + + if (serviceCalendarFrameObject.serviceCalendar() != null) { + // Creating the DayTypes for ServiceCalendar + serviceCalendarFrameObject + .serviceCalendar() + .calendarData() + .dayTypes() + .forEach((dayTypeRef, dayType) -> { + activeDatesForDayTypeRef.put( + dayTypeRef, + new ActiveDates(new ArrayList<>()) + ); + addDayType(dayType); + }); + } + + // Creating ActiveDates form DayTypeAssignments for Dates in ServiceCalendarFrame + activeDatesForDates( + serviceCalendarFrameObject.calendarData(), + serviceCalendarFrameObject.validBetween() + ); + + if (serviceCalendarFrameObject.serviceCalendar() != null) { + // Creating ActiveDates form DayTypeAssignments for Dates in ServiceCalendar + activeDatesForDates( + serviceCalendarFrameObject.serviceCalendar().calendarData(), + serviceCalendarFrameObject.serviceCalendar().validBetween() + ); + } + + // Creating ActiveDates form DayTypeAssignments for OperatingDays for ServiceCalendarFrame + activeDatesForOperatingDays( + serviceCalendarFrameObject.calendarData(), + serviceCalendarFrameObject.validBetween() + ); + + if (serviceCalendarFrameObject.serviceCalendar() != null) { + // Creating ActiveDates form DayTypeAssignments for OperatingDays for ServiceCalendar + activeDatesForOperatingDays( + serviceCalendarFrameObject.serviceCalendar().calendarData(), + serviceCalendarFrameObject.serviceCalendar().validBetween() + ); + } + + // Creating ActiveDates form DayTypeAssignments for OperatingPeriods for ServiceCalendarFrame + activeDatesForOperatingPeriods( + serviceCalendarFrameObject.calendarData(), + serviceCalendarFrameObject.validBetween() + ); + + if (serviceCalendarFrameObject.serviceCalendar() != null) { + // Creating ActiveDates form DayTypeAssignments for OperatingPeriods for ServiceCalendar + activeDatesForOperatingPeriods( + serviceCalendarFrameObject.serviceCalendar().calendarData(), + serviceCalendarFrameObject.serviceCalendar().validBetween() + ); + } + + return Map.copyOf(activeDatesForDayTypeRef); + } + + public Map buildPerOperatingDay( + ServiceCalendarFrameObject serviceCalendarFrameObject + ) { + return serviceCalendarFrameObject + .calendarData() + .operatingDays() + .entrySet() + .stream() + .filter(entry -> + isWithinValidRange( + entry.getValue().getCalendarDate(), + serviceCalendarFrameObject.validBetween() + ) + ) + .collect( + toMap( + Map.Entry::getKey, + entry -> + new ActiveDates( + List.of(entry.getValue().getCalendarDate().toLocalDate()) + ) + ) + ); + } + + private void addDayType(DayType dayType) { + if (dayType.getProperties() != null) { + for (PropertyOfDay propertyOfDay : dayType + .getProperties() + .getPropertyOfDay()) { + List daysOfWeeks = propertyOfDay.getDaysOfWeek(); + + for (DayOfWeekEnumeration dayOfWeek : daysOfWeeks) { + List dayTypeEnums = convertDayOfWeek(dayOfWeek); + + for (DayOfWeekEnumeration dayTypeEnum : dayTypeEnums) { + int mask = 1 << dayTypeEnum.ordinal(); + this.intDayTypes |= mask; + } + } + } + } + } + + private void activeDatesForDates( + CalendarData calendarData, + ValidBetween validBetween + ) { + // Dates + for (DayTypeId dayTypeId : calendarData.dayTypeAssignments().keySet()) { + Collection dayTypeAssignments = calendarData + .dayTypeAssignments() + .get(dayTypeId); + Map> includedAndExcludedDates = + findIncludedAndExcludedDates(dayTypeAssignments, validBetween); + + Optional + .ofNullable(includedAndExcludedDates.get(Boolean.FALSE)) + .filter(Predicate.not(List::isEmpty)) + .ifPresent(excludedDates::addAll); + + // It should be true here, otherwise error for missing reference should already be reported. + // If it's false, it means that the DayTypeAssignment is referring to a dayType that is not defined in the dayTypes. + if (activeDatesForDayTypeRef.containsKey(dayTypeId)) { + Optional + .ofNullable(includedAndExcludedDates.get(Boolean.TRUE)) + .map(activeDatesForDayTypeRef.get(dayTypeId).dates()::addAll); + } + } + } + + private void activeDatesForOperatingDays( + CalendarData calendarData, + ValidBetween validBetween + ) { + // Operating days + for (DayTypeId dayTypeId : calendarData.dayTypeAssignments().keySet()) { + Collection dayTypeAssignments = calendarData + .dayTypeAssignments() + .get(dayTypeId); + Map> includedAndExcludedDates = + findIncludedAndExcludedOperatingDays( + dayTypeAssignments, + validBetween, + calendarData.operatingDays() + ); + + Optional + .ofNullable(includedAndExcludedDates.get(Boolean.FALSE)) + .filter(Predicate.not(List::isEmpty)) + .ifPresent(excludedDates::addAll); + + // It should be true here, otherwise error for missing reference should already be reported. + // If it's false, it means that the DayTypeAssignment is referring to a dayType that is not defined in the dayTypes. + if (activeDatesForDayTypeRef.containsKey(dayTypeId)) { + Optional + .ofNullable(includedAndExcludedDates.get(Boolean.TRUE)) + .map(activeDatesForDayTypeRef.get(dayTypeId).dates()::addAll); + } + } + } + + private void activeDatesForOperatingPeriods( + CalendarData calendarData, + ValidBetween validBetween + ) { + for (DayTypeId dayTypeId : calendarData.dayTypeAssignments().keys()) { + Collection dayTypeAssignments = calendarData + .dayTypeAssignments() + .get(dayTypeId); + dayTypeAssignments.forEach(dayTypeAssignment -> { + Optional + .ofNullable(dayTypeAssignment.getOperatingPeriodRef()) + .map(JAXBElement::getValue) + .map(VersionOfObjectRefStructure::getRef) + .map(calendarData.operatingPeriods()::get) + .ifPresent(operatingPeriod -> { + ValidOperatingPeriod validOperatingPeriod = ValidOperatingPeriod.of( + operatingPeriod, + validBetween, + calendarData.operatingDays() + ); + activeDatesForDayTypeRef + .get(dayTypeId) + .dates() + .addAll(validOperatingPeriod.toDates(excludedDates, intDayTypes)); + }); + }); + } + } + + private static Map> findIncludedAndExcludedDates( + Collection dayTypeAssignments, + ValidBetween validBetween + ) { + return dayTypeAssignments + .stream() + .filter(dayTypeAssignment -> + Optional + .ofNullable(dayTypeAssignment.getDate()) + .filter(date -> isWithinValidRange(date, validBetween)) + .isPresent() + ) + .collect( + groupingBy( + dayTypeAssignment -> + getOrDefault(dayTypeAssignment.isIsAvailable(), Boolean.TRUE), + mapping( + dayTypeAssignment -> dayTypeAssignment.getDate().toLocalDate(), + toList() + ) + ) + ); + } + + private Map> findIncludedAndExcludedOperatingDays( + Collection dayTypeAssignments, + ValidBetween validBetween, + Map operatingDays + ) { + return dayTypeAssignments + .stream() + .filter(dayTypeAssignment -> + Optional + .ofNullable(OperatingDayId.of(dayTypeAssignment)) + .map(operatingDays::get) + .map(OperatingDay::getCalendarDate) + .filter(dateOfOperation -> + isWithinValidRange(dateOfOperation, validBetween) + ) + .isPresent() + ) + .collect( + groupingBy( + dta -> getOrDefault(dta.isIsAvailable(), Boolean.TRUE), + mapping( + dta -> + Optional + .ofNullable(OperatingDayId.of(dta)) + .map(operatingDays::get) + .map(OperatingDay_VersionStructure::getCalendarDate) + .map(LocalDateTime::toLocalDate) + .orElse(null), + toList() + ) + ) + ); + } + + private static List convertDayOfWeek( + DayOfWeekEnumeration dayOfWeek + ) { + List days = new ArrayList<>(); + + switch (dayOfWeek) { + case MONDAY: + days.add(DayOfWeekEnumeration.MONDAY); + break; + case TUESDAY: + days.add(DayOfWeekEnumeration.TUESDAY); + break; + case WEDNESDAY: + days.add(DayOfWeekEnumeration.WEDNESDAY); + break; + case THURSDAY: + days.add(DayOfWeekEnumeration.THURSDAY); + break; + case FRIDAY: + days.add(DayOfWeekEnumeration.FRIDAY); + break; + case SATURDAY: + days.add(DayOfWeekEnumeration.SATURDAY); + break; + case SUNDAY: + days.add(DayOfWeekEnumeration.SUNDAY); + break; + case EVERYDAY: + days.add(DayOfWeekEnumeration.MONDAY); + days.add(DayOfWeekEnumeration.TUESDAY); + days.add(DayOfWeekEnumeration.WEDNESDAY); + days.add(DayOfWeekEnumeration.THURSDAY); + days.add(DayOfWeekEnumeration.FRIDAY); + days.add(DayOfWeekEnumeration.SATURDAY); + days.add(DayOfWeekEnumeration.SUNDAY); + break; + case WEEKDAYS: + days.add(DayOfWeekEnumeration.MONDAY); + days.add(DayOfWeekEnumeration.TUESDAY); + days.add(DayOfWeekEnumeration.WEDNESDAY); + days.add(DayOfWeekEnumeration.THURSDAY); + days.add(DayOfWeekEnumeration.FRIDAY); + break; + case WEEKEND: + days.add(DayOfWeekEnumeration.SATURDAY); + days.add(DayOfWeekEnumeration.SUNDAY); + break; + case NONE: + // None + break; + } + return days; + } +} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/CalendarData.java b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/CalendarData.java new file mode 100644 index 00000000..0f03c81b --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/CalendarData.java @@ -0,0 +1,17 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import com.google.common.collect.Multimap; +import java.util.Map; +import org.entur.netex.validation.validator.model.DayTypeId; +import org.entur.netex.validation.validator.model.OperatingDayId; +import org.rutebanken.netex.model.DayType; +import org.rutebanken.netex.model.DayTypeAssignment; +import org.rutebanken.netex.model.OperatingDay; +import org.rutebanken.netex.model.OperatingPeriod; + +public record CalendarData( + Map dayTypes, + Map operatingPeriods, + Map operatingDays, + Multimap dayTypeAssignments +) {} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/CalendarUtilities.java b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/CalendarUtilities.java new file mode 100644 index 00000000..a034752d --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/CalendarUtilities.java @@ -0,0 +1,143 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import jakarta.xml.bind.JAXBElement; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collector; +import org.rutebanken.netex.model.AvailabilityCondition; +import org.rutebanken.netex.model.Common_VersionFrameStructure; +import org.rutebanken.netex.model.DayType; +import org.rutebanken.netex.model.DayTypeAssignment; +import org.rutebanken.netex.model.DayTypesInFrame_RelStructure; +import org.rutebanken.netex.model.DayTypes_RelStructure; +import org.rutebanken.netex.model.OperatingPeriod_VersionStructure; +import org.rutebanken.netex.model.OperatingPeriods_RelStructure; +import org.rutebanken.netex.model.ValidBetween; +import org.rutebanken.netex.model.ValidityConditions_RelStructure; + +public class CalendarUtilities { + + static Collector> toMultimap( + Function keyMapper, + Function valueMapper + ) { + return Collector.of( + ArrayListMultimap::create, // Supplier: Create a new Multimap + (multimap, assignment) -> + multimap.put( + keyMapper.apply(assignment), + valueMapper.apply(assignment) + ), // Accumulator + (m1, m2) -> { // Combiner + m1.putAll(m2); + return m1; + } + ); + } + + static ValidBetween getValidBetween( + ValidityConditions_RelStructure validityConditionStruct + ) { + return Optional + .ofNullable(validityConditionStruct) + .map( + ValidityConditions_RelStructure::getValidityConditionRefOrValidBetweenOrValidityCondition_ + ) + .filter(elements -> !elements.isEmpty()) + .map(elements -> elements.get(0)) + .flatMap(CalendarUtilities::toValidBetween) + .orElse(null); + } + + private static Optional toValidBetween( + Object validityConditionElement + ) { + if (validityConditionElement instanceof ValidBetween) { + return Optional.of((ValidBetween) validityConditionElement); + } + + if (validityConditionElement instanceof javax.xml.bind.JAXBElement) { + return handleJaxbElement( + (javax.xml.bind.JAXBElement) validityConditionElement + ); + } + + throw new RuntimeException( + "Only support ValidBetween and AvailabilityCondition as validityCondition" + ); + } + + private static Optional handleJaxbElement( + javax.xml.bind.JAXBElement jaxbElement + ) { + Object value = jaxbElement.getValue(); + + if (value instanceof AvailabilityCondition availabilityCondition) { + return Optional.of( + new ValidBetween() + .withFromDate(availabilityCondition.getFromDate()) + .withToDate(availabilityCondition.getToDate()) + ); + } + + throw new RuntimeException( + "Only support ValidBetween and AvailabilityCondition as validityCondition" + ); + } + + public static ValidBetween getValidityForFrameOrDefault( + Common_VersionFrameStructure frameStructure, + ValidBetween defaultValidity + ) { + if (frameStructure.getContentValidityConditions() != null) { + return getValidBetween(frameStructure.getContentValidityConditions()); + } + + if (frameStructure.getValidityConditions() != null) { + return getValidBetween(frameStructure.getValidityConditions()); + } + + if ( + frameStructure.getValidBetween() != null && + !frameStructure.getValidBetween().isEmpty() + ) { + return frameStructure.getValidBetween().get(0); + } + return defaultValidity; + } + + static boolean isWithinValidRange( + LocalDateTime dateOfOperation, + ValidBetween validBetween + ) { + if (validBetween == null) { + // Always valid + return true; + } else if ( + validBetween.getFromDate() != null && validBetween.getToDate() != null + ) { + // Limited by both from and to date + return ( + !dateOfOperation.isBefore(validBetween.getFromDate()) && + !dateOfOperation.isAfter(validBetween.getToDate()) + ); + } else if (validBetween.getFromDate() != null) { + // Must be after valid start date + return !dateOfOperation.isBefore(validBetween.getFromDate()); + } else if (validBetween.getToDate() != null) { + // Must be before valid start date + return dateOfOperation.isBefore(validBetween.getToDate()); + } else { + // Both from and to empty + return true; + } + } + + static V getOrDefault(V value, V defaultValue) { + return value != null ? value : defaultValue; + } +} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarFrameObject.java b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarFrameObject.java new file mode 100644 index 00000000..81564c2d --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarFrameObject.java @@ -0,0 +1,133 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import static no.entur.antu.netexdata.collectors.activedatecollector.calender.CalendarUtilities.getValidityForFrameOrDefault; +import static no.entur.antu.netexdata.collectors.activedatecollector.calender.CalendarUtilities.toMultimap; + +import com.google.common.collect.Multimap; +import jakarta.xml.bind.JAXBElement; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import no.entur.antu.exception.AntuException; +import org.entur.netex.validation.validator.model.DayTypeId; +import org.entur.netex.validation.validator.model.OperatingDayId; +import org.rutebanken.netex.model.DayType; +import org.rutebanken.netex.model.DayTypeAssignment; +import org.rutebanken.netex.model.DayTypeAssignmentsInFrame_RelStructure; +import org.rutebanken.netex.model.DayTypesInFrame_RelStructure; +import org.rutebanken.netex.model.EntityStructure; +import org.rutebanken.netex.model.OperatingDay; +import org.rutebanken.netex.model.OperatingDaysInFrame_RelStructure; +import org.rutebanken.netex.model.OperatingPeriod; +import org.rutebanken.netex.model.OperatingPeriodsInFrame_RelStructure; +import org.rutebanken.netex.model.ServiceCalendarFrame; +import org.rutebanken.netex.model.ValidBetween; + +public record ServiceCalendarFrameObject( + ValidBetween validBetween, + CalendarData calendarData, + ServiceCalendarObject serviceCalendar +) { + public static ServiceCalendarFrameObject ofNullable( + ServiceCalendarFrame serviceCalendarFrame + ) { + return ofNullable(serviceCalendarFrame, null); + } + + public static ServiceCalendarFrameObject ofNullable( + ServiceCalendarFrame serviceCalendarFrame, + ValidBetween compositeFrameValidity + ) { + if (serviceCalendarFrame == null) { + throw new AntuException( + "ServiceCalendarFrame_VersionFrameStructure is null" + ); + } + ValidBetween serviceCalendarFrameValidity = getValidityForFrameOrDefault( + serviceCalendarFrame, + compositeFrameValidity + ); + return new ServiceCalendarFrameObject( + serviceCalendarFrameValidity, + new CalendarData( + getDayTypes(serviceCalendarFrame), + getOperatingPeriods(serviceCalendarFrame), + getOperatingDays(serviceCalendarFrame), + getDayTypeAssignmentByDayTypeId(serviceCalendarFrame) + ), + ServiceCalendarObject.ofNullable( + serviceCalendarFrame.getServiceCalendar(), + serviceCalendarFrameValidity + ) + ); + } + + private static Multimap getDayTypeAssignmentByDayTypeId( + ServiceCalendarFrame serviceCalendarFrame + ) { + return Optional + .ofNullable(serviceCalendarFrame.getDayTypeAssignments()) + .map(DayTypeAssignmentsInFrame_RelStructure::getDayTypeAssignment) + .stream() + .flatMap(Collection::stream) + .collect(toMultimap(DayTypeId::of, Function.identity())); + } + + private static Map getOperatingDays( + ServiceCalendarFrame serviceCalendarFrame + ) { + return Optional + .ofNullable(serviceCalendarFrame.getOperatingDays()) + .map(OperatingDaysInFrame_RelStructure::getOperatingDay) + .stream() + .flatMap(Collection::stream) + .collect(Collectors.toMap(OperatingDayId::of, Function.identity())); + } + + private static Map getOperatingPeriods( + ServiceCalendarFrame serviceCalendarFrame + ) { + return Optional + .ofNullable(serviceCalendarFrame.getOperatingPeriods()) + .map( + OperatingPeriodsInFrame_RelStructure::getOperatingPeriodOrUicOperatingPeriod + ) + .stream() + .flatMap(List::stream) + .filter(OperatingPeriod.class::isInstance) + .map(OperatingPeriod.class::cast) + .collect(Collectors.toMap(EntityStructure::getId, Function.identity())); + } + + private static Map getDayTypes( + ServiceCalendarFrame serviceCalendarFrame + ) { + return Optional + .ofNullable(serviceCalendarFrame.getDayTypes()) + .map(ServiceCalendarFrameObject::parseDayTypes) + .stream() + .flatMap(List::stream) + .filter(dayType -> DayTypeId.isValid(dayType.getId())) + .collect( + Collectors.toMap( + dayType -> new DayTypeId(dayType.getId()), + Function.identity() + ) + ); + } + + private static List parseDayTypes( + DayTypesInFrame_RelStructure element + ) { + return element + .getDayType_() + .stream() + .map(JAXBElement::getValue) + .filter(DayType.class::isInstance) + .map(DayType.class::cast) + .toList(); + } +} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarObject.java b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarObject.java new file mode 100644 index 00000000..56c59f3d --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarObject.java @@ -0,0 +1,168 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import static no.entur.antu.netexdata.collectors.activedatecollector.calender.CalendarUtilities.getValidBetween; +import static no.entur.antu.netexdata.collectors.activedatecollector.calender.CalendarUtilities.toMultimap; + +import com.google.common.collect.Multimap; +import jakarta.xml.bind.JAXBElement; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.entur.netex.validation.validator.model.DayTypeId; +import org.entur.netex.validation.validator.model.OperatingDayId; +import org.rutebanken.netex.model.DayType; +import org.rutebanken.netex.model.DayTypeAssignment; +import org.rutebanken.netex.model.DayTypeAssignments_RelStructure; +import org.rutebanken.netex.model.DayTypes_RelStructure; +import org.rutebanken.netex.model.EntityStructure; +import org.rutebanken.netex.model.OperatingDay; +import org.rutebanken.netex.model.OperatingDays_RelStructure; +import org.rutebanken.netex.model.OperatingPeriod; +import org.rutebanken.netex.model.OperatingPeriod_VersionStructure; +import org.rutebanken.netex.model.OperatingPeriods_RelStructure; +import org.rutebanken.netex.model.ServiceCalendar; +import org.rutebanken.netex.model.ValidBetween; + +public record ServiceCalendarObject( + ValidBetween validBetween, + CalendarData calendarData +) { + static ServiceCalendarObject ofNullable( + ServiceCalendar serviceCalendar, + ValidBetween serviceCalendarFrameValidity + ) { + if (serviceCalendar == null) { + return null; + } + return new ServiceCalendarObject( + getServiceCalendarValidity(serviceCalendar, serviceCalendarFrameValidity), + new CalendarData( + getDayTypes(serviceCalendar), + getOperatingPeriods(serviceCalendar), + getOperatingDays(serviceCalendar), + getDayTypeAssignmentByDayTypeId(serviceCalendar) + ) + ); + } + + private static Multimap getDayTypeAssignmentByDayTypeId( + ServiceCalendar serviceCalendar + ) { + return Optional + .ofNullable(serviceCalendar.getDayTypeAssignments()) + .map(DayTypeAssignments_RelStructure::getDayTypeAssignment) + .stream() + .flatMap(Collection::stream) + .collect(toMultimap(DayTypeId::of, Function.identity())); + } + + private static Map getOperatingDays( + ServiceCalendar serviceCalendar + ) { + return Optional + .ofNullable(serviceCalendar.getOperatingDays()) + .map(OperatingDays_RelStructure::getOperatingDayRefOrOperatingDay) + .stream() + .flatMap(Collection::stream) + .filter(OperatingDay.class::isInstance) + .map(OperatingDay.class::cast) + .collect(Collectors.toMap(OperatingDayId::of, Function.identity())); + } + + private static Map getOperatingPeriods( + ServiceCalendar serviceCalendar + ) { + return Optional + .ofNullable(serviceCalendar.getOperatingPeriods()) + .map(ServiceCalendarObject::parseOperatingPeriods) + .stream() + .flatMap(List::stream) + .filter(OperatingPeriod.class::isInstance) + .map(OperatingPeriod.class::cast) + .collect(Collectors.toMap(EntityStructure::getId, Function.identity())); + } + + private static Map getDayTypes( + ServiceCalendar serviceCalendar + ) { + return Optional + .ofNullable(serviceCalendar.getDayTypes()) + .map(ServiceCalendarObject::parseDayTypes) + .stream() + .flatMap(List::stream) + .filter(dayType -> DayTypeId.isValid(dayType.getId())) + .collect( + Collectors.toMap( + dayType -> new DayTypeId(dayType.getId()), + Function.identity() + ) + ); + } + + private static ValidBetween getServiceCalendarValidity( + ServiceCalendar serviceCalendar, + ValidBetween serviceCalendarFrameValidity + ) { + if ( + serviceCalendar.getFromDate() != null && + serviceCalendar.getToDate() != null + ) { + LocalDateTime fromDateTime = serviceCalendar.getFromDate(); + LocalDateTime toDateTime = serviceCalendar.getToDate(); + return new ValidBetween() + .withFromDate(fromDateTime) + .withToDate(toDateTime); + } else { + ValidBetween entityValidity = getValidBetweenForServiceCalendar( + serviceCalendar + ); + if (entityValidity != null) { + return entityValidity; + } + return serviceCalendarFrameValidity; + } + } + + static ValidBetween getValidBetweenForServiceCalendar( + ServiceCalendar entityStruct + ) { + ValidBetween validBetween = null; + + if (entityStruct.getValidityConditions() != null) { + validBetween = getValidBetween(entityStruct.getValidityConditions()); + } else if ( + entityStruct.getValidBetween() != null && + !entityStruct.getValidBetween().isEmpty() + ) { + validBetween = entityStruct.getValidBetween().get(0); + } + + return validBetween; + } + + private static List parseDayTypes(DayTypes_RelStructure dayTypes) { + return dayTypes + .getDayTypeRefOrDayType_() + .stream() + .map(JAXBElement::getValue) + .filter(DayType.class::isInstance) + .map(DayType.class::cast) + .toList(); + } + + private static List parseOperatingPeriods( + OperatingPeriods_RelStructure operatingPeriods + ) { + return operatingPeriods + .getOperatingPeriodRefOrOperatingPeriodOrUicOperatingPeriod() + .stream() + .map(JAXBElement::getValue) + .filter(OperatingPeriod_VersionStructure.class::isInstance) + .map(OperatingPeriod_VersionStructure.class::cast) + .toList(); + } +} diff --git a/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ValidOperatingPeriod.java b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ValidOperatingPeriod.java new file mode 100644 index 00000000..f79c5e23 --- /dev/null +++ b/src/main/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ValidOperatingPeriod.java @@ -0,0 +1,101 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.entur.netex.validation.validator.model.OperatingDayId; +import org.rutebanken.netex.model.OperatingDay; +import org.rutebanken.netex.model.OperatingPeriod; +import org.rutebanken.netex.model.ValidBetween; + +record ValidOperatingPeriod(LocalDate startDate, LocalDate endDate) { + static ValidOperatingPeriod of( + OperatingPeriod operatingPeriod, + ValidBetween validBetween, + Map operatingDays + ) { + LocalDate fromDate = getFromDate(operatingPeriod, operatingDays); + LocalDate toDate = getToDate(operatingPeriod, operatingDays); + + return cutToValidityCondition(fromDate, toDate, validBetween); + } + + private static LocalDate getFromDate( + OperatingPeriod operatingPeriod, + Map operatingDays + ) { + return Optional + .ofNullable(OperatingDayId.ofFromOperatingDayRef(operatingPeriod)) + .map(operatingDays::get) + .map(OperatingDay::getCalendarDate) + .map(LocalDateTime::toLocalDate) + .orElseGet(() -> operatingPeriod.getFromDate().toLocalDate()); + } + + private static LocalDate getToDate( + OperatingPeriod operatingPeriod, + Map operatingDays + ) { + return Optional + .ofNullable(OperatingDayId.ofToOperatingDayRef(operatingPeriod)) + .map(operatingDays::get) + .map(OperatingDay::getCalendarDate) + .map(LocalDateTime::toLocalDate) + .orElseGet(() -> operatingPeriod.getToDate().toLocalDate()); + } + + // Adjust operating period to validity condition + private static ValidOperatingPeriod cutToValidityCondition( + LocalDate startDate, + LocalDate endDate, + ValidBetween validBetween + ) { + LocalDate validFrom = validBetween.getFromDate() != null + ? validBetween.getFromDate().toLocalDate() + : null; + LocalDate validTo = validBetween.getToDate() != null + ? validBetween.getToDate().toLocalDate() + : null; + + // Check if the period is completely outside the valid range + if ( + (validFrom != null && endDate.isBefore(validFrom)) || + (validTo != null && startDate.isAfter(validTo)) + ) { + return new ValidOperatingPeriod(startDate, endDate); + } + + // Adjust the start and end dates to be within the valid range + LocalDate adjustedStart = ( + validFrom != null && startDate.isBefore(validFrom) + ) + ? validFrom + : startDate; + LocalDate adjustedEnd = (validTo != null && endDate.isAfter(validTo)) + ? validTo + : endDate; + + return new ValidOperatingPeriod(adjustedStart, adjustedEnd); + } + + List toDates(List excludedDates, int intDayTypes) { + List dates = new ArrayList<>(); + + if (intDayTypes != 0) { + LocalDate date = startDate; + + while (!date.isAfter(endDate)) { + int aDayOfWeek = date.getDayOfWeek().getValue() - 1; + int aDayOfWeekFlag = 1 << aDayOfWeek; + if ((intDayTypes & aDayOfWeekFlag) == aDayOfWeekFlag) { + if (!excludedDates.contains(date)) dates.add(date); + } + date = date.plusDays(1); + } + } + return dates; + } +} diff --git a/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesContext.java b/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesContext.java new file mode 100644 index 00000000..1f3a765d --- /dev/null +++ b/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesContext.java @@ -0,0 +1,144 @@ +package no.entur.antu.validation.validator.interchange.waittime; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.entur.netex.validation.validator.jaxb.NetexDataRepository; +import org.entur.netex.validation.validator.model.ActiveDates; +import org.entur.netex.validation.validator.model.ActiveDatesId; +import org.entur.netex.validation.validator.model.DayTypeId; +import org.entur.netex.validation.validator.model.OperatingDayId; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.entur.netex.validation.validator.model.ServiceJourneyId; +import org.entur.netex.validation.validator.model.ServiceJourneyInterchangeInfo; +import org.entur.netex.validation.validator.model.ServiceJourneyStop; + +public record UnexpectedWaitTimeAndActiveDatesContext( + ServiceJourneyInterchangeInfo serviceJourneyInterchangeInfo, + // ServiceJourneyStop at the fromStopPoint in fromJourneyRef from Cache + ServiceJourneyStop fromServiceJourneyStop, + // ServiceJourneySStop at the toStopPoint in toJourneyRef from Cache + ServiceJourneyStop toServiceJourneyStop, + // Active dates for the fromJourneyRef from Cache + List fromServiceJourneyActiveDates, + // Active dates for the toJourneyRef from Cache + List toServiceJourneyActiveDates +) { + public static class Builder { + + private final String validationReportId; + private final NetexDataRepository netexDataRepository; + private Map> serviceJourneyIdListMap; + private Map> serviceJourneyDayTypesMap; + private Map activeDatesMap; + private Map> serviceJourneyOperatingDaysMap; + + public Builder( + String validationReportId, + NetexDataRepository netexDataRepository + ) { + this.validationReportId = validationReportId; + this.netexDataRepository = netexDataRepository; + } + + public UnexpectedWaitTimeAndActiveDatesContext.Builder primeCache() { + serviceJourneyIdListMap = + netexDataRepository.serviceJourneyStops(validationReportId); + serviceJourneyDayTypesMap = + netexDataRepository.serviceJourneyDayTypes(validationReportId); + activeDatesMap = netexDataRepository.activeDates(validationReportId); + serviceJourneyOperatingDaysMap = + netexDataRepository.serviceJourneyOperatingDays(validationReportId); + return this; + } + + public UnexpectedWaitTimeAndActiveDatesContext build( + ServiceJourneyInterchangeInfo serviceJourneyInterchangeInfo + ) { + return new UnexpectedWaitTimeAndActiveDatesContext( + serviceJourneyInterchangeInfo, + serviceJourneyStopAtScheduleStopPoint( + serviceJourneyInterchangeInfo.fromJourneyRef(), + serviceJourneyInterchangeInfo.fromStopPoint() + ), + serviceJourneyStopAtScheduleStopPoint( + serviceJourneyInterchangeInfo.toJourneyRef(), + serviceJourneyInterchangeInfo.toStopPoint() + ), + activeDatesForServiceJourney( + serviceJourneyInterchangeInfo.fromJourneyRef() + ), + activeDatesForServiceJourney( + serviceJourneyInterchangeInfo.toJourneyRef() + ) + ); + } + + private List activeDatesForServiceJourney( + ServiceJourneyId serviceJourneyId + ) { + List activeDateOfDayTypes = Optional + .ofNullable(serviceJourneyId) + .map(serviceJourneyDayTypesMap::get) + .map(dayTypeIds -> + dayTypeIds + .stream() + .map(activeDatesMap::get) + .map(ActiveDates::dates) + .flatMap(List::stream) + .toList() + ) + .orElse(List.of()); + + List activeDateOfDatedServiceJourneys = Optional + .ofNullable(serviceJourneyId) + .map(serviceJourneyOperatingDaysMap::get) + .map(dayTypeIds -> + dayTypeIds + .stream() + .map(activeDatesMap::get) + .map(ActiveDates::dates) + .flatMap(List::stream) + .toList() + ) + .orElse(List.of()); + + return Stream + .of(activeDateOfDayTypes, activeDateOfDatedServiceJourneys) + .flatMap(List::stream) + .toList(); + } + + private ServiceJourneyStop serviceJourneyStopAtScheduleStopPoint( + ServiceJourneyId serviceJourneyId, + ScheduledStopPointId scheduledStopPointId + ) { + return Optional + .ofNullable(serviceJourneyId) + .map(serviceJourneyIdListMap::get) + .flatMap(serviceJourneyStops -> + serviceJourneyStops + .stream() + .filter(serviceJourneyStop -> + serviceJourneyStop + .scheduledStopPointId() + .equals(scheduledStopPointId) + ) + .map(ServiceJourneyStop::fixMissingTimeValues) + .findFirst() + ) + .orElse(null); + } + } + + public boolean isValid() { + return ( + serviceJourneyInterchangeInfo != null && + serviceJourneyInterchangeInfo.isValid() && + fromServiceJourneyStop != null && + toServiceJourneyStop != null + ); + } +} diff --git a/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesError.java b/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesError.java new file mode 100644 index 00000000..8927720c --- /dev/null +++ b/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesError.java @@ -0,0 +1,53 @@ +package no.entur.antu.validation.validator.interchange.waittime; + +import no.entur.antu.validation.ValidationError; +import no.entur.antu.validation.utilities.Comparison; + +public record UnexpectedWaitTimeAndActiveDatesError( + RuleCode ruleCode, + String interchangeId, + String fromStopPointName, + String toStopPointName, + Comparison waitTimeComparison +) + implements ValidationError { + @Override + public String getRuleCode() { + return ruleCode.toString(); + } + + @Override + public String validationReportEntryMessage() { + return ( + String.format( + "Wait time between stop points (%s - %s) is more than expected. Expected: %s, actual: %s.", + fromStopPointName, + toStopPointName, + waitTimeComparison.expected(), + waitTimeComparison.actual() + ) + ); + } + + @Override + public String getEntityId() { + return interchangeId; + } + + enum RuleCode implements no.entur.antu.validation.RuleCode { + WAIT_TIME_ABOVE_THE_CONFIGURED_THRESHOLD( + "Wait time is above the configured threshold." + ); + + private final String errorMessage; + + RuleCode(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + } +} diff --git a/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesValidator.java b/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesValidator.java new file mode 100644 index 00000000..ae6feeba --- /dev/null +++ b/src/main/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesValidator.java @@ -0,0 +1,200 @@ +package no.entur.antu.validation.validator.interchange.waittime; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import no.entur.antu.validation.utilities.Comparison; +import org.entur.netex.validation.validator.AbstractDatasetValidator; +import org.entur.netex.validation.validator.DataLocation; +import org.entur.netex.validation.validator.ValidationReport; +import org.entur.netex.validation.validator.ValidationReportEntry; +import org.entur.netex.validation.validator.ValidationReportEntryFactory; +import org.entur.netex.validation.validator.jaxb.NetexDataRepository; +import org.entur.netex.validation.validator.model.ServiceJourneyInterchangeInfo; +import org.entur.netex.validation.validator.model.ServiceJourneyStop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Verify that wait time is not above configured threshold. + * Must check that the two vehicle journeys share at least one active date, + * or in the case of interchanges around midnight; consecutive dates. + * Chouette reference: + * 3-Interchange-8-1, + * 3-Interchange-8-2, + * 3-Interchange-10 + */ +public class UnexpectedWaitTimeAndActiveDatesValidator + extends AbstractDatasetValidator { + + private static final Logger LOGGER = LoggerFactory.getLogger( + UnexpectedWaitTimeAndActiveDatesValidator.class + ); + + // Marduk/Chouette config parameter: interchange_max_wait_seconds = 3600 Seconds + + // Warning wait time for interchange is 1 hour + private static final int INTERCHANGE_WARNING_WAIT_TIME_MILLIS = 3600000; // 1 Hour + + // Maximum wait time for interchange is 3 hours + private static final int INTERCHANGE_ERROR_WAIT_TIME_MILLIS = + INTERCHANGE_WARNING_WAIT_TIME_MILLIS * 3; // 3 Hours + + private final NetexDataRepository netexDataRepository; + + public UnexpectedWaitTimeAndActiveDatesValidator( + ValidationReportEntryFactory validationReportEntryFactory, + NetexDataRepository netexDataRepository + ) { + super(validationReportEntryFactory); + this.netexDataRepository = netexDataRepository; + } + + @Override + public ValidationReport validate(ValidationReport validationReport) { + LOGGER.info("Validating interchange wait time."); + + List serviceJourneyInterchangeInfos = + netexDataRepository.serviceJourneyInterchangeInfos( + validationReport.getValidationReportId() + ); + + if ( + serviceJourneyInterchangeInfos == null || + serviceJourneyInterchangeInfos.isEmpty() + ) { + return validationReport; + } + + UnexpectedWaitTimeAndActiveDatesContext.Builder builder = + new UnexpectedWaitTimeAndActiveDatesContext.Builder( + validationReport.getValidationReportId(), + netexDataRepository + ); + + builder.primeCache(); + + serviceJourneyInterchangeInfos + .stream() + .map(builder::build) + .filter(Objects::nonNull) + .filter(UnexpectedWaitTimeAndActiveDatesContext::isValid) + .map(this::validateWaitTime) + .filter(Objects::nonNull) + .forEach(validationReport::addValidationReportEntry); + + return validationReport; + } + + private ValidationReportEntry validateWaitTime( + UnexpectedWaitTimeAndActiveDatesContext context + ) { + long MILLIS_PER_DAY = 86400000L; + + int dayOffsetDiff = + context.toServiceJourneyStop().departureDayOffset() - + context.fromServiceJourneyStop().arrivalDayOffset(); + + long msWait = + ( + Optional + .ofNullable(context.toServiceJourneyStop().departureTime()) + .map(LocalTime::toSecondOfDay) + .orElse(0) - + Optional + .ofNullable(context.fromServiceJourneyStop().arrivalTime()) + .map(LocalTime::toSecondOfDay) + .orElse(0) + ) * + 1000L; + + if (msWait < 0) { + msWait = MILLIS_PER_DAY + msWait; + dayOffsetDiff--; + } + + if (!hasSharedActiveDate(context, dayOffsetDiff)) { + return createValidationReportEntry( + "NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE", + context.serviceJourneyInterchangeInfo().interchangeId(), + context.serviceJourneyInterchangeInfo().filename(), + context.fromServiceJourneyStop(), + context.toServiceJourneyStop(), + Comparison.of( + String.valueOf(msWait / 1000), + String.valueOf(INTERCHANGE_ERROR_WAIT_TIME_MILLIS / 1000) + ) + ); + } else if (msWait > INTERCHANGE_WARNING_WAIT_TIME_MILLIS) { + if (msWait > INTERCHANGE_ERROR_WAIT_TIME_MILLIS) { + return createValidationReportEntry( + "WAIT_TIME_IN_INTERCHANGE_EXCEEDS_MAX_LIMIT", + context.serviceJourneyInterchangeInfo().interchangeId(), + context.serviceJourneyInterchangeInfo().filename(), + context.fromServiceJourneyStop(), + context.toServiceJourneyStop(), + Comparison.of( + String.valueOf(msWait / 1000), + String.valueOf(INTERCHANGE_ERROR_WAIT_TIME_MILLIS / 1000) + ) + ); + } else { + return createValidationReportEntry( + "WAIT_TIME_IN_INTERCHANGE_EXCEEDS_WARNING_LIMIT", + context.serviceJourneyInterchangeInfo().interchangeId(), + context.serviceJourneyInterchangeInfo().filename(), + context.fromServiceJourneyStop(), + context.toServiceJourneyStop(), + Comparison.of( + String.valueOf(msWait / 1000), + String.valueOf(INTERCHANGE_WARNING_WAIT_TIME_MILLIS / 1000) + ) + ); + } + } + return null; + } + + private boolean hasSharedActiveDate( + UnexpectedWaitTimeAndActiveDatesContext context, + int daysOffset + ) { + List fromServiceJourneyActiveDates = + context.fromServiceJourneyActiveDates(); + for (LocalDate toServiceJourneyActiveDate : context.toServiceJourneyActiveDates()) { + LocalDate toServiceJourneyActiveDateWithOffset = + toServiceJourneyActiveDate.plusDays(daysOffset); + if ( + fromServiceJourneyActiveDates.contains( + toServiceJourneyActiveDateWithOffset + ) + ) { + return true; + } + } + return false; + } + + private ValidationReportEntry createValidationReportEntry( + String ruleCode, + String interchangeId, + String filename, + ServiceJourneyStop fromJourneyStop, + ServiceJourneyStop toJourneyStop, + Comparison comparison + ) { + return createValidationReportEntry( + ruleCode, + new DataLocation(interchangeId, filename, 0, 0), + String.format( + "Wait time between stops (%s) and (%s) is expected %s sec. but was %s sec.", + fromJourneyStop.scheduledStopPointId(), + toJourneyStop.scheduledStopPointId(), + comparison.expected(), + comparison.actual() + ) + ); + } +} diff --git a/src/main/java/no/entur/antu/validation/validator/support/NetexUtils.java b/src/main/java/no/entur/antu/validation/validator/support/NetexUtils.java index 4d92e9bd..de9c3def 100644 --- a/src/main/java/no/entur/antu/validation/validator/support/NetexUtils.java +++ b/src/main/java/no/entur/antu/validation/validator/support/NetexUtils.java @@ -6,10 +6,12 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; 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.ServiceJourney; import org.rutebanken.netex.model.StopPointInJourneyPattern; import org.rutebanken.netex.model.TimetabledPassingTime; @@ -88,4 +90,30 @@ public static StopPointInJourneyPattern stopPointInJourneyPattern( .findFirst() .orElse(null); } + + /** + * Returns the Stream of all the valid ServiceJourneys in all the TimeTableFrames. + * The valid serviceJourneys are those that have number of timetabledPassingTime equals to number of StopPointsInJourneyPattern. + * This is validated with SERVICE_JOURNEY_10. + */ + public static List validServiceJourneys( + JAXBValidationContext validationContext + ) { + return validationContext + .serviceJourneys() + .stream() + .filter(serviceJourney -> { + JourneyPattern journeyPattern = validationContext.journeyPattern( + serviceJourney + ); + if (journeyPattern == null) { + return false; + } + return ( + stopPointsInJourneyPattern(journeyPattern).size() == + validationContext.timetabledPassingTimes(serviceJourney).size() + ); + }) + .toList(); + } } diff --git a/src/main/resources/configuration.antu.yaml b/src/main/resources/configuration.antu.yaml index cae0e0aa..173861b7 100644 --- a/src/main/resources/configuration.antu.yaml +++ b/src/main/resources/configuration.antu.yaml @@ -129,3 +129,12 @@ validationRuleConfigs: - code: TO_POINT_REF_IN_INTERCHANGE_IS_NOT_PART_OF_TO_JOURNEY_REF name: ToPointRef in interchange is not a part of ToJourneyRef severity: WARNING + - code: NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE + name: No shared active date found in interchange + severity: WARNING + - code: WAIT_TIME_IN_INTERCHANGE_EXCEEDS_WARNING_LIMIT + name: Wait time in interchange exceeds warning limit + severity: WARNING + - code: WAIT_TIME_IN_INTERCHANGE_EXCEEDS_MAX_LIMIT + name: Wait time in interchange exceeds maximum limit + severity: WARNING diff --git a/src/test/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ActiveDatesBuilderTest.java b/src/test/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ActiveDatesBuilderTest.java new file mode 100644 index 00000000..2b5569d1 --- /dev/null +++ b/src/test/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ActiveDatesBuilderTest.java @@ -0,0 +1,13 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import no.entur.antu.netextestdata.NetexEntitiesTestFactory; +import org.junit.jupiter.api.Test; + +class ActiveDatesBuilderTest { + + @Test + void testBuildActiveDatesPerDayTypes() { + NetexEntitiesTestFactory netexEntitiesTestFactory = + new NetexEntitiesTestFactory(); + } +} diff --git a/src/test/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarFrameObjectTest.java b/src/test/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarFrameObjectTest.java new file mode 100644 index 00000000..2badb60a --- /dev/null +++ b/src/test/java/no/entur/antu/netexdata/collectors/activedatecollector/calender/ServiceCalendarFrameObjectTest.java @@ -0,0 +1,37 @@ +package no.entur.antu.netexdata.collectors.activedatecollector.calender; + +import java.time.LocalDate; +import no.entur.antu.netextestdata.NetexEntitiesTestFactory; +import org.junit.jupiter.api.Test; +import org.rutebanken.netex.model.DayOfWeekEnumeration; + +class ServiceCalendarFrameObjectTest { + + @Test + void ofServiceCalendarFrame() { + NetexEntitiesTestFactory testFactory = new NetexEntitiesTestFactory(); + NetexEntitiesTestFactory.CreateDayType createDayType = testFactory + .dayType(1) + .withDaysOfWeek( + DayOfWeekEnumeration.MONDAY, + DayOfWeekEnumeration.TUESDAY + ); + + NetexEntitiesTestFactory.CreateOperatingDay createOperatingDay = + testFactory.operatingDay(1, LocalDate.of(2014, 11, 20)); + + NetexEntitiesTestFactory.CreateOperatingPeriod createOperatingPeriod = + testFactory.operatingPeriod( + 1, + LocalDate.of(2014, 11, 21), + LocalDate.of(2014, 11, 22) + ); + + testFactory + .serviceCalendarFrame(1) + .withDayTypes(createDayType) + .withOperatingDays(createOperatingDay) + .withOperatingPeriods(createOperatingPeriod); + + } +} diff --git a/src/test/java/no/entur/antu/validation/ValidationTest.java b/src/test/java/no/entur/antu/validation/ValidationTest.java index 50038826..3445d492 100644 --- a/src/test/java/no/entur/antu/validation/ValidationTest.java +++ b/src/test/java/no/entur/antu/validation/ValidationTest.java @@ -119,6 +119,30 @@ protected void mockGetServiceJourneyStops( .thenReturn(serviceJourneyStops); } + protected void mockGetServiceJourneyDayTypes( + Map> serviceJourneyDayTypes + ) { + Mockito + .when(netexDataRepositoryMock.serviceJourneyDayTypes(anyString())) + .thenReturn(serviceJourneyDayTypes); + } + + protected void mockGetServiceJourneyOperatingDays( + Map> serviceJourneyOperatingDays + ) { + Mockito + .when(netexDataRepositoryMock.serviceJourneyOperatingDays(anyString())) + .thenReturn(serviceJourneyOperatingDays); + } + + protected void mockGetActiveDays( + Map activeDates + ) { + Mockito + .when(netexDataRepositoryMock.activeDates(anyString())) + .thenReturn(activeDates); + } + protected void mockGetServiceJourneyInterchangeInfo( List serviceJourneyInterchangeInfos ) { diff --git a/src/test/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesValidatorTest.java b/src/test/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesValidatorTest.java new file mode 100644 index 00000000..e11faf00 --- /dev/null +++ b/src/test/java/no/entur/antu/validation/validator/interchange/waittime/UnexpectedWaitTimeAndActiveDatesValidatorTest.java @@ -0,0 +1,1006 @@ +package no.entur.antu.validation.validator.interchange.waittime; + +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import no.entur.antu.netextestdata.NetexEntitiesTestFactory; +import no.entur.antu.validation.ValidationTest; +import org.entur.netex.validation.validator.ValidationReport; +import org.entur.netex.validation.validator.ValidationReportEntry; +import org.entur.netex.validation.validator.model.ActiveDates; +import org.entur.netex.validation.validator.model.ActiveDatesId; +import org.entur.netex.validation.validator.model.DayTypeId; +import org.entur.netex.validation.validator.model.OperatingDayId; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.entur.netex.validation.validator.model.ServiceJourneyId; +import org.entur.netex.validation.validator.model.ServiceJourneyInterchangeInfo; +import org.entur.netex.validation.validator.model.ServiceJourneyStop; +import org.junit.jupiter.api.Test; +import org.rutebanken.netex.model.ServiceJourneyInterchange; + +class UnexpectedWaitTimeAndActiveDatesValidatorTest extends ValidationTest { + + private class TestInterchange { + + static class TestServiceJourney { + + private TestServiceJourney(int serviceJourneyId) { + this.serviceJourneyId = serviceJourneyId; + } + + static class TestActiveDates { + + String activeDatesRef; + List activeDates = new ArrayList<>(); + + public ActiveDatesId getActiveDatesId() { + return ActiveDatesId.of(activeDatesRef); + } + + public ActiveDates getActiveDates() { + return new ActiveDates(activeDates); + } + + TestActiveDates withActiveDatesRef(String activeDatesRef) { + this.activeDatesRef = activeDatesRef; + return this; + } + + TestActiveDates addActiveDate(LocalDate activeDate) { + this.activeDates.add(activeDate); + return this; + } + } + + int serviceJourneyId; + int scheduledStopPointId; + List dayTypes = new ArrayList<>(); + List operatingDays = new ArrayList<>(); + LocalTime arrivalTime; + LocalTime departureTime; + int arrivalDayOffset; + int departureDayOffset; + + public ServiceJourneyId serviceJourneyId() { + return new ServiceJourneyId("TST:ServiceJourney:" + serviceJourneyId); + } + + public ScheduledStopPointId scheduledStopPointId() { + return new ScheduledStopPointId( + "TST:ScheduledStopPoint:" + scheduledStopPointId + ); + } + + public ServiceJourneyStop serviceJourneyStop() { + return new ServiceJourneyStop( + scheduledStopPointId(), + arrivalTime, + departureTime, + arrivalDayOffset, + departureDayOffset + ); + } + + public List dayTypeRefs() { + return dayTypes + .stream() + .map(activeDate -> activeDate.activeDatesRef) + .map(DayTypeId::new) + .toList(); + } + + public List operatingDayRefs() { + return operatingDays + .stream() + .map(activeDate -> activeDate.activeDatesRef) + .map(OperatingDayId::new) + .toList(); + } + + public Map activeDatesMap() { + return Stream + .of(dayTypes, operatingDays) + .flatMap(List::stream) + .collect( + toMap( + TestActiveDates::getActiveDatesId, + TestActiveDates::getActiveDates + ) + ); + } + + TestServiceJourney withScheduledStopPointId(int scheduledStopPointId) { + this.scheduledStopPointId = scheduledStopPointId; + return this; + } + + TestServiceJourney addTestDayType( + int dayTypeId, + LocalDate... activeDates + ) { + TestActiveDates dayTypes = new TestActiveDates() + .withActiveDatesRef("TST:DayType:" + dayTypeId); + Stream.of(activeDates).forEach(dayTypes::addActiveDate); + this.dayTypes.add(dayTypes); + return this; + } + + TestServiceJourney addTestOperatingDays( + int operatingDayId, + LocalDate... activeDates + ) { + TestActiveDates operatingDays = new TestActiveDates() + .withActiveDatesRef("TST:OperatingDay:" + operatingDayId); + Stream.of(activeDates).forEach(operatingDays::addActiveDate); + this.operatingDays.add(operatingDays); + return this; + } + + TestServiceJourney withArrivalTime(LocalTime arrivalTime) { + this.arrivalTime = arrivalTime; + return this; + } + + TestServiceJourney withDepartureTime(LocalTime departureTime) { + this.departureTime = departureTime; + return this; + } + + TestServiceJourney withArrivalDayOffset(int arrivalDayOffset) { + this.arrivalDayOffset = arrivalDayOffset; + return this; + } + + TestServiceJourney withDepartureDayOffset(int departureDayOffset) { + this.departureDayOffset = departureDayOffset; + return this; + } + } + + private TestServiceJourney fromServiceJourney; + private TestServiceJourney toServiceJourney; + + TestServiceJourney newTestServiceJourney(int serviceJourneyId) { + return new TestServiceJourney(serviceJourneyId); + } + + TestInterchange withFromServiceJourney( + TestServiceJourney fromServiceJourney + ) { + this.fromServiceJourney = fromServiceJourney; + return this; + } + + TestInterchange withToServiceJourney(TestServiceJourney toServiceJourney) { + this.toServiceJourney = toServiceJourney; + return this; + } + + public void doMock() { + mockGetServiceJourneyStops( + Map.of( + fromServiceJourney.serviceJourneyId(), + List.of(fromServiceJourney.serviceJourneyStop()), + toServiceJourney.serviceJourneyId(), + List.of(toServiceJourney.serviceJourneyStop()) + ) + ); + + mockGetServiceJourneyDayTypes( + Map.of( + fromServiceJourney.serviceJourneyId(), + fromServiceJourney.dayTypeRefs(), + toServiceJourney.serviceJourneyId(), + toServiceJourney.dayTypeRefs() + ) + ); + + mockGetServiceJourneyOperatingDays( + Map.of( + fromServiceJourney.serviceJourneyId(), + fromServiceJourney.operatingDayRefs(), + toServiceJourney.serviceJourneyId(), + toServiceJourney.operatingDayRefs() + ) + ); + + mockGetActiveDays( + Stream + .of( + fromServiceJourney.activeDatesMap(), + toServiceJourney.activeDatesMap() + ) + .flatMap(map -> map.entrySet().stream()) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)) + ); + } + + private ValidationReport runTest() { + NetexEntitiesTestFactory factory = new NetexEntitiesTestFactory(); + + ServiceJourneyInterchange serviceJourneyInterchange = factory + .serviceJourneyInterchange() + .withFromPointRef(fromServiceJourney.scheduledStopPointId()) + .withToPointRef(toServiceJourney.scheduledStopPointId()) + .withFromJourneyRef(fromServiceJourney.serviceJourneyId()) + .withToJourneyRef(toServiceJourney.serviceJourneyId()) + .create(); + + mockGetServiceJourneyInterchangeInfo( + List.of( + ServiceJourneyInterchangeInfo.of( + "test.xml", + serviceJourneyInterchange + ) + ) + ); + + return runDatasetValidation( + UnexpectedWaitTimeAndActiveDatesValidator.class + ); + } + } + + @Test + void testValidWaitTimeAndActiveDays() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + /* + * Interchange with feeder service journey with days types and consumer service journey with dated service journey. + * Is this a real world scenario? + */ + @Test + void testValidWaitTimeAndActiveDays_MixOfDaysTypesAndDatedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + @Test + void testValidWaitTimeAndActiveDaysWithDaysOffSet() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + .withDepartureDayOffset(3) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + .withArrivalDayOffset(3) + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + @Test + void testWaitTimeEqualsWarningLimit() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 53, 0)) + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + /* + * Test that the wait time between two service journeys is less than the warning limit. + * Waiting Limit is 1 hour + */ + @Test + void testWaitTimeExceedingWarningLimit() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(10, 55, 0)) // waiting time 1 hour and 2 minutes + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_WARNING_LIMIT") + ); + } + + /* + * Test that the wait time between two service journeys is less than the warning limit. + * Waiting Limit is 1 hour + */ + @Test + void testWaitTimeExceedingWarningLimitWithDayOffset() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 2)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(10, 55, 0)) // waiting time 1 hour and 2 minutes + .withArrivalDayOffset(1) + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_WARNING_LIMIT") + ); + } + + /* + * Test that the wait time between two service journeys is less than the maximum limit. + * Maximum Limit is 3 hours + */ + @Test + void testWaitTimeExceedingErrorLimit() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(12, 55, 0)) // waiting time 3 hours 2 minutes + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_MAX_LIMIT") + ); + } + + /* + * Test that the wait time between two service journeys is less than the maximum limit. + * Maximum Limit is 3 hours + */ + @Test + void testWaitTimeExceedingErrorLimitWithDayOffset() { + TestInterchange testInterchange = new TestInterchange(); + + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + .withDepartureDayOffset(1) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 2)) + .withArrivalTime(LocalTime.of(12, 55, 0)) // waiting time 3 hours 2 minutes + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_MAX_LIMIT") + ); + } + + @Test + void testNoSharedActiveDays_oneDayTypePerServiceJourney() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(2, LocalDate.of(2024, 11, 2)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE") + ); + } + + @Test + void testNoSharedActiveDays_multipleDayTypePerServiceJourney() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .addTestDayType(2, LocalDate.of(2024, 11, 3)) + .addTestDayType(3, LocalDate.of(2024, 11, 5)) + .addTestDayType(4, LocalDate.of(2024, 11, 7)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(5, LocalDate.of(2024, 11, 2)) + .addTestDayType(6, LocalDate.of(2024, 11, 4)) + .addTestDayType(7, LocalDate.of(2024, 11, 6)) + .addTestDayType(8, LocalDate.of(2024, 11, 8)) + .addTestDayType(9, LocalDate.of(2024, 11, 10)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE") + ); + } + + @Test + void testHasSharedActiveDay_multipleDayTypePerServiceJourney() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .addTestDayType(2, LocalDate.of(2024, 11, 3)) // shared active date + .addTestDayType(3, LocalDate.of(2024, 11, 5)) + .addTestDayType(4, LocalDate.of(2024, 11, 7)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestDayType(5, LocalDate.of(2024, 11, 2)) + .addTestDayType(6, LocalDate.of(2024, 11, 3)) // shared active date + .addTestDayType(7, LocalDate.of(2024, 11, 6)) + .addTestDayType(8, LocalDate.of(2024, 11, 8)) + .addTestDayType(9, LocalDate.of(2024, 11, 10)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + @Test + void testValidWaitTimeAndActiveDays_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + @Test + void testValidWaitTimeAndActiveDaysWithDaysOffSet_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + .withDepartureDayOffset(3) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + .withArrivalDayOffset(3) + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + @Test + void testWaitTimeEqualsWarningLimit_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 53, 0)) + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(0)); + } + + /* + * Test that the wait time between two service journeys is less than the warning limit. + * Waiting Limit is 1 hour + */ + @Test + void testWaitTimeExceedingWarningLimit_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(10, 55, 0)) // waiting time 1 hour and 2 minutes + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_WARNING_LIMIT") + ); + } + + /* + * Test that the wait time between two service journeys is less than the warning limit. + * Waiting Limit is 1 hour + */ + @Test + void testWaitTimeExceedingWarningLimitWithDayOffset_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 2)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(10, 55, 0)) // waiting time 1 hour and 2 minutes + .withArrivalDayOffset(1) + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_WARNING_LIMIT") + ); + } + + /* + * Test that the wait time between two service journeys is less than the maximum limit. + * Maximum Limit is 3 hours + */ + @Test + void testWaitTimeExceedingErrorLimit_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(12, 55, 0)) // waiting time 3 hours 2 minutes + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_MAX_LIMIT") + ); + } + + /* + * Test that the wait time between two service journeys is less than the maximum limit. + * Maximum Limit is 3 hours + */ + @Test + void testWaitTimeExceedingErrorLimitWithDayOffset_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + .withDepartureDayOffset(1) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 2)) + .withArrivalTime(LocalTime.of(12, 55, 0)) // waiting time 3 hours 2 minutes + ); + + testInterchange.doMock(); + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("WAIT_TIME_IN_INTERCHANGE_EXCEEDS_MAX_LIMIT") + ); + } + + @Test + void testNoSharedActiveDays_datedServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestOperatingDays(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 2)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE") + ); + } + + @Test + void testNoDatedServiceJourneyOrDayTypeExistsForConsumerServiceJourney() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(1) + .withScheduledStopPointId(1) + .addTestDayType(1, LocalDate.of(2024, 11, 1)) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(2) + .withScheduledStopPointId(2) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE") + ); + } + + @Test + void testNoDatedServiceJourneyOrDayTypeExistsForFeederServiceJourney() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(5) + .withScheduledStopPointId(1) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(6) + .withScheduledStopPointId(2) + .addTestOperatingDays(2, LocalDate.of(2024, 11, 1)) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE") + ); + } + + @Test + void testNoDatedServiceJourneyOrDayTypeExistsForBothServiceJourneys() { + TestInterchange testInterchange = new TestInterchange(); + testInterchange + .withFromServiceJourney( + testInterchange + .newTestServiceJourney(5) + .withScheduledStopPointId(1) + .withDepartureTime(LocalTime.of(9, 53, 0)) + ) + .withToServiceJourney( + testInterchange + .newTestServiceJourney(6) + .withScheduledStopPointId(2) + .withArrivalTime(LocalTime.of(9, 56, 0)) + ); + + testInterchange.doMock(); + + ValidationReport validationReport = testInterchange.runTest(); + + assertThat(validationReport.getValidationReportEntries().size(), is(1)); + + assertThat( + validationReport + .getValidationReportEntries() + .stream() + .findFirst() + .map(ValidationReportEntry::getName) + .orElse(null), + is("NO_SHARED_ACTIVE_DATE_FOUND_IN_INTERCHANGE") + ); + } +}