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/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));
+ }
+}
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());
+ }
}