From 145705df98a910f15bae521b5e865b0d0a427e45 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 6 Mar 2026 10:57:18 +0100 Subject: [PATCH 01/15] Upgrade to NeTEx 2.0 schema for 1.x branch (Java EE 8) --- .github/workflows/deploy.yml | 6 +- bindings.xjb | 52 ++++++++---- pom.xml | 29 +++---- .../netex/validation/NeTExValidator.java | 5 +- .../model/AbstractUnmarshalFrameTest.java | 15 +++- .../netex/model/MarshalUnmarshalTest.java | 83 ++++++++++--------- .../rutebanken/netex/model/ToStringTest.java | 4 +- .../model/UnmarshalResourceFrameTest.java | 4 +- .../UnmarshalServiceCalendarFrameTest.java | 2 +- .../model/UnmarshalServiceFrameTest.java | 16 ++-- .../netex/model/UnmarshalSiteFrameTest.java | 14 ++-- .../model/UnmarshalTimetableFrameTest.java | 2 +- 12 files changed, 139 insertions(+), 93 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8cd6fb36..c982b2ef 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,10 +2,10 @@ name: Build and Deploy on: push: branches: - - 1.x + - upgrade-netex-2.0_for_1.x pull_request: branches: - - 1.x + - upgrade-netex-2.0_for_1.x jobs: maven-verify: runs-on: ubuntu-24.04 @@ -52,7 +52,7 @@ jobs: publish-snapshot: name: Publish snapshot to Maven Central - if: github.repository_owner == 'entur' && github.event_name == 'push' && github.ref == 'refs/heads/1.x' + if: github.repository_owner == 'entur' && github.event_name == 'push' && github.ref == 'refs/heads/upgrade-netex-2.0_for_1.x' needs: maven-verify uses: ./.github/workflows/maven-jreleaser-release.yml with: diff --git a/bindings.xjb b/bindings.xjb index 93024ab6..33bcf900 100644 --- a/bindings.xjb +++ b/bindings.xjb @@ -15,14 +15,14 @@ - + + schemaLocation="./src/main/resources/xsd/2.0/netex_framework/netex_genericFramework/netex_organisation_version.xsd"> @@ -31,35 +31,28 @@ + schemaLocation="./src/main/resources/xsd/2.0/netex_part_3/part3_fares/netex_salesOfferPackage_version.xsd"> - - - - - - + schemaLocation="./src/main/resources/xsd/2.0/netex_part_3/part3_salesTransactions/netex_salesContract_version.xsd"> + schemaLocation="./src/main/resources/xsd/2.0/netex_part_3/part3_salesTransactions/netex_retailConsortium_version.xsd"> + schemaLocation="./src/main/resources/xsd/2.0/netex_framework/netex_responsibility/netex_version_support.xsd"> @@ -69,14 +62,43 @@ - + schemaLocation="./src/main/resources/xsd/2.0/netex_framework/netex_genericFramework/netex_zone_version.xsd"> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index b523541b..c1471a9f 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 4.0.0 org.entur netex-java-model - 1.0.16-SNAPSHOT + 1.1.0-SNAPSHOT netex-java-model Generates Java model from NeTEx xsds using jaxb. @@ -67,9 +67,10 @@ 1.8 NeTEx - master - https://github.com/entur/${netexRepoName} - 1.16 + v2.0 + https://github.com/NeTEx-CEN/${netexRepoName} + https://github.com/entur/${netexRepoName} + 2.0 2.3.3 @@ -272,9 +273,9 @@ - ${netexGithubUrl}/archive/${netexBranch}.zip + ${cenNetexGithubUrl}/archive/${netexBranch}.zip src/main/resources/xsd/${netexVersion} - NeTEx-${netexBranch}/xsd/* + NeTEx-${netexVersion}/xsd/* ./bin/netex-download-extract.sh @@ -303,7 +304,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.15.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.15.zip src/main/resources/xsd/1.15 NeTEx-tags-v1.0.15/xsd/* @@ -319,7 +320,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.14.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.14.zip src/main/resources/xsd/1.14 NeTEx-tags-v1.0.14/xsd/* @@ -335,7 +336,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.13.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.13.zip src/main/resources/xsd/1.13 NeTEx-tags-v1.0.13/xsd/* @@ -351,7 +352,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.12.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.12.zip src/main/resources/xsd/1.12 NeTEx-tags-v1.0.12/xsd/* @@ -367,7 +368,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.11.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.11.zip src/main/resources/xsd/1.11 NeTEx-tags-v1.0.11/xsd/* @@ -383,7 +384,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.10-entur.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.10-entur.zip src/main/resources/xsd/1.10 NeTEx-tags-v1.0.10-entur/xsd/* @@ -399,7 +400,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.9.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.9.zip src/main/resources/xsd/1.09 NeTEx-tags-v1.0.9/xsd/* @@ -415,7 +416,7 @@ - ${netexGithubUrl}/archive/tags/v1.0.8.zip + ${enturNetexGithubUrl}/archive/tags/v1.0.8.zip src/main/resources/xsd/1.08 NeTEx-tags-v1.0.8/xsd/* diff --git a/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java b/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java index 4b3f4a83..b95d0529 100644 --- a/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java +++ b/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java @@ -44,7 +44,8 @@ public enum NetexVersion { v1_13 ("1.13"), v1_14 ("1.14"), v1_15 ("1.15"), - v1_16 ("1.16"); + v1_16 ("1.16"), + v2_0 ("2.0"); private final String folderName; @@ -59,7 +60,7 @@ public String toString() { } private final Schema neTExSchema; - public static final NetexVersion LATEST = NetexVersion.v1_16; + public static final NetexVersion LATEST = NetexVersion.v2_0; private static final Map VALIDATORS_PER_VERSION = new EnumMap<>(NetexVersion.class); diff --git a/src/test/java/org/rutebanken/netex/model/AbstractUnmarshalFrameTest.java b/src/test/java/org/rutebanken/netex/model/AbstractUnmarshalFrameTest.java index e3fddbc6..e7bb2ee2 100644 --- a/src/test/java/org/rutebanken/netex/model/AbstractUnmarshalFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/AbstractUnmarshalFrameTest.java @@ -32,6 +32,19 @@ public static void initContext() throws JAXBException { } - + /** + * Helper method to extract string value from MultilingualString. + * In NeTEx 2.0, MultilingualString uses mixed content instead of a simple value. + */ + protected static String getStringValue(MultilingualString multilingualString) { + if (multilingualString == null || multilingualString.getContent() == null) { + return null; + } + return multilingualString.getContent().stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .findFirst() + .orElse(null); + } } diff --git a/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java b/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java index 78b8b7bc..05492a61 100644 --- a/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java +++ b/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java @@ -25,13 +25,15 @@ import javax.xml.bind.Unmarshaller; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamWriter; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.temporal.ChronoField; import java.util.Arrays; import java.util.List; @@ -55,7 +57,7 @@ void publicationDeliveryWithOffsetDateTime() throws JAXBException { Marshaller marshaller = jaxbContext.createMarshaller(); PublicationDeliveryStructure publicationDelivery = new PublicationDeliveryStructure() - .withDescription(new MultilingualString().withValue("value").withLang("no").withTextIdType("")).withPublicationTimestamp(LocalDateTime.now().withNano(0)) + .withDescription(new MultilingualString().withContent("value").withLang("no").withTextIdType("")).withPublicationTimestamp(LocalDateTime.now().withNano(0)) .withParticipantRef("participantRef"); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); @@ -75,7 +77,7 @@ void publicationDeliveryWithOffsetDateTime() throws JAXBException { System.out.println(actual.getPublicationTimestamp()); assertThat(actual.getPublicationTimestamp()).isEqualTo(publicationDelivery.getPublicationTimestamp()); assertThat(actual.getDescription()).isNotNull(); - assertThat(actual.getDescription().getValue()).isEqualTo(publicationDelivery.getDescription().getValue()); + assertThat(actual.getDescription().getContent()).isEqualTo(publicationDelivery.getDescription().getContent()); assertThat(actual.getParticipantRef()).isEqualTo(publicationDelivery.getParticipantRef()); } @@ -134,7 +136,7 @@ void timetableWithVehicleModes() throws JAXBException { Marshaller marshaller = jaxbContext.createMarshaller(); TimetableFrame timetableFrame = factory.createTimetableFrame().withVersion("any").withId("TimetableFrame") - .withName(factory.createMultilingualString().withValue("TimetableFrame")).withVehicleModes(VehicleModeEnumeration.AIR); + .withName(factory.createMultilingualString().withContent("TimetableFrame")).withVehicleModes(AllPublicTransportModesEnumeration.AIR); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); @@ -151,9 +153,9 @@ void timetableWithVehicleModes() throws JAXBException { assertThat(actual.getVersion()).isNotNull().isNotEmpty().isEqualTo(timetableFrame.getVersion()); assertThat(actual.getId()).isNotNull().isNotEmpty().isEqualTo(timetableFrame.getId()); - assertThat(actual.getName().getValue()).isNotNull().isNotEmpty().isEqualTo(timetableFrame.getName().getValue()); + assertThat(actual.getName().getContent()).isNotNull().isNotEmpty().isEqualTo(timetableFrame.getName().getContent()); assertThat(actual.getVehicleModes()).isNotNull().isNotEmpty().hasSize(1); - assertThat(actual.getVehicleModes().get(0)).isNotNull().isEqualTo(VehicleModeEnumeration.AIR); + assertThat(actual.getVehicleModes().get(0)).isNotNull().isEqualTo(AllPublicTransportModesEnumeration.AIR); } @Test @@ -162,11 +164,11 @@ void dayTypeWithPropertiesOfDay() throws JAXBException { List daysOfWeek = Arrays.asList(DayOfWeekEnumeration.MONDAY, DayOfWeekEnumeration.TUESDAY, DayOfWeekEnumeration.WEDNESDAY, DayOfWeekEnumeration.THURSDAY, DayOfWeekEnumeration.FRIDAY); - PropertyOfDay propertyOfDay = factory.createPropertyOfDay().withDescription(factory.createMultilingualString().withValue("PropertyOfDay")) - .withName(factory.createMultilingualString().withValue("PropertyOfDay")).withDaysOfWeek(daysOfWeek); + PropertyOfDay propertyOfDay = factory.createPropertyOfDay().withDescription(factory.createMultilingualString().withContent("PropertyOfDay")) + .withName(factory.createMultilingualString().withContent("PropertyOfDay")).withDaysOfWeek(daysOfWeek); PropertiesOfDay_RelStructure propertiesOfDay = factory.createPropertiesOfDay_RelStructure().withPropertyOfDay(propertyOfDay); DayType dayType = factory.createDayType().withVersion("any").withId(String.format("%s:dt:weekday", "SK4488")) - .withName(factory.createMultilingualString().withValue("Ukedager (mandag til fredag)")).withProperties(propertiesOfDay); + .withName(factory.createMultilingualString().withContent("Ukedager (mandag til fredag)")).withProperties(propertiesOfDay); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); @@ -183,73 +185,80 @@ void dayTypeWithPropertiesOfDay() throws JAXBException { assertThat(actual.getVersion()).isNotNull().isNotEmpty().isEqualTo(dayType.getVersion()); assertThat(actual.getId()).isNotNull().isNotEmpty().isEqualTo(dayType.getId()); - assertThat(actual.getName().getValue()).isNotNull().isNotEmpty().isEqualTo(dayType.getName().getValue()); + assertThat(actual.getName().getContent()).isNotNull().isNotEmpty().isEqualTo(dayType.getName().getContent()); assertThat(actual.getProperties().getPropertyOfDay().get(0).getDaysOfWeek()).isNotNull().isNotEmpty().hasSize(5) .hasOnlyElementsOfType(DayOfWeekEnumeration.class).hasSameElementsAs(daysOfWeek); } @Test - void datedCallWithLocalDate() throws JAXBException { + void marshalledNamespacePrefixes() throws JAXBException { Marshaller marshaller = jaxbContext.createMarshaller(); - DatedCall datedCall = new DatedCall().withArrivalDate(LocalDateTime.now().with(ChronoField.MILLI_OF_DAY, 0)); + PublicationDeliveryStructure publicationDeliveryStructure = new PublicationDeliveryStructure(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - marshaller.marshal(factory.createDatedCall(datedCall), byteArrayOutputStream); + marshaller.marshal(factory.createPublicationDelivery(publicationDeliveryStructure), byteArrayOutputStream); String xml = byteArrayOutputStream.toString(); System.out.println(xml); - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - - JAXBElement actual = (JAXBElement) unmarshaller.unmarshal(new ByteArrayInputStream(xml.getBytes())); - - assertThat(actual.getValue().getArrivalDate()).isEqualTo(datedCall.getArrivalDate()); - + assertThat(xml) + .as("Namespace declaration without prefix for netex").contains("xmlns=\"http://www.netex.org.uk/netex\"") + .as(" actual = (JAXBElement) unmarshaller.unmarshal(new ByteArrayInputStream(xml.getBytes())); + + assertThat(actual.getValue().getFromDate().getHour()).isEqualTo(condition.getFromDate().getHour()); + assertThat(actual.getValue().getFromDate()).isEqualToIgnoringNanos(condition.getFromDate()); } @Test - void datedCallWithLocalDateTime() throws JAXBException { + void availabilityConditionWithCreatedLocalDateTime() throws JAXBException { Marshaller marshaller = jaxbContext.createMarshaller(); - DatedCall datedCall = new DatedCall().withChanged(LocalDateTime.now()); + AvailabilityCondition condition = new AvailabilityCondition() + .withCreated(LocalDateTime.now()) + .withId("test:condition:2") + .withVersion("1"); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - marshaller.marshal(factory.createDatedCall(datedCall), byteArrayOutputStream); + marshaller.marshal(factory.createAvailabilityCondition(condition), byteArrayOutputStream); String xml = byteArrayOutputStream.toString(); System.out.println(xml); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - JAXBElement actual = (JAXBElement) unmarshaller.unmarshal(new ByteArrayInputStream(xml.getBytes())); + @SuppressWarnings("unchecked") + JAXBElement actual = (JAXBElement) unmarshaller.unmarshal(new ByteArrayInputStream(xml.getBytes())); - assertThat(actual.getValue().getChanged().getHour()).isEqualTo(datedCall.getChanged().getHour()); - assertThat(actual.getValue().getChanged()).isEqualToIgnoringNanos(datedCall.getChanged()); + assertThat(actual.getValue().getCreated().getHour()).isEqualTo(condition.getCreated().getHour()); + assertThat(actual.getValue().getCreated()).isEqualToIgnoringNanos(condition.getCreated()); } @Test @@ -264,12 +273,12 @@ void unmarshalPublicationDeliveryAndVerifyValidBetween() throws JAXBException, I CompositeFrame compositeFrame = (CompositeFrame) actual.dataObjects.compositeFrameOrCommonFrame .get(0).getValue(); ValidityConditions_RelStructure validityConditions = compositeFrame.getValidityConditions(); - ValidBetween validBetweenWithTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition_().get(0); + ValidBetween validBetweenWithTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition_Dummy().get(0); assertThat(validBetweenWithTimezone.getFromDate()).isNotNull(); assertThat(validBetweenWithTimezone.getToDate()).isNotNull(); assertThat(validBetweenWithTimezone.getToDate()).hasToString("2017-01-01T11:00"); - ValidBetween validBetweenWithoutTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition_().get(1); + ValidBetween validBetweenWithoutTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition_Dummy().get(1); assertThat(validBetweenWithoutTimezone.getFromDate()).isNotNull(); assertThat(validBetweenWithoutTimezone.getToDate()).isNotNull(); diff --git a/src/test/java/org/rutebanken/netex/model/ToStringTest.java b/src/test/java/org/rutebanken/netex/model/ToStringTest.java index 895f5677..a2e253d8 100644 --- a/src/test/java/org/rutebanken/netex/model/ToStringTest.java +++ b/src/test/java/org/rutebanken/netex/model/ToStringTest.java @@ -23,8 +23,8 @@ class ToStringTest { @Test void toStringMethod() { StopPlace stopPlace = new StopPlace() - .withName(new MultilingualString().withValue("berger")) - .withDescription(new MultilingualString().withValue("description")); + .withName(new MultilingualString().withContent("berger")) + .withDescription(new MultilingualString().withContent("description")); System.out.println(stopPlace.toString()); assertThat(stopPlace.toString()).contains("berger"); assertThat(stopPlace.toString()).contains("description"); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java index 67e2324a..7a1595dc 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java @@ -110,8 +110,8 @@ void unmarshalResourceFrame() throws JAXBException { CompositeFrame compositeFrame = (CompositeFrame) publicationDeliveryStructure.getDataObjects().getCompositeFrameOrCommonFrame().get(0).getValue(); ResourceFrame resourceFrame = (ResourceFrame) compositeFrame.getFrames().getCommonFrame().get(0).getValue(); - Authority authority = (Authority) resourceFrame.getOrganisations().getOrganisation_().get(0).getValue(); - assertEquals("Vy", authority.getName().getValue()); + Authority authority = (Authority) resourceFrame.getOrganisations().getOrganisation_Dummy().get(0).getValue(); + assertEquals("Vy", getStringValue(authority.getName())); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java index b469cff6..06535fa4 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java @@ -128,7 +128,7 @@ void unmarshalServiceCalendarFrame() throws JAXBException { ServiceCalendarFrame serviceCalendarFrame = (ServiceCalendarFrame) compositeFrame.getFrames().getCommonFrame().get(0).getValue(); - DayType dayType = (DayType) serviceCalendarFrame.getDayTypes().getDayType_().get(0).getValue(); + DayType dayType = (DayType) serviceCalendarFrame.getDayTypes().getDayType_Dummy().get(0).getValue(); assertEquals(DayOfWeekEnumeration.TUESDAY, dayType.getProperties().getPropertyOfDay().get(0).getDaysOfWeek().get(0)); OperatingDay operatingDay = serviceCalendarFrame.getOperatingDays().getOperatingDay().get(0); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java index 26c385e7..dc5d6626 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java @@ -146,21 +146,21 @@ void unmarshalServiceFrame() throws JAXBException { CompositeFrame compositeFrame = (CompositeFrame) publicationDeliveryStructure.getDataObjects().getCompositeFrameOrCommonFrame().get(0).getValue(); ServiceFrame serviceFrame = (ServiceFrame) compositeFrame.getFrames().getCommonFrame().get(0).getValue(); - Line line = (Line) serviceFrame.getLines().getLine_().get(0).getValue(); - assertEquals("Narvik-Riksgränsen (Sverige)", line.getName().getValue()); - assertEquals("F8", line.getPublicCode()); + Line line = (Line) serviceFrame.getLines().getLine_Dummy().get(0).getValue(); + assertEquals("Narvik-Riksgränsen (Sverige)", getStringValue(line.getName())); + assertEquals("F8", line.getPublicCode().getValue()); assertEquals("F8", line.getPrivateCode().getValue()); assertEquals("rail", line.getTransportMode().value()); - Route route = (Route) serviceFrame.getRoutes().getRoute_().get(0).getValue(); - assertEquals("Riksgränsen-Narvik Ofotbanen", route.getName().getValue()); - assertEquals("KJ-NK", route.getShortName().getValue()); + Route route = (Route) serviceFrame.getRoutes().getRoute_Dummy().get(0).getValue(); + assertEquals("Riksgränsen-Narvik Ofotbanen", getStringValue(route.getName())); + assertEquals("KJ-NK", getStringValue(route.getShortName())); PointOnRoute pointOnRoute = route.getPointsInSequence().getPointOnRoute().get(0); assertEquals("VYG:RoutePoint:KJ", pointOnRoute.getPointRef().getValue().getRef()); - JourneyPattern journeyPattern = (JourneyPattern) serviceFrame.getJourneyPatterns().getJourneyPattern_OrJourneyPatternView().get(0).getValue(); - assertEquals("KMB-NK", journeyPattern.getName().getValue()); + JourneyPattern journeyPattern = (JourneyPattern) serviceFrame.getJourneyPatterns().getJourneyPattern_Dummy().get(0).getValue(); + assertEquals("KMB-NK", getStringValue(journeyPattern.getName())); assertEquals("VYG:Route:F8-R", journeyPattern.getRouteRef().getRef()); StopPointInJourneyPattern stopPointInJourneyPattern = (StopPointInJourneyPattern) journeyPattern.getPointsInSequence().getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern().get(0); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java index 125f0f9e..2d8c151f 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java @@ -157,19 +157,19 @@ void unmarshalSiteFrame() throws JAXBException { PublicationDeliveryStructure publicationDeliveryStructure = jaxbElement.getValue(); SiteFrame siteFrame = (SiteFrame) publicationDeliveryStructure.getDataObjects().getCompositeFrameOrCommonFrame().get(0).getValue(); - StopPlace stopPlace = (StopPlace) siteFrame.getStopPlaces().getStopPlace_().get(0).getValue(); - assertEquals("Krokstien", stopPlace.getName().getValue()); - assertEquals(AllVehicleModesOfTransportEnumeration.BUS, stopPlace.getTransportMode()); + StopPlace stopPlace = (StopPlace) siteFrame.getStopPlaces().getStopPlace().get(0); + assertEquals("Krokstien", getStringValue(stopPlace.getName())); + assertEquals(AllPublicTransportModesEnumeration.BUS, stopPlace.getTransportMode()); assertEquals(StopTypeEnumeration.ONSTREET_BUS, stopPlace.getStopPlaceType()); - assertEquals("BRA:TariffZone:311", stopPlace.getTariffZones().getTariffZoneRef_().get(0).getValue().getRef()); + assertEquals("BRA:TariffZone:311", stopPlace.getTariffZones().getTariffZoneRef_Dummy().get(0).getValue().getRef()); Quay quay = (Quay) stopPlace.getQuays().getQuayRefOrQuay().get(0).getValue(); assertEquals(BigDecimal.valueOf(59.910579), quay.getCentroid().getLocation().getLatitude()); TariffZone tariffZone = (TariffZone) siteFrame.getTariffZones().getTariffZone().get(0).getValue(); - assertEquals("1", tariffZone.getName().getValue()); + assertEquals("1", getStringValue(tariffZone.getName())); GroupOfStopPlaces groupOfStopPlaces = siteFrame.getGroupsOfStopPlaces().getGroupOfStopPlaces().get(0); - assertEquals("Lillehammer", groupOfStopPlaces.getName().getValue()); + assertEquals("Lillehammer", getStringValue(groupOfStopPlaces.getName())); assertEquals("NSR:StopPlace:420", groupOfStopPlaces.getMembers().getStopPlaceRef().get(0).getValue().getRef()); } @@ -211,7 +211,7 @@ void unmarshalTopographicPlaceWithPolygon() throws JAXBException { SiteFrame siteFrame = (SiteFrame) publicationDeliveryStructure.getDataObjects().getCompositeFrameOrCommonFrame().get(0).getValue(); TopographicPlace topographicPlace = siteFrame.getTopographicPlaces().getTopographicPlace().get(0); - assertEquals("Oslo", topographicPlace.getDescriptor().getName().getValue()); + assertEquals("Oslo", getStringValue(topographicPlace.getDescriptor().getName())); assertEquals(TopographicPlaceTypeEnumeration.MUNICIPALITY, topographicPlace.getTopographicPlaceType()); assertNotNull(topographicPlace.getPolygon()); } diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalTimetableFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalTimetableFrameTest.java index 163fd7a1..4bf92ac7 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalTimetableFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalTimetableFrameTest.java @@ -120,7 +120,7 @@ void unmarshalTimetableFrame() throws JAXBException { TimetableFrame timetableFrame = (TimetableFrame) compositeFrame.getFrames().getCommonFrame().get(0).getValue(); ServiceJourney serviceJourney = (ServiceJourney) timetableFrame.getVehicleJourneys().getVehicleJourneyOrDatedVehicleJourneyOrNormalDatedVehicleJourney().get(0); - assertEquals("96", serviceJourney.getName().getValue()); + assertEquals("96", getStringValue(serviceJourney.getName())); assertEquals("96", serviceJourney.getPrivateCode().getValue()); assertEquals("rail", serviceJourney.getTransportMode().value()); From a967d9e7aca79ff8c4adf8be1c76672b47fde2e6 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:41:54 +0200 Subject: [PATCH 02/15] Download NeTEx XSD from the TransmodelEcosystem repository The NeTEx GitHub repository moved from NeTEx-CEN/NeTEx to TransmodelEcosystem/NeTEx. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c1471a9f..7556ad9e 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ NeTEx v2.0 - https://github.com/NeTEx-CEN/${netexRepoName} + https://github.com/TransmodelEcosystem/${netexRepoName} https://github.com/entur/${netexRepoName} 2.0 From ffd89ece6df1c23b6c9d0d3c33e3ceed82bb5547 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:42:24 +0200 Subject: [PATCH 03/15] Download the 1.16.1 schema for validating 1.16 documents With the model generated from NeTEx 2.0, the 1.16 schema was no longer downloaded, so NetexVersion.v1_16 could not load its schema. The 1.16.1 schema is now downloaded as a legacy validation schema like 1.15 and earlier. A document declaring 1.16 is validated against it: patch releases of the schema are backward compatible and documents only ever declare the minor version. --- pom.xml | 16 +++++++ .../netex/validation/NeTExValidator.java | 22 +++++++-- .../netex/validation/NeTExValidatorTest.java | 45 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 7556ad9e..099fa8d3 100644 --- a/pom.xml +++ b/pom.xml @@ -295,6 +295,22 @@ + + + download-extract-legacy-1.16 + generate-resources + + exec + + + + ${enturNetexGithubUrl}/archive/tags/v1.0.16.1.zip + src/main/resources/xsd/1.16.1 + NeTEx-tags-v1.0.16.1/xsd/* + + ./bin/netex-download-extract.sh + + download-extract-legacy-1.15 diff --git a/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java b/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java index b95d0529..8b1354a7 100644 --- a/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java +++ b/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java @@ -44,18 +44,32 @@ public enum NetexVersion { v1_13 ("1.13"), v1_14 ("1.14"), v1_15 ("1.15"), - v1_16 ("1.16"), + /** + * NeTEx 1.16, validated against the latest 1.16 patch schema. Patch releases of the schema + * are backward compatible, and documents only ever declare the minor version, so a document + * declaring 1.16 is validated against the 1.16.1 schema. + */ + v1_16 ("1.16", "1.16.1"), v2_0 ("2.0"); + private final String version; private final String folderName; - NetexVersion(String folderName) { + NetexVersion(String version) { + this(version, version); + } + + NetexVersion(String version, String folderName) { + this.version = version; this.folderName = folderName; } + /** + * @return the NeTEx version, which is not always the name of the folder holding its schema. + */ @Override public String toString() { - return folderName; + return version; } } private final Schema neTExSchema; @@ -103,7 +117,7 @@ public NeTExValidator() throws IOException, SAXException { public NeTExValidator(NetexVersion version) throws IOException, SAXException { SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - String resourceName = "xsd/"+ version +"/NeTEx_publication.xsd"; + String resourceName = "xsd/"+ version.folderName +"/NeTEx_publication.xsd"; LOGGER.info("Loading resource: {}", resourceName); URL resource = getClass().getClassLoader().getResource(resourceName); if(resource == null) { diff --git a/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java b/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java index 1d9b00c7..f5dc6e89 100644 --- a/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java +++ b/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java @@ -24,6 +24,7 @@ import java.io.StringReader; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; class NeTExValidatorTest { @@ -52,4 +53,48 @@ void validatePublicationDeliveryWithLatestVersion() throws IOException, SAXExcep } + /** + * AssociatedContract / ContractRef on a ResponsibilityRoleAssignment was added in NeTEx 1.16.1 + * (entur/NeTEx#63). 1.16 is validated against the 1.16.1 schema, so it is valid against 1.16 too. + * It is not validated against 2.0 here: 2.0 enforces a key reference from ContractRef to a Contract + * declared in the same document, which 1.16.1 has no place for in a ResourceFrame. + */ + private static final String RESPONSIBILITY_SET_WITH_ASSOCIATED_CONTRACT = "\n" + + "\n" + + " 2026-09-10T10:00:00\n" + + " TST\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Contracted operation\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + @Test + void latestVersionIs2_0() { + assertEquals(NeTExValidator.NetexVersion.v2_0, NeTExValidator.LATEST); + } + + @Test + void version1_16IsNamed1_16() { + assertEquals("1.16", NeTExValidator.NetexVersion.v1_16.toString()); + } + + @Test + void validateResponsibilitySetWithAssociatedContractAgainstVersion1_16() throws IOException, SAXException { + NeTExValidator validator = NeTExValidator.getNeTExValidator(NeTExValidator.NetexVersion.v1_16); + validator.validate(new StreamSource(new StringReader(RESPONSIBILITY_SET_WITH_ASSOCIATED_CONTRACT))); + } + } \ No newline at end of file From 51f5a00fce7325c5865a3b4cd22bf812888818af Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:42:57 +0200 Subject: [PATCH 04/15] Map xsd:date to java.time.LocalDate xsd:date elements (OperatingDay/CalendarDate, DayTypeAssignment/Date, ServiceCalendar/FromDate and ToDate, ...) were previously bound to LocalDateTime at midnight. They are now bound to LocalDate. LocalDateXmlAdapter accepts an optional trailing zone offset when unmarshalling, as permitted by xsd:date, and ignores it. Dates are always marshalled as yyyy-MM-dd and no longer depend on the JVM's default offset. xsd:dateTime elements are still bound to LocalDateTime. --- bindings.xjb | 2 +- .../rutebanken/util/LocalDateXmlAdapter.java | 31 ++++---- .../UnmarshalServiceCalendarFrameTest.java | 5 +- .../util/LocalDateXmlAdapterTest.java | 70 +++++++++++++++++++ 4 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 src/test/java/org/rutebanken/util/LocalDateXmlAdapterTest.java diff --git a/bindings.xjb b/bindings.xjb index 33bcf900..c5b845b6 100644 --- a/bindings.xjb +++ b/bindings.xjb @@ -8,7 +8,7 @@ - + diff --git a/src/main/java/org/rutebanken/util/LocalDateXmlAdapter.java b/src/main/java/org/rutebanken/util/LocalDateXmlAdapter.java index 359c214f..8429131d 100644 --- a/src/main/java/org/rutebanken/util/LocalDateXmlAdapter.java +++ b/src/main/java/org/rutebanken/util/LocalDateXmlAdapter.java @@ -16,30 +16,31 @@ package org.rutebanken.util; import javax.xml.bind.annotation.adapters.XmlAdapter; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; +import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.time.temporal.ChronoField; -public class LocalDateXmlAdapter extends XmlAdapter { - private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd") +/** + * Maps xs:date to {@link LocalDate}. An optional trailing zone offset ({@code 2026-05-03+02:00}, + * {@code 2026-05-03Z}), as permitted by xs:date, is accepted and ignored when unmarshalling. Dates + * are always marshalled as {@code yyyy-MM-dd}. + */ +public class LocalDateXmlAdapter extends XmlAdapter { + + private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE) .optionalStart().appendPattern("XXXXX").optionalEnd() - .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) - .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) - .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0) - .parseDefaulting(ChronoField.OFFSET_SECONDS,OffsetDateTime.now().getLong(ChronoField.OFFSET_SECONDS) ) - .toFormatter(); + .toFormatter(); @Override - public LocalDateTime unmarshal(String inputDate) { - return LocalDateTime.parse(inputDate, formatter); + public LocalDate unmarshal(String inputDate) { + return LocalDate.parse(inputDate, PARSER); } @Override - public String marshal(LocalDateTime inputDate) { - if(inputDate != null) { - return formatter.format(inputDate); + public String marshal(LocalDate inputDate) { + if (inputDate != null) { + return DateTimeFormatter.ISO_LOCAL_DATE.format(inputDate); } else { return null; } diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java index 06535fa4..453c19aa 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java @@ -21,6 +21,7 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.ByteArrayInputStream; +import java.time.LocalDate; import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -132,13 +133,13 @@ void unmarshalServiceCalendarFrame() throws JAXBException { assertEquals(DayOfWeekEnumeration.TUESDAY, dayType.getProperties().getPropertyOfDay().get(0).getDaysOfWeek().get(0)); OperatingDay operatingDay = serviceCalendarFrame.getOperatingDays().getOperatingDay().get(0); - assertEquals(LocalDateTime.of(2023,10,29,0,0), operatingDay.getCalendarDate()); + assertEquals(LocalDate.of(2023, 10, 29), operatingDay.getCalendarDate()); OperatingPeriod operatingPeriod = (OperatingPeriod) serviceCalendarFrame.getOperatingPeriods().getOperatingPeriodOrUicOperatingPeriod().get(0); assertEquals(LocalDateTime.of(2023,8,17,0,0), operatingPeriod.getFromDate()); DayTypeAssignment dayTypeAssignment = serviceCalendarFrame.getDayTypeAssignments().getDayTypeAssignment().get(0); - assertEquals(LocalDateTime.of(2024,5,1,0,0), dayTypeAssignment.getDate()); + assertEquals(LocalDate.of(2024, 5, 1), dayTypeAssignment.getDate()); } diff --git a/src/test/java/org/rutebanken/util/LocalDateXmlAdapterTest.java b/src/test/java/org/rutebanken/util/LocalDateXmlAdapterTest.java new file mode 100644 index 00000000..6d7a37db --- /dev/null +++ b/src/test/java/org/rutebanken/util/LocalDateXmlAdapterTest.java @@ -0,0 +1,70 @@ +/* + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ + +package org.rutebanken.util; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class LocalDateXmlAdapterTest { + + private final LocalDateXmlAdapter adapter = new LocalDateXmlAdapter(); + + @Test + void testUnmarshalBasicDate() { + LocalDate result = adapter.unmarshal("2026-08-31"); + assertEquals(LocalDate.of(2026, 8, 31), result); + } + + @Test + void testUnmarshalDateWithOffset() { + LocalDate result = adapter.unmarshal("2026-05-03+02:00"); + assertEquals(LocalDate.of(2026, 5, 3), result); + } + + @Test + void testUnmarshalDateWithUtcDesignator() { + LocalDate result = adapter.unmarshal("2026-05-03Z"); + assertEquals(LocalDate.of(2026, 5, 3), result); + } + + @Test + void testUnmarshalDateTimeIsRejected() { + assertThrows(DateTimeParseException.class, () -> adapter.unmarshal("2026-05-03T00:00:00")); + } + + @Test + void testMarshalBasicDate() { + String result = adapter.marshal(LocalDate.of(2026, 8, 31)); + assertEquals("2026-08-31", result); + } + + @Test + void testMarshalNull() { + assertNull(adapter.marshal(null)); + } + + @Test + void testRoundTrip() { + LocalDate original = LocalDate.of(2026, 5, 3); + assertEquals(original, adapter.unmarshal(adapter.marshal(original))); + } +} From 0c76e44ea2cbe62085bcdb4c393b60db4620e8cf Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:43:14 +0200 Subject: [PATCH 05/15] Remove the dead offset default from the date-time and time adapters parseDefaulting(OFFSET_SECONDS, ) had no effect: LocalDateTime.from and LocalTime.from never read the offset, so an offset in the input is ignored whether it is present, absent or defaulted. Parsing and formatting results are unchanged. The adapters now document that behaviour, as LocalDateXmlAdapter already does. --- .../util/LocalDateTimeISO8601XmlAdapter.java | 12 ++++++------ .../rutebanken/util/LocalTimeISO8601XmlAdapter.java | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/rutebanken/util/LocalDateTimeISO8601XmlAdapter.java b/src/main/java/org/rutebanken/util/LocalDateTimeISO8601XmlAdapter.java index 36fbc05b..e2c067dd 100644 --- a/src/main/java/org/rutebanken/util/LocalDateTimeISO8601XmlAdapter.java +++ b/src/main/java/org/rutebanken/util/LocalDateTimeISO8601XmlAdapter.java @@ -17,20 +17,20 @@ import javax.xml.bind.annotation.adapters.XmlAdapter; import java.time.LocalDateTime; -import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; +/** + * Maps xs:dateTime to {@link LocalDateTime}. An optional trailing zone offset or UTC designator is accepted + * and ignored when unmarshalling, so the local date-time is taken as written in the document. + */ public class LocalDateTimeISO8601XmlAdapter extends XmlAdapter { private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd'T'HH:mm:ss") .optionalStart().appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true).optionalEnd() - .optionalStart().appendPattern("XXXXX") - .optionalEnd() - -// - .parseDefaulting(ChronoField.OFFSET_SECONDS,OffsetDateTime.now().getLong(ChronoField.OFFSET_SECONDS) ).toFormatter(); + .optionalStart().appendPattern("XXXXX").optionalEnd() + .toFormatter(); @Override public LocalDateTime unmarshal(String inputDate) { diff --git a/src/main/java/org/rutebanken/util/LocalTimeISO8601XmlAdapter.java b/src/main/java/org/rutebanken/util/LocalTimeISO8601XmlAdapter.java index 35372428..fdf3780e 100644 --- a/src/main/java/org/rutebanken/util/LocalTimeISO8601XmlAdapter.java +++ b/src/main/java/org/rutebanken/util/LocalTimeISO8601XmlAdapter.java @@ -17,20 +17,20 @@ import javax.xml.bind.annotation.adapters.XmlAdapter; import java.time.LocalTime; -import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; +/** + * Maps xs:time to {@link LocalTime}. An optional trailing zone offset or UTC designator is accepted + * and ignored when unmarshalling, so the local time is taken as written in the document. + */ public class LocalTimeISO8601XmlAdapter extends XmlAdapter { private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("HH:mm:ss") .optionalStart().appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true).optionalEnd() - .optionalStart().appendPattern("XXXXX") - .optionalEnd() - -// - .parseDefaulting(ChronoField.OFFSET_SECONDS,OffsetDateTime.now().getLong(ChronoField.OFFSET_SECONDS) ).toFormatter(); + .optionalStart().appendPattern("XXXXX").optionalEnd() + .toFormatter(); @Override public LocalTime unmarshal(String inputDate) { From d5b8f5efd58f81231e48dac5069e9a6fbfe0d6c3 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:43:19 +0200 Subject: [PATCH 06/15] Make the NeTExValidator constructors private Instances are cached per version and obtained through the static getNeTExValidator methods, which the constructor Javadoc already asked callers to use. PublicationDeliveryClient now uses the cached validator. --- .../client/PublicationDeliveryClient.java | 2 +- .../netex/validation/NeTExValidator.java | 12 +++--------- .../netex/validation/NeTExValidatorTest.java | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/rutebanken/netex/client/PublicationDeliveryClient.java b/src/main/java/org/rutebanken/netex/client/PublicationDeliveryClient.java index a06ff764..0b515255 100644 --- a/src/main/java/org/rutebanken/netex/client/PublicationDeliveryClient.java +++ b/src/main/java/org/rutebanken/netex/client/PublicationDeliveryClient.java @@ -51,7 +51,7 @@ public PublicationDeliveryClient(String publicationDeliveryUrl, boolean validate this.jaxbContext = JAXBContext.newInstance(PublicationDeliveryStructure.class); this.validateAgainstSchema = validateAgainstSchema; if(validateAgainstSchema) { - neTExValidator = new NeTExValidator(); + neTExValidator = NeTExValidator.getNeTExValidator(); } } diff --git a/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java b/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java index 8b1354a7..8139f2ee 100644 --- a/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java +++ b/src/main/java/org/rutebanken/netex/validation/NeTExValidator.java @@ -105,16 +105,10 @@ public static NeTExValidator getNeTExValidator() throws IOException, SAXExceptio /** - * Use static getNeTExValidator to avoid initializing more schemas than needed. + * Validators are cached per version; use {@link #getNeTExValidator(NetexVersion)} or + * {@link #getNeTExValidator()}. */ - public NeTExValidator() throws IOException, SAXException { - this(LATEST); - } - - /** - * Use static getNeTExValidator to avoid initializing more schemas than needed. - */ - public NeTExValidator(NetexVersion version) throws IOException, SAXException { + private NeTExValidator(NetexVersion version) throws IOException, SAXException { SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); String resourceName = "xsd/"+ version.folderName +"/NeTEx_publication.xsd"; diff --git a/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java b/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java index f5dc6e89..a7498ba4 100644 --- a/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java +++ b/src/test/java/org/rutebanken/netex/validation/NeTExValidatorTest.java @@ -15,6 +15,7 @@ package org.rutebanken.netex.validation; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; @@ -28,17 +29,19 @@ class NeTExValidatorTest { - private final NeTExValidator neTExValidator = NeTExValidator.getNeTExValidator(); - - public NeTExValidatorTest() throws IOException, SAXException { } - - public static final String xml = "\n" + + private static final String MINIMAL_PUBLICATION_DELIVERY = "\n" + "\n" + " 2016-11-29T13:32:06.869+01:00\n" + " NSR\n" + ""; - + private NeTExValidator neTExValidator; + + @BeforeEach + void setUp() throws IOException, SAXException { + neTExValidator = NeTExValidator.getNeTExValidator(); + } + @Test void validationFailsForInvalidXml() { String xml = "\n" + @@ -49,8 +52,7 @@ void validationFailsForInvalidXml() { @Test void validatePublicationDeliveryWithLatestVersion() throws IOException, SAXException { - neTExValidator.validate(new StreamSource(new StringReader(xml))); - + neTExValidator.validate(new StreamSource(new StringReader(MINIMAL_PUBLICATION_DELIVERY))); } /** From 78d26d739bd32f04bc102692115cfea7e9e81f94 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:43:19 +0200 Subject: [PATCH 07/15] Make OmitNullsToStringStyle final with a private constructor The style is only meant to be used through INSTANCE. This also removes the javac this-escape warning about the overridable setters called from the constructor. --- .../java/org/rutebanken/netex/OmitNullsToStringStyle.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/rutebanken/netex/OmitNullsToStringStyle.java b/src/main/java/org/rutebanken/netex/OmitNullsToStringStyle.java index 6148a1e7..33047352 100644 --- a/src/main/java/org/rutebanken/netex/OmitNullsToStringStyle.java +++ b/src/main/java/org/rutebanken/netex/OmitNullsToStringStyle.java @@ -17,15 +17,12 @@ import org.apache.commons.lang3.builder.ToStringStyle; -public class OmitNullsToStringStyle extends org.apache.commons.lang3.builder.ToStringStyle { +public final class OmitNullsToStringStyle extends ToStringStyle { - /** - * - */ private static final long serialVersionUID = 7043613680114711384L; public static final ToStringStyle INSTANCE = new OmitNullsToStringStyle(); - public OmitNullsToStringStyle() { + private OmitNullsToStringStyle() { this.setUseClassName(false); this.setUseIdentityHashCode(false); this.setUseFieldNames(true); From 44cf152d11437f0a09d252b23ee8c5e7cbcbbe76 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:43:36 +0200 Subject: [PATCH 08/15] Drop the cxf-xjc-runtime dependency The generated code references nothing under org.apache.cxf: the toString plugin uses OmitNullsToStringStyle from this library. The runtime-scoped jar was only leaked to consumers. --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index 099fa8d3..a8a17e3f 100644 --- a/pom.xml +++ b/pom.xml @@ -140,12 +140,6 @@ ${glassfish-jaxb.version} runtime - - org.apache.cxf.xjc-utils - cxf-xjc-runtime - ${cxf-xjc.version} - runtime - From 586ac0a658e1c4b8c77d2b868369653b83bac500 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:43:47 +0200 Subject: [PATCH 09/15] Remove pre-JReleaser relics from the pom Releases and snapshots are published by JReleaser through the GitHub workflows. The jfrog snapshot repository, the wagon-ssh-external extension, the maven-release-plugin configuration, the sign and release profiles (GPG signing and the decommissioned oss.sonatype.org staging) and the unused nexus-staging, gpg, release and dependency plugin version properties are left over from the previous release process. --- pom.xml | 142 -------------------------------------------------------- 1 file changed, 142 deletions(-) diff --git a/pom.xml b/pom.xml index a8a17e3f..a7ad5cf4 100644 --- a/pom.xml +++ b/pom.xml @@ -53,13 +53,6 @@ https://github.com/entur/netex-java-model/tree/master HEAD - - - snapshots - entur2-snapshots - https://entur2.jfrog.io/entur2/libs-snapshot-local - - @@ -91,14 +84,10 @@ 5.9.1 - 1.6.13 1.6.0 2.22.2 3.2.1 3.3.1 - 2.5.3 - 1.6 - 3.1.2 3.8.1 3.2.0 @@ -115,10 +104,6 @@ - - gpg - ${env.GPG_KEY_NAME} - @@ -181,15 +166,6 @@ - - - - org.apache.maven.wagon - wagon-ssh-external - 2.10 - - @@ -560,24 +536,6 @@ - - org.apache.maven.plugins - maven-release-plugin - ${maven-release-plugin.version} - - true - false - release,sign,!noPublicDeploy - deploy - - - - org.apache.maven.scm - maven-scm-provider-gitexe - 1.9.2 - - - org.apache.maven.plugins maven-enforcer-plugin @@ -644,106 +602,6 @@ - - sign - - false - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - false - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - - org.apache.maven.plugins - maven-gpg-plugin - ${maven-gpg-plugin.version} - - - sign-artifacts - verify - - sign - - - - - - org.apache.maven.plugins - maven-source-plugin - ${maven-source-plugin.version} - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - none - - - - attach-javadocs - - jar - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} - true - - ossrh - https://oss.sonatype.org/ - - false - - - - - - - - release - - false - - - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - sonar From 861d8a7bf8184b10717a51a2fb74bf7ee2c3097b Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:43:52 +0200 Subject: [PATCH 10/15] Delete the downloaded XSD directory on mvn clean The build only removes the schema folders it is about to download again, so folders from earlier builds (1.16) ended up in locally built jars. --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index a7ad5cf4..6e2c7222 100644 --- a/pom.xml +++ b/pom.xml @@ -175,6 +175,14 @@ maven-clean-plugin ${maven-clean-plugin.version} + + + + + src/main/resources/xsd + + + maven-install-plugin From 16a3dce62d05cccb46bd76840061388ce08a519e Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:43:52 +0200 Subject: [PATCH 11/15] Drop release-plugin and shade-plugin entries from .gitignore maven-release-plugin, buildnumber-maven-plugin and maven-shade-plugin are not used, so their backup and output files never appear. --- .gitignore | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.gitignore b/.gitignore index ddac564d..0d150fa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,5 @@ target/ -pom.xml.tag -pom.xml.releaseBackup pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties profile .idea *.iml From 9405169427888f3bb9620a52ea78df47f1772427 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:44:32 +0200 Subject: [PATCH 12/15] Add a manual trigger to the build and build the fallback release with Java 8 deploy.yml can be triggered manually (workflow_dispatch). The manual release workflow still set up Java 11, while every other 1.x workflow builds and publishes with Java 8. --- .github/workflows/deploy.yml | 1 + .github/workflows/release-manual.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c982b2ef..8d83d01a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,6 @@ name: Build and Deploy on: + workflow_dispatch: push: branches: - upgrade-netex-2.0_for_1.x diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml index b88ea872..d9cae1de 100644 --- a/.github/workflows/release-manual.yml +++ b/.github/workflows/release-manual.yml @@ -37,8 +37,8 @@ jobs: - uses: actions/setup-java@v4 with: - java-version: 11 - distribution: liberica + java-version: 8 + distribution: temurin cache: maven - name: Install xmlstarlet From b9ae8346ec5cb27eddf98a01fe1e3316512e26c6 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:44:32 +0200 Subject: [PATCH 13/15] Update README and CLAUDE.md for NeTEx 2.0 on the 1.x line State the NeTEx 2.0 schema and its source, the legacy validation range, the LocalDate mapping, the Java 8 / Java EE 8 baseline and the GML and SIRI packages. The README gains the usage, build and structure sections of the master README, with the 1.x coordinates. --- CLAUDE.md | 16 ++++---- README.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6c729cdc..0141adcd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,12 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -**netex-java-model** is a Java library that generates JAXB model classes from NeTEx (Network Timetable Exchange) XML Schema Definition (XSD) files. The XSD files are downloaded from https://github.com/entur/NeTEx and compiled into Java classes at build time. +**netex-java-model** (Maven coordinates `org.entur:netex-java-model`, 1.x line) is a Java library that generates JAXB model classes from NeTEx (Network Timetable Exchange) XML Schema Definition (XSD) files. The NeTEx 2.0 XSD is downloaded from https://github.com/TransmodelEcosystem/NeTEx and compiled into Java classes at build time. The legacy schemas used only for validation (1.04beta - 1.16.1) are downloaded from the Entur fork at https://github.com/entur/NeTEx. + +The 1.x branch targets Java 8 and Java EE 8 (`javax.xml.bind`, JAXB 2.3); the Jakarta EE line of the library lives on `master`. - **Organization**: Entur AS (Norwegian public transport data provider) - **License**: EUPL-1.2 -- **Java Version**: 8 (builds on 8 in CI) -- **Package**: `org.rutebanken.netex.model` (generated), `org.rutebanken.netex` and `org.rutebanken.util` (source) +- **Java Version**: 8 (builds and publishes on 8 in CI) +- **Package**: `org.rutebanken.netex.model` (generated), `org.rutebanken.netex`, `org.rutebanken.netex.client`, `org.rutebanken.netex.validation` and `org.rutebanken.util` (source). GML and SIRI types are generated in `net.opengis.gml._3` and `uk.org.siri.siri`. ## Build Commands @@ -48,20 +50,20 @@ mvn validate ## Key Technical Details - **JAXB Bindings**: The `bindings.xjb` file customizes code generation, including: - - Java 8 time type adapters (`LocalDateTime`, `LocalTime`, `Duration`) + - Java 8 time type adapters (`LocalDateTime`, `LocalDate`, `LocalTime`, `Duration`) - Property name conflict resolution - Package naming (`org.rutebanken.netex.model`) - **Generated Code**: Model classes are generated in `target/generated-sources/` during build. Do not manually edit generated code. -- **NeTEx Version**: Configured in `pom.xml` via `` property (currently 1.16). +- **NeTEx Version**: Configured in `pom.xml` via `` property (currently 2.0). -- **Validation**: `NeTExValidator` class provides XML schema validation against NeTEx XSD. +- **Validation**: `NeTExValidator` class provides XML schema validation against NeTEx XSD, for versions 1.04beta through 2.0. Documents declaring 1.16 are validated against the 1.16.1 schema. ## CI/CD GitHub Actions workflows in `.github/workflows/`: -- `deploy.yml`: Main build, test, Sonar scan, and snapshot publishing (triggers on `1.x` branch) +- `deploy.yml`: Main build, test, and snapshot publishing (temporarily triggered from the `upgrade-netex-2.0_for_1.x` branch until the merge into `1.x`) - Release workflows: GitFlow-based release management - Publishes to Maven Central via JReleaser diff --git a/README.md b/README.md index f2a7693c..db691a49 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,118 @@ -# netex-java-model [![CircleCI](https://circleci.com/gh/entur/netex-java-model/tree/master.svg?style=svg)](https://circleci.com/gh/entur/netex-java-model/tree/master) +# netex-java-model (1.x) -Downloads the NeTEx-XML XSD files from https://github.com/entur/NeTEx and generates java model using jaxb. +[![Build and push](https://github.com/entur/netex-java-model/actions/workflows/deploy.yml/badge.svg?branch=1.x)](https://github.com/entur/netex-java-model/actions/workflows/deploy.yml) +[![Maven Central](https://img.shields.io/maven-central/v/org.entur/netex-java-model.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/org.entur/netex-java-model) +[![Maven Snapshots](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fcentral.sonatype.com%2Frepository%2Fmaven-snapshots%2Forg%2Fentur%2Fnetex-java-model%2Fmaven-metadata.xml&label=Snapshot)](https://central.sonatype.com/repository/maven-snapshots) +[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](https://joinup.ec.europa.eu/software/page/eupl) +[![Java 8+](https://img.shields.io/badge/Java-8%2B-orange)](https://adoptium.net/) -You need xmlstarlet installed. +Java model classes generated from [NeTEx](https://github.com/TransmodelEcosystem/NeTEx) (Network Timetable Exchange) XML Schema Definition (XSD) files using JAXB. Maintained by [Entur AS](https://www.entur.org). -```mvn clean install``` +This is the **1.x line**, which targets Java 8 and Java EE 8 (`javax.xml.bind`). The `master` branch holds the Jakarta EE line of the library. +## Overview -## Maven central -This project is available in the central maven repository. -See https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.entur%22 +This library downloads the NeTEx XSD files from the [NeTEx repository](https://github.com/TransmodelEcosystem/NeTEx) and compiles them into JAXB-annotated Java model classes at build time. The generated classes support full marshalling and unmarshalling of NeTEx XML documents, including schema validation against multiple NeTEx versions. The legacy schemas used only for validation (1.04beta - 1.16.1) are downloaded from the [Entur NeTEx fork](https://github.com/entur/NeTEx). -# Related project +- **NeTEx version**: 2.0 (model generation), with legacy validation support back to 1.04beta +- **JAXB**: Java XML Bind 2.3 (Java EE 8, `javax.xml.bind`) +- **Generated package**: `org.rutebanken.netex.model` +- **GML types package**: `net.opengis.gml._3` +- **SIRI types package**: `uk.org.siri.siri` -See https://github.com/entur/netex-protobuf for a conversion of NeTEx xsd files to protobuf descriptors. +## Usage + +### Release (Maven Central) + +```xml + + org.entur + netex-java-model + LATEST + +``` + +See [Maven Central](https://central.sonatype.com/artifact/org.entur/netex-java-model) for the latest released version. + +### Snapshot + +Snapshots are published to the Sonatype snapshot repository. Add the repository to your `pom.xml`: + +```xml + + + sonatype-snapshots + https://central.sonatype.com/repository/maven-snapshots + + true + + + +``` + +Then add the dependency with the snapshot version: + +```xml + + org.entur + netex-java-model + LATEST-SNAPSHOT + +``` + +## Building from Source + +### Prerequisites + +- Java 8+ +- Maven 3.x +- [`xmlstarlet`](https://xmlstar.sourceforge.net/) (`apt install xmlstarlet` on Debian/Ubuntu, `brew install xmlstarlet` on macOS) + +### Build + +```bash +# Full build: downloads XSD files, generates JAXB classes, compiles, runs tests +mvn clean install + +# Run tests only +mvn test + +# Package without running tests +mvn package -DskipTests +``` + +The XSD files are downloaded from GitHub during the `generate-sources` phase and placed under `src/main/resources/xsd/`. The JAXB model is generated into `target/generated-sources/xjc/`. + +## Project Structure + +``` +├── bin/ +│ ├── netex-download-extract.sh # Downloads NeTEx XSD from GitHub +│ ├── annotation-replacer.sh # Post-processes generated Java files +│ └── version_updater.sh # Updates version references in bindings +├── bindings.xjb # JAXB customization bindings +├── src/main/java/org/rutebanken/ +│ ├── netex/ # NeTEx utilities (validation, client, toString style) +│ └── util/ # XML adapters for Java time types +├── src/main/resources/xsd/ # Downloaded NeTEx XSD files (build artifact) +└── src/test/ # JUnit 5 marshalling/unmarshalling tests +``` + +## Key Technical Details + +**JAXB customizations** (`bindings.xjb`): +- Java time type adapters: `LocalDateTime`, `LocalDate` and `LocalTime` via adapters in `org.rutebanken.util`, `Duration` via `jaxb-java-time-adapters` +- Fluent builder API (`withX()` methods) via `jaxb-fluent-api` +- Property name conflict resolution +- Package mapping to `org.rutebanken.netex.model` + +**Schema validation**: `NeTExValidator` supports validation against NeTEx versions 1.04beta through 2.0. A version is validated against its latest patch schema: documents declaring 1.16 are validated against the 1.16.1 schema, which is backward compatible with 1.16. + +## License + +Licensed under the [European Union Public Licence (EUPL) version 1.2](https://joinup.ec.europa.eu/software/page/eupl). + +## Related Projects + +- [netex-protobuf](https://github.com/entur/netex-protobuf) — NeTEx XSD files converted to protobuf descriptors +- [NeTEx (Entur fork)](https://github.com/entur/NeTEx) — legacy NeTEx XSD files used by this library for validation From 65a3f23c8b861dede29345a9b020f5537716ea50 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Tue, 13 Oct 2026 15:49:09 +0200 Subject: [PATCH 14/15] Pin CXF XJC to 3.x and JUnit to 5.x for Renovate CXF XJC 4.x generates Jakarta XML Binding code and JUnit 6 requires Java 17; the 1.x line targets Java 8 and Java EE 8. --- renovate.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/renovate.json b/renovate.json index f45d8f11..11f1b6d7 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,21 @@ { "extends": [ "config:base" + ], + "packageRules": [ + { + "description": "Pin CXF XJC to 3.x - 4.x generates Jakarta XML Binding (jakarta.xml.bind) code, this branch targets Java EE 8 (javax.xml.bind)", + "allowedVersions": "<4.0.0", + "matchPackageNames": [ + "/^org\\.apache\\.cxf/" + ] + }, + { + "description": "Pin JUnit to 5.x - JUnit 6 requires Java 17, this branch targets Java 8", + "allowedVersions": "<6.0.0", + "matchPackageNames": [ + "/^org\\.junit/" + ] + } ] } From be4eb764576f85fc70ecc118864f4e9768ecb3ed Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Thu, 15 Oct 2026 10:10:36 +0200 Subject: [PATCH 15/15] Hide the _Dummy suffix of the NeTEx 2.0 substitution-group heads NeTEx 2.0 names its abstract substitution-group heads with a _Dummy suffix (DayType_Dummy is the head of DayType), where 1.x used a trailing underscore. xjc leaked the suffix into the generated API as getDayType_Dummy(), getDayTypeRefOrDayType_Dummy(), createDayType_Dummy() and classes such as MediumAccessDevice_Dummy. Add bin/generate-dummy-bindings.py, which derives one JAXB binding per leak from the schema and writes them into a marked block of bindings.xjb: properties drop the suffix (getDayType(), getDayTypeRefOrDayType()), while factory methods and the few classes generated for heads keep a trailing underscore as in netex-java-model 1.0.x (createDayType_(), MediumAccessDevice_), since dropping it there collides with the factory method of the concrete element. The hand-written TariffZone binding is covered by the generated block. Tests and docs are updated accordingly. --- CLAUDE.md | 1 + README.md | 2 + bin/generate-dummy-bindings.py | 204 ++++++ bindings.xjb | 655 +++++++++++++++++- .../netex/model/MarshalUnmarshalTest.java | 4 +- .../model/UnmarshalResourceFrameTest.java | 2 +- .../UnmarshalServiceCalendarFrameTest.java | 2 +- .../model/UnmarshalServiceFrameTest.java | 6 +- .../netex/model/UnmarshalSiteFrameTest.java | 2 +- 9 files changed, 863 insertions(+), 15 deletions(-) create mode 100755 bin/generate-dummy-bindings.py diff --git a/CLAUDE.md b/CLAUDE.md index 0141adcd..dca8ff67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,7 @@ mvn validate - Java 8 time type adapters (`LocalDateTime`, `LocalDate`, `LocalTime`, `Duration`) - Property name conflict resolution - Package naming (`org.rutebanken.netex.model`) + - A generated block (between `BEGIN/END generated _Dummy bindings` markers) that hides the `_Dummy` suffix of the abstract substitution-group heads: `getDayType()` instead of `getDayType_Dummy()`, `createDayType_(...)` in `ObjectFactory`. Do not edit the block by hand; rerun `bin/generate-dummy-bindings.py src/main/resources/xsd//NeTEx_publication.xsd bindings.xjb` after a schema upgrade. - **Generated Code**: Model classes are generated in `target/generated-sources/` during build. Do not manually edit generated code. diff --git a/README.md b/README.md index db691a49..671c395b 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ The XSD files are downloaded from GitHub during the `generate-sources` phase and ├── bin/ │ ├── netex-download-extract.sh # Downloads NeTEx XSD from GitHub │ ├── annotation-replacer.sh # Post-processes generated Java files +│ ├── generate-dummy-bindings.py # Regenerates the bindings hiding the _Dummy suffix │ └── version_updater.sh # Updates version references in bindings ├── bindings.xjb # JAXB customization bindings ├── src/main/java/org/rutebanken/ @@ -105,6 +106,7 @@ The XSD files are downloaded from GitHub during the `generate-sources` phase and - Fluent builder API (`withX()` methods) via `jaxb-fluent-api` - Property name conflict resolution - Package mapping to `org.rutebanken.netex.model` +- Hiding of the `_Dummy` suffix that NeTEx 2.0 puts on abstract substitution-group heads (`DayType_Dummy` is the head of `DayType`): containers expose `getDayType()` instead of `getDayType_Dummy()`, `ObjectFactory` exposes `createDayType_(...)` and the few classes generated for such heads keep a trailing underscore, as in `netex-java-model` 1.0.x. These bindings are generated by `bin/generate-dummy-bindings.py` and must be regenerated when the schema is upgraded. **Schema validation**: `NeTExValidator` supports validation against NeTEx versions 1.04beta through 2.0. A version is validated against its latest patch schema: documents declaring 1.16 are validated against the 1.16.1 schema, which is backward compatible with 1.16. diff --git a/bin/generate-dummy-bindings.py b/bin/generate-dummy-bindings.py new file mode 100755 index 00000000..89f84a0a --- /dev/null +++ b/bin/generate-dummy-bindings.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +Generate the JAXB bindings that hide the NeTEx "_Dummy" naming convention. + +NeTEx 2.0 declares the abstract substitution-group heads with a "_Dummy" suffix +(for example DayType_Dummy, head of DayType). Without customisation, xjc leaks the +suffix into the generated API: getDayType_Dummy(), getDayTypeRefOrDayType_Dummy(), +createDayType_Dummy(...) and classes such as MediumAccessDevice_Dummy. + +This script walks the schema files and emits one binding per leak: + + * a property rename for every element referencing a _Dummy head + (getDayType_Dummy() -> getDayType()); when the reference sits in a repeated + model group, the group itself is renamed (getDayTypeRefOrDayType()) + * a factory-method rename for every _Dummy element declaration + (createDayType_Dummy(...) -> createDayType_(...)); dropping the suffix entirely + would collide with the factory method of the concrete element + * a class rename for the _Dummy elements with an inline type and for the + _Dummy complex types (MediumAccessDevice_Dummy -> MediumAccessDevice_) + +The generated block is written into the bindings file between the BEGIN/END +markers below (and appended before the closing tag if the markers are absent). +Re-run it after upgrading the NeTEx schema: + + bin/generate-dummy-bindings.py src/main/resources/xsd/2.0/NeTEx_publication.xsd bindings.xjb + +Only the schema files reachable from the entry point through xsd:include and +xsd:import are visited; xjc rejects bindings for files outside the compilation. +""" + +import pathlib +import sys +import xml.etree.ElementTree as ET + +XS = "http://www.w3.org/2001/XMLSchema" +SUFFIX = "_Dummy" +BEGIN = "" +END = "" +PARTICLES = {"element", "group", "choice", "sequence", "all", "any"} +MODEL_GROUPS = {"choice", "sequence", "all"} + + +def tag(node): + return node.tag.split("}")[1] + + +def is_particle(node): + return tag(node) in PARTICLES + + +def repeated(node): + max_occurs = node.get("maxOccurs", "1") + return max_occurs == "unbounded" or int(max_occurs) > 1 + + +def lower_first(name): + return name[0].lower() + name[1:] + + +def label(node): + """Mirror xjc's ParticleBinder.computeLabel for the default property name.""" + kind = tag(node) + if kind == "element": + return node.get("name") or node.get("ref") + if kind == "group": + return node.get("ref") + if kind == "any": + return "any" + # xjc only uses the first three particles of a model group for its name. + return "Or".join(label(c) for c in [c for c in node if is_particle(c)][:3]) + + +def xpath_step(node, parent): + """One XPath step, with a positional predicate only when siblings share the tag.""" + siblings = [c for c in parent if c.tag == node.tag] + step = "xsd:" + tag(node) + if len(siblings) > 1: + step += "[%d]" % (siblings.index(node) + 1) + return step + + +def xpath(node, parents, schema): + """Absolute path from the named top-level component down to node.""" + chain = [] + current = node + while parents[current] is not schema: + chain.append(current) + current = parents[current] + top = current + if top.get("name") is None: + raise SystemExit("top-level component without a name: %s" % top.tag) + path = "//xsd:%s[@name = '%s']" % (tag(top), top.get("name")) + for n in reversed(chain): + path += "/" + xpath_step(n, parents[n]) + return path + + +def property_target(ref_element, parents): + """The node xjc binds to a property for this reference: the outermost repeated + model group containing it, or the element itself when there is none.""" + target = ref_element + current = parents[ref_element] + while tag(current) in MODEL_GROUPS: + if repeated(current): + target = current + current = parents[current] + return target + + +def process_schema(xsd_path): + schema = ET.parse(xsd_path).getroot() + parents = {c: p for p in schema.iter() for c in p} + properties = [] # (xpath, new name, old name) + factories = [] # (element name, new name) + classes = [] # (xpath, new name) + for node in schema.iter(): + kind = tag(node) + if kind == "element" and (node.get("ref") or "").endswith(SUFFIX): + target = property_target(node, parents) + old = lower_first(label(target)) + new = old.replace(SUFFIX, "") + properties.append((xpath(target, parents, schema), new, old)) + elif kind == "element" and (node.get("name") or "").endswith(SUFFIX) and parents[node] is schema: + name = node.get("name") + bare = name[: -len(SUFFIX)] + if any(tag(c) == "complexType" for c in node): + classes.append((xpath(node, parents, schema), bare + "_")) + else: + factories.append((name, bare + "_")) + elif kind == "complexType" and (node.get("name") or "").endswith(SUFFIX) and parents[node] is schema: + bare = node.get("name")[: -len(SUFFIX)] + classes.append((xpath(node, parents, schema), bare + "_")) + # The same repeated group is reached once per _Dummy reference it contains. + properties = list(dict.fromkeys(properties)) + return properties, factories, classes + + +def reachable_schemas(entry): + """All schema files included or imported, directly or not, from the entry point.""" + seen = set() + todo = [entry] + while todo: + path = todo.pop().resolve() + if path in seen: + continue + seen.add(path) + for node in ET.parse(path).getroot(): + if tag(node) in ("include", "import") and node.get("schemaLocation"): + todo.append(path.parent / node.get("schemaLocation")) + return sorted(seen) + + +def render(entry, bindings_path): + lines = ["\t" + BEGIN] + old_names = [] + for xsd_path in reachable_schemas(entry): + properties, factories, classes = process_schema(xsd_path) + if not (properties or factories or classes): + continue + location = "./" + xsd_path.relative_to(bindings_path.parent).as_posix() + lines.append('\t' % location) + for path, new, old in properties: + old_names.append(old) + lines.append('\t\t' % path) + lines.append('\t\t\t' % new) + lines.append("\t\t") + for name, new in factories: + lines.append("\t\t" % name) + lines.append('\t\t\t' % new) + lines.append("\t\t") + for path, new in classes: + lines.append('\t\t' % path) + lines.append('\t\t\t' % new) + lines.append("\t\t") + lines.append("\t") + lines.append("\t" + END) + return "\n".join(lines) + "\n", old_names + + +def splice(bindings_path, block): + text = bindings_path.read_text() + begin = text.find("\t" + BEGIN) + end = text.find(END) + if begin >= 0 and end >= 0: + end = text.index("\n", end) + 1 + return text[:begin] + block + text[end:] + closing = text.rstrip().rfind("") + return text[:closing].rstrip() + "\n\n" + block + "\n\n" + + +def main(): + if len(sys.argv) != 3: + raise SystemExit("usage: generate-dummy-bindings.py ") + entry = pathlib.Path(sys.argv[1]).resolve() + bindings_path = pathlib.Path(sys.argv[2]).resolve() + block, old_names = render(entry, bindings_path) + bindings_path.write_text(splice(bindings_path, block)) + # Printed so the result can be checked against the names xjc generated before. + for name in old_names: + print(name) + + +if __name__ == "__main__": + main() diff --git a/bindings.xjb b/bindings.xjb index c5b845b6..ee1f2738 100644 --- a/bindings.xjb +++ b/bindings.xjb @@ -61,13 +61,6 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java b/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java index 05492a61..ce4cc79e 100644 --- a/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java +++ b/src/test/java/org/rutebanken/netex/model/MarshalUnmarshalTest.java @@ -273,12 +273,12 @@ void unmarshalPublicationDeliveryAndVerifyValidBetween() throws JAXBException, I CompositeFrame compositeFrame = (CompositeFrame) actual.dataObjects.compositeFrameOrCommonFrame .get(0).getValue(); ValidityConditions_RelStructure validityConditions = compositeFrame.getValidityConditions(); - ValidBetween validBetweenWithTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition_Dummy().get(0); + ValidBetween validBetweenWithTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition().get(0); assertThat(validBetweenWithTimezone.getFromDate()).isNotNull(); assertThat(validBetweenWithTimezone.getToDate()).isNotNull(); assertThat(validBetweenWithTimezone.getToDate()).hasToString("2017-01-01T11:00"); - ValidBetween validBetweenWithoutTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition_Dummy().get(1); + ValidBetween validBetweenWithoutTimezone = (ValidBetween) validityConditions.getValidityConditionRefOrValidBetweenOrValidityCondition().get(1); assertThat(validBetweenWithoutTimezone.getFromDate()).isNotNull(); assertThat(validBetweenWithoutTimezone.getToDate()).isNotNull(); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java index 7a1595dc..a84b0129 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalResourceFrameTest.java @@ -110,7 +110,7 @@ void unmarshalResourceFrame() throws JAXBException { CompositeFrame compositeFrame = (CompositeFrame) publicationDeliveryStructure.getDataObjects().getCompositeFrameOrCommonFrame().get(0).getValue(); ResourceFrame resourceFrame = (ResourceFrame) compositeFrame.getFrames().getCommonFrame().get(0).getValue(); - Authority authority = (Authority) resourceFrame.getOrganisations().getOrganisation_Dummy().get(0).getValue(); + Authority authority = (Authority) resourceFrame.getOrganisations().getOrganisation().get(0).getValue(); assertEquals("Vy", getStringValue(authority.getName())); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java index 453c19aa..1a0b4ecc 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceCalendarFrameTest.java @@ -129,7 +129,7 @@ void unmarshalServiceCalendarFrame() throws JAXBException { ServiceCalendarFrame serviceCalendarFrame = (ServiceCalendarFrame) compositeFrame.getFrames().getCommonFrame().get(0).getValue(); - DayType dayType = (DayType) serviceCalendarFrame.getDayTypes().getDayType_Dummy().get(0).getValue(); + DayType dayType = (DayType) serviceCalendarFrame.getDayTypes().getDayType().get(0).getValue(); assertEquals(DayOfWeekEnumeration.TUESDAY, dayType.getProperties().getPropertyOfDay().get(0).getDaysOfWeek().get(0)); OperatingDay operatingDay = serviceCalendarFrame.getOperatingDays().getOperatingDay().get(0); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java index dc5d6626..0beb8c0f 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalServiceFrameTest.java @@ -146,20 +146,20 @@ void unmarshalServiceFrame() throws JAXBException { CompositeFrame compositeFrame = (CompositeFrame) publicationDeliveryStructure.getDataObjects().getCompositeFrameOrCommonFrame().get(0).getValue(); ServiceFrame serviceFrame = (ServiceFrame) compositeFrame.getFrames().getCommonFrame().get(0).getValue(); - Line line = (Line) serviceFrame.getLines().getLine_Dummy().get(0).getValue(); + Line line = (Line) serviceFrame.getLines().getLine().get(0).getValue(); assertEquals("Narvik-Riksgränsen (Sverige)", getStringValue(line.getName())); assertEquals("F8", line.getPublicCode().getValue()); assertEquals("F8", line.getPrivateCode().getValue()); assertEquals("rail", line.getTransportMode().value()); - Route route = (Route) serviceFrame.getRoutes().getRoute_Dummy().get(0).getValue(); + Route route = (Route) serviceFrame.getRoutes().getRoute().get(0).getValue(); assertEquals("Riksgränsen-Narvik Ofotbanen", getStringValue(route.getName())); assertEquals("KJ-NK", getStringValue(route.getShortName())); PointOnRoute pointOnRoute = route.getPointsInSequence().getPointOnRoute().get(0); assertEquals("VYG:RoutePoint:KJ", pointOnRoute.getPointRef().getValue().getRef()); - JourneyPattern journeyPattern = (JourneyPattern) serviceFrame.getJourneyPatterns().getJourneyPattern_Dummy().get(0).getValue(); + JourneyPattern journeyPattern = (JourneyPattern) serviceFrame.getJourneyPatterns().getJourneyPattern().get(0).getValue(); assertEquals("KMB-NK", getStringValue(journeyPattern.getName())); assertEquals("VYG:Route:F8-R", journeyPattern.getRouteRef().getRef()); diff --git a/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java b/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java index 2d8c151f..d6b3e093 100644 --- a/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java +++ b/src/test/java/org/rutebanken/netex/model/UnmarshalSiteFrameTest.java @@ -161,7 +161,7 @@ void unmarshalSiteFrame() throws JAXBException { assertEquals("Krokstien", getStringValue(stopPlace.getName())); assertEquals(AllPublicTransportModesEnumeration.BUS, stopPlace.getTransportMode()); assertEquals(StopTypeEnumeration.ONSTREET_BUS, stopPlace.getStopPlaceType()); - assertEquals("BRA:TariffZone:311", stopPlace.getTariffZones().getTariffZoneRef_Dummy().get(0).getValue().getRef()); + assertEquals("BRA:TariffZone:311", stopPlace.getTariffZones().getTariffZoneRef().get(0).getValue().getRef()); Quay quay = (Quay) stopPlace.getQuays().getQuayRefOrQuay().get(0).getValue(); assertEquals(BigDecimal.valueOf(59.910579), quay.getCentroid().getLocation().getLatitude());