From f2bc5e3bbc0ee7c77413d69a9bd5addd01b3c769 Mon Sep 17 00:00:00 2001 From: teppope Date: Tue, 21 Jul 2026 13:54:14 +0300 Subject: [PATCH 1/2] Add parking AvailabilityCondition validation rules - Narrow SiteFrame 'unexpected' warning to fire only when SiteFrame has no Parking content - Add DefaultParkingValidationTreeFactory with rules for AvailabilityCondition structure inside Parking/validityConditions: missing DayTypeRef, missing Timeband time fields, and invalid time range (EndTime < StartTime) - Register parking validation tree in PublicationDeliveryValidationTreeFactory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DefaultParkingValidationTreeFactory.java | 52 +++++ ...DefaultSiteFrameValidationTreeFactory.java | 4 +- ...licationDeliveryValidationTreeFactory.java | 15 ++ ...faultParkingValidationTreeFactoryTest.java | 190 ++++++++++++++++++ ...ultSiteFrameValidationTreeFactoryTest.java | 43 +++- 5 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactory.java create mode 100644 src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryTest.java diff --git a/src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactory.java b/src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactory.java new file mode 100644 index 00000000..c4ec8c07 --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactory.java @@ -0,0 +1,52 @@ +package org.entur.netex.validation.validator.xpath.tree; + +import org.entur.netex.validation.validator.Severity; +import org.entur.netex.validation.validator.xpath.ValidationTreeFactory; +import org.entur.netex.validation.validator.xpath.rules.ValidateNotExist; + +/** + * Construct a validation tree builder for Parking elements within a SiteFrame. + * Validates the structure of AvailabilityCondition entries on Parking entities, + * including DayTypeRef presence and Timeband time range validity. + */ +public class DefaultParkingValidationTreeFactory implements ValidationTreeFactory { + + public static final String CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE = + "PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE"; + public static final String CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME = + "PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME"; + public static final String CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE = + "PARKING_TIMEBAND_INVALID_TIME_RANGE"; + + @Override + public ValidationTreeBuilder builder() { + return new ValidationTreeBuilder("Parking", "SiteFrame/parkings/Parking") + .withRule( + new ValidateNotExist( + "validityConditions/AvailabilityCondition[not(dayTypes/DayTypeRef)]", + CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE, + "Parking AvailabilityCondition missing DayTypeRef", + "Each AvailabilityCondition on a Parking must reference a DayType via dayTypes/DayTypeRef", + Severity.ERROR + ) + ) + .withRule( + new ValidateNotExist( + "validityConditions/AvailabilityCondition/timebands/Timeband[not(StartTime) or not(EndTime)]", + CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME, + "Parking Timeband missing StartTime or EndTime", + "Each Timeband on a Parking AvailabilityCondition must have both StartTime and EndTime", + Severity.ERROR + ) + ) + .withRule( + new ValidateNotExist( + "validityConditions/AvailabilityCondition/timebands/Timeband[StartTime and EndTime and StartTime != '00:00:00' and EndTime != '00:00:00' and EndTime < StartTime]", + CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE, + "Parking Timeband EndTime before StartTime", + "Timeband EndTime must not be before StartTime (use 00:00:00 to denote midnight/24h)", + Severity.ERROR + ) + ); + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactory.java b/src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactory.java index 2793b0fe..1cf2704e 100644 --- a/src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactory.java +++ b/src/main/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactory.java @@ -14,7 +14,7 @@ public ValidationTreeBuilder builder() { return new ValidationTreeBuilder("Site Frame", "SiteFrame") .withRuleForLineFile( new ValidateNotExist( - ".", + ".[not(parkings/Parking)]", CODE_SITE_FRAME_IN_LINE_FILE, "SiteFrame unexpected SiteFrame in Line file", "Unexpected element SiteFrame. It will be ignored", @@ -23,7 +23,7 @@ public ValidationTreeBuilder builder() { ) .withRuleForCommonFile( new ValidateNotExist( - ".", + ".[not(parkings/Parking)]", CODE_SITE_FRAME_IN_COMMON_FILE, "SiteFrame unexpected SiteFrame in Common file", "Unexpected element SiteFrame. It will be ignored", diff --git a/src/main/java/org/entur/netex/validation/validator/xpath/tree/PublicationDeliveryValidationTreeFactory.java b/src/main/java/org/entur/netex/validation/validator/xpath/tree/PublicationDeliveryValidationTreeFactory.java index 961c168e..9599448c 100644 --- a/src/main/java/org/entur/netex/validation/validator/xpath/tree/PublicationDeliveryValidationTreeFactory.java +++ b/src/main/java/org/entur/netex/validation/validator/xpath/tree/PublicationDeliveryValidationTreeFactory.java @@ -33,6 +33,8 @@ public class PublicationDeliveryValidationTreeFactory implements ValidationTreeF new DefaultVehicleScheduleFrameValidationTreeFactory().builder(); private ValidationTreeBuilder multipleFramesValidationTreeBuilder = new DefaultMultipleFramesValidationTreeFactory().builder(); + private ValidationTreeBuilder parkingValidationTreeBuilder = + new DefaultParkingValidationTreeFactory().builder(); @Override public ValidationTreeBuilder builder() { @@ -79,6 +81,9 @@ public ValidationTreeBuilder builder() { framesInCompositeFrameValidationTree.withSubTreeBuilder(tree); }); + dataObjectsValidationTree.withSubTreeBuilder(parkingValidationTreeBuilder); + framesInCompositeFrameValidationTree.withSubTreeBuilder(parkingValidationTreeBuilder); + return validationTreeBuilder; } @@ -183,4 +188,14 @@ public void setMultipleFramesValidationTreeBuilder( ) { this.multipleFramesValidationTreeBuilder = multipleFramesValidationTreeBuilder; } + + public ValidationTreeBuilder parkingValidationTreeBuilder() { + return parkingValidationTreeBuilder; + } + + public void setParkingValidationTreeBuilder( + ValidationTreeBuilder parkingValidationTreeBuilder + ) { + this.parkingValidationTreeBuilder = parkingValidationTreeBuilder; + } } diff --git a/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryTest.java b/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryTest.java new file mode 100644 index 00000000..f84ea432 --- /dev/null +++ b/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryTest.java @@ -0,0 +1,190 @@ +package org.entur.netex.validation.validator.xpath.tree; + +import static org.entur.netex.validation.validator.xpath.tree.DefaultParkingValidationTreeFactory.CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE; +import static org.entur.netex.validation.validator.xpath.tree.DefaultParkingValidationTreeFactory.CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE; +import static org.entur.netex.validation.validator.xpath.tree.DefaultParkingValidationTreeFactory.CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.entur.netex.validation.test.xpath.support.TestValidationContextBuilder; +import org.entur.netex.validation.validator.ValidationIssue; +import org.entur.netex.validation.validator.xpath.ValidationTree; +import org.entur.netex.validation.validator.xpath.XPathRuleValidationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultParkingValidationTreeFactoryTest { + + private static final String NETEX_FRAGMENT_VALID = + """ + + + + true + + + + + + 08:00:00 + 20:00:00 + + + + + false + + + + + + + """; + + private static final String NETEX_FRAGMENT_MIDNIGHT_OPEN = + """ + + + + true + + + + + + 00:00:00 + 00:00:00 + + + + + + """; + + private static final String NETEX_FRAGMENT_MISSING_DAY_TYPE_REF = + """ + + + + true + + + 08:00:00 + 20:00:00 + + + + + + """; + + private static final String NETEX_FRAGMENT_MISSING_END_TIME = + """ + + + + true + + + + + + 08:00:00 + + + + + + """; + + private static final String NETEX_FRAGMENT_INVALID_TIME_RANGE = + """ + + + + true + + + + + + 20:00:00 + 08:00:00 + + + + + + """; + + private ValidationTree validationTree; + + @BeforeEach + void setUp() { + validationTree = new DefaultParkingValidationTreeFactory().builder().build(); + } + + @Test + void testValidParkingWithOpeningHours() { + XPathRuleValidationContext context = TestValidationContextBuilder + .ofNetexFragment(NETEX_FRAGMENT_VALID) + .build(); + assertTrue(validationTree.validate(context).isEmpty()); + } + + @Test + void testValidParkingWithMidnightOpenHours() { + XPathRuleValidationContext context = TestValidationContextBuilder + .ofNetexFragment(NETEX_FRAGMENT_MIDNIGHT_OPEN) + .build(); + assertTrue( + validationTree.validate(context, CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE).isEmpty() + ); + } + + @Test + void testAvailabilityConditionWithoutDayTypeRef() { + XPathRuleValidationContext context = TestValidationContextBuilder + .ofNetexFragment(NETEX_FRAGMENT_MISSING_DAY_TYPE_REF) + .build(); + List issues = validationTree.validate( + context, + CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE + ); + assertEquals(1, issues.size()); + assertEquals( + CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE, + issues.get(0).rule().code() + ); + } + + @Test + void testTimebandWithoutEndTime() { + XPathRuleValidationContext context = TestValidationContextBuilder + .ofNetexFragment(NETEX_FRAGMENT_MISSING_END_TIME) + .build(); + List issues = validationTree.validate( + context, + CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME + ); + assertEquals(1, issues.size()); + assertEquals( + CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME, + issues.get(0).rule().code() + ); + } + + @Test + void testTimebandWithEndTimeBeforeStartTime() { + XPathRuleValidationContext context = TestValidationContextBuilder + .ofNetexFragment(NETEX_FRAGMENT_INVALID_TIME_RANGE) + .build(); + List issues = validationTree.validate( + context, + CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE + ); + assertEquals(1, issues.size()); + assertEquals(CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE, issues.get(0).rule().code()); + } +} diff --git a/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactoryTest.java b/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactoryTest.java index 977ba2c2..57a76e14 100644 --- a/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactoryTest.java +++ b/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultSiteFrameValidationTreeFactoryTest.java @@ -3,6 +3,7 @@ import static org.entur.netex.validation.validator.xpath.tree.DefaultSiteFrameValidationTreeFactory.CODE_SITE_FRAME_IN_COMMON_FILE; import static org.entur.netex.validation.validator.xpath.tree.DefaultSiteFrameValidationTreeFactory.CODE_SITE_FRAME_IN_LINE_FILE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import org.entur.netex.validation.test.xpath.support.TestValidationContextBuilder; @@ -14,11 +15,22 @@ class DefaultSiteFrameValidationTreeFactoryTest { - private static final String NETEX_FRAGMENT_INVALID = + private static final String NETEX_FRAGMENT_WITHOUT_PARKING = """ """; + private static final String NETEX_FRAGMENT_WITH_PARKING = + """ + + + + Test Parking + + + + """; + private ValidationTree validationTree; @BeforeEach @@ -29,7 +41,7 @@ void setUp() { @Test void testSiteFrameInLineFile() { XPathRuleValidationContext xpathValidationContext = TestValidationContextBuilder - .ofNetexFragment(NETEX_FRAGMENT_INVALID) + .ofNetexFragment(NETEX_FRAGMENT_WITHOUT_PARKING) .build(); List validationIssues = validationTree.validate( xpathValidationContext, @@ -42,7 +54,7 @@ void testSiteFrameInLineFile() { @Test void testSiteFrameInCommonFile() { XPathRuleValidationContext xpathValidationContext = TestValidationContextBuilder - .ofNetexFragment(NETEX_FRAGMENT_INVALID) + .ofNetexFragment(NETEX_FRAGMENT_WITHOUT_PARKING) .withFilename("_common.xml") .build(); List validationIssues = validationTree.validate( @@ -52,4 +64,29 @@ void testSiteFrameInCommonFile() { assertEquals(1, validationIssues.size()); assertEquals(CODE_SITE_FRAME_IN_COMMON_FILE, validationIssues.get(0).rule().code()); } + + @Test + void testSiteFrameWithParkingIsNotUnexpectedInLineFile() { + XPathRuleValidationContext xpathValidationContext = TestValidationContextBuilder + .ofNetexFragment(NETEX_FRAGMENT_WITH_PARKING) + .build(); + List validationIssues = validationTree.validate( + xpathValidationContext, + CODE_SITE_FRAME_IN_LINE_FILE + ); + assertTrue(validationIssues.isEmpty()); + } + + @Test + void testSiteFrameWithParkingIsNotUnexpectedInCommonFile() { + XPathRuleValidationContext xpathValidationContext = TestValidationContextBuilder + .ofNetexFragment(NETEX_FRAGMENT_WITH_PARKING) + .withFilename("_parking.xml") + .build(); + List validationIssues = validationTree.validate( + xpathValidationContext, + CODE_SITE_FRAME_IN_COMMON_FILE + ); + assertTrue(validationIssues.isEmpty()); + } } From 2ab6bdc5b77cfedc85d3656271a0b5cc9516c3a5 Mon Sep 17 00:00:00 2001 From: teppope Date: Wed, 22 Jul 2026 09:02:56 +0300 Subject: [PATCH 2/2] Add integration tests for parking AvailabilityCondition validation rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...gValidationTreeFactoryIntegrationTest.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryIntegrationTest.java diff --git a/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryIntegrationTest.java b/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryIntegrationTest.java new file mode 100644 index 00000000..817a7833 --- /dev/null +++ b/src/test/java/org/entur/netex/validation/validator/xpath/tree/DefaultParkingValidationTreeFactoryIntegrationTest.java @@ -0,0 +1,217 @@ +package org.entur.netex.validation.validator.xpath.tree; + +import static org.entur.netex.validation.validator.xpath.tree.DefaultParkingValidationTreeFactory.CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE; +import static org.entur.netex.validation.validator.xpath.tree.DefaultParkingValidationTreeFactory.CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE; +import static org.entur.netex.validation.validator.xpath.tree.DefaultParkingValidationTreeFactory.CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import org.entur.netex.validation.validator.NetexValidatorsRunner; +import org.entur.netex.validation.validator.ValidationReport; +import org.entur.netex.validation.validator.ValidationReportEntry; +import org.entur.netex.validation.validator.schema.NetexSchemaValidator; +import org.entur.netex.validation.validator.xpath.XPathRuleValidator; +import org.entur.netex.validation.xml.NetexXMLParser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for parking AvailabilityCondition validation rules. + * Runs the full NetexValidatorsRunner pipeline (schema + XPath) against + * minimal parking NeTEx files to verify that rules fire on invalid data + * and are silent on valid data. + */ +class DefaultParkingValidationTreeFactoryIntegrationTest { + + private static final String CODESPACE = "FSR"; + private static final String REPORT_ID = "test-report"; + private static final String FILE_NAME = "FSR_parking.xml"; + + /** + * Minimal parking NeTEx that passes schema validation and satisfies all three + * parking rules. Used as the base for "bad" variants by replacing specific elements. + */ + private static final String VALID_PARKING_NETEX = + """ + + + 2026-01-01T00:00:00 + FSR + + + + + 2026-01-01T00:00:00 + 2030-01-01T00:00:00 + + + + + Test Operator + operator + + + + + + + Weekday + + + Monday Tuesday Wednesday Thursday Friday + + + + + + + + + Test Stop + other + + + + + + + 2026-01-01T00:00:00 + 2030-01-01T00:00:00 + true + + + + + + 08:00:00 + 18:00:00 + + + + + Test Parking + + + parkAndRide + openSpace + 50 + + + + + + """; + + private NetexValidatorsRunner runner; + + @BeforeEach + void setUp() { + // Use a custom entry factory that stores rule.code() in the name field so tests + // can assert against the CODE_* constants rather than fragile rule name strings. + NetexXMLParser netexXMLParser = new NetexXMLParser(Collections.emptySet()); + NetexSchemaValidator netexSchemaValidator = new NetexSchemaValidator(100); + XPathRuleValidator xPathRuleValidator = new XPathRuleValidator( + new PublicationDeliveryValidationTreeFactory() + ); + runner = + NetexValidatorsRunner + .of() + .withNetexXMLParser(netexXMLParser) + .withNetexSchemaValidator(netexSchemaValidator) + .withXPathValidators(List.of(xPathRuleValidator)) + .withValidationReportEntryFactory(issue -> + new ValidationReportEntry( + issue.message(), + issue.rule().code(), + issue.rule().severity(), + issue.dataLocation() + ) + ) + .build(); + } + + @Test + void testValidParkingProducesNoParkingRuleViolations() { + ValidationReport report = validate(VALID_PARKING_NETEX); + List parkingCodes = parkingRuleCodes(report); + assertTrue( + parkingCodes.isEmpty(), + "Expected no parking rule violations but got: " + parkingCodes + ); + } + + @Test + void testAvailabilityConditionWithoutDayTypeRefIsReported() { + // Remove the entire block — AvailabilityCondition without dayTypes/DayTypeRef + String xml = VALID_PARKING_NETEX.replaceAll( + "\\s*[^<]*[^<]*", + "" + ); + ValidationReport report = validate(xml); + assertTrue( + hasCode(report, CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE), + "Expected " + + CODE_PARKING_AVAILABILITY_CONDITION_WITHOUT_DAY_TYPE + + " but got: " + + parkingRuleCodes(report) + ); + } + + @Test + void testTimebandWithoutEndTimeIsReported() { + String xml = VALID_PARKING_NETEX.replace("18:00:00", ""); + ValidationReport report = validate(xml); + assertTrue( + hasCode(report, CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME), + "Expected " + + CODE_PARKING_TIMEBAND_WITHOUT_START_OR_END_TIME + + " but got: " + + parkingRuleCodes(report) + ); + } + + @Test + void testTimebandWithEndTimeBeforeStartTimeIsReported() { + String xml = VALID_PARKING_NETEX + .replace("08:00:00", "18:00:00") + .replace("18:00:00", "08:00:00"); + ValidationReport report = validate(xml); + assertTrue( + hasCode(report, CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE), + "Expected " + + CODE_PARKING_TIMEBAND_INVALID_TIME_RANGE + + " but got: " + + parkingRuleCodes(report) + ); + } + + private ValidationReport validate(String xml) { + return runner.validate( + CODESPACE, + REPORT_ID, + FILE_NAME, + xml.getBytes(StandardCharsets.UTF_8) + ); + } + + private static List parkingRuleCodes(ValidationReport report) { + return report + .getValidationReportEntries() + .stream() + .map(ValidationReportEntry::getName) + .filter(name -> name.startsWith("PARKING_")) + .toList(); + } + + private static boolean hasCode(ValidationReport report, String code) { + return report + .getValidationReportEntries() + .stream() + .anyMatch(e -> e.getName().equals(code)); + } +}