Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,19 @@ mvn generate-sources

**Note:** Tests run with `-Duser.timezone=Europe/Oslo` (configured in surefire plugin).

**Note:** Surefire runs tests on the module path because `module-info.java` exists.

## Architecture

### Code Generation (bulk of the codebase)

The `cxf-xjc-plugin` generates ~1,900 Java classes from XSD schemas during `generate-sources`:
- **SIRI 2.0** XSDs → `uk.org.siri.siri20.*` (plus `uk.org.ifopt.siri20`, `uk.org.acbs.siri20`, `eu.datex2.siri20`)
- **SIRI 2.1** XSDs → `uk.org.siri.siri21.*` (plus `uk.org.ifopt.siri21`, `uk.org.acbs.siri21`, `eu.datex2.siri21`)
- **SIRI 2.0** XSDs → `org.entur.siri.schema.v20.siri` (plus `.ifopt`, `.acsb`, `.datex2`)
- **SIRI 2.1** XSDs → `org.entur.siri.schema.v21.siri` (plus `.ifopt`, `.acsb`, `.datex2`, `.gml`)

Generated sources land in `target/generated-sources`. JAXB binding customizations are in `src/main/resources/siri-{2.0,2.1}/xjb/jaxb-bindings.xml` — these map `xsd:dateTime`/`xsd:time` to `java.time.ZonedDateTime` and `xsd:duration` to `java.time.Duration`.

### Handwritten Code (~9 files)
### Handwritten Code (~11 files)

| Package | Class | Purpose |
|---------|-------|---------|
Expand All @@ -47,16 +49,18 @@ Generated sources land in `target/generated-sources`. JAXB binding customization
| `org.entur.siri.validator` | `SiriValidationEventHandler` | Collects validation errors |
| `org.entur.siri21.util` | `SiriXml` | SIRI 2.1 XML marshalling/unmarshalling (JAXB) |
| `org.entur.siri21.util` | `SiriJson` | SIRI 2.1 JSON conversion (Jackson + Jakarta XML Bind) |
| `org.rutebanken.siri20.util` | `SiriXml` | SIRI 2.0 XML marshalling/unmarshalling (legacy package) |
| `org.entur.siri20.util` | `SiriXml` | SIRI 2.0 XML marshalling/unmarshalling |
| `org.entur.siri20.util` | `SiriJson` | SIRI 2.0 JSON conversion (Jackson + Jakarta XML Bind) |
| `org.entur.siri` | `UnsupportedSiriVersionException` | Exception for unsupported SIRI versions |
| (module root) | `module-info.java` | JPMS descriptor: `open module org.entur.siri` |

### Key Design Decisions

- **Jakarta EE**: Uses `jakarta.xml.bind` (3.0+), not legacy `javax.xml.bind`.
- **Java 11** target (source and bytecode) via `<release>` in the compiler plugin; CI builds on JDK 25, and any JDK from 11 to 25 works locally.
- **JPMS**: `src/main/java/module-info.java` declares `open module org.entur.siri` (open because the consumer's JAXB runtime needs reflective access). When you add a generated or handwritten package, add an `exports` line for it.
- **No `NamespacePrefixMapper` in the API**: `SiriXml.toXml` takes a `Consumer<Marshaller>` customizer. The `JAXBNamespacePrefixMapper` stub shares a package with the JAXB RI, so it cannot be used on the module path.
- **JUnit 4** for tests.
- **JSON support exists only for SIRI 2.1** (`SiriJson`), not 2.0.
- **SIRI 2.0 utility lives in legacy package** `org.rutebanken.siri20.util` (historical).

### Test Data

Expand Down
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,56 @@ Source 1.0 - 1.4 http://user47094.vs.easily.co.uk/siri/

Source 2.0 -> https://www.vdv.de/siri.aspx

Also provides XML-validation through method `SiriValidator.validate`.
Also provides XML-validation through method `SiriValidator.validate`.

## Java modules (JPMS)

The jar is a named module, `org.entur.siri`. It is an `open module`, because the JAXB runtime that you supply needs reflective access to the generated classes. It requires `jakarta.xml.bind` and Jackson. You must also provide a JAXB implementation, for example `com.sun.xml.bind:jaxb-impl`.

```java
module my.app {
requires org.entur.siri;
}
```

## Upgrading to 3.0

**Breaking change: the generated packages are renamed.**
The XML namespaces and the wire format are unchanged. Update your imports:

| Before | After |
|---|---|
| `uk.org.siri.siri20` / `siri21` | `org.entur.siri.schema.v20.siri` / `v21.siri` |
| `uk.org.ifopt.siri20` / `siri21` | `org.entur.siri.schema.v20.ifopt` / `v21.ifopt` |
| `uk.org.acbs.siri20` / `siri21` | `org.entur.siri.schema.v20.acsb` / `v21.acsb` |
| `eu.datex2.siri20.schema._2_0rc1._2_0` / `siri21...` | `org.entur.siri.schema.v20.datex2` / `v21.datex2` |
| `net.opengis.gml.siri` | `org.entur.siri.schema.v21.gml` |


**Breaking change: `NamespacePrefixMapper` is removed from `SiriXml`.**
The overloads `toXml(Siri, NamespacePrefixMapper)` and `toXml(Siri, NamespacePrefixMapper, OutputStream)` in `org.entur.siri21.util.SiriXml` and `org.entur.siri20.util.SiriXml` now take a `Consumer<Marshaller>` instead. The `JAXBNamespacePrefixMapper` dependency is removed, because it shares a package with the JAXB reference implementation and cannot be used on the module path.

Before:

```java
SiriXml.toXml(siri, myNamespacePrefixMapper);
```

After: set the property of your own JAXB implementation on the marshaller.

```java
SiriXml.toXml(siri, marshaller -> {
try {
marshaller.setProperty("org.glassfish.jaxb.namespacePrefixMapper", myNamespacePrefixMapper);
} catch (PropertyException e) {
throw new IllegalStateException(e);
}
});
```

The property name depends on your JAXB implementation. Use the one that your implementation documents. If your own code imports `com.sun.xml.bind.marshaller.NamespacePrefixMapper`, add the dependency that provides it yourself. This library no longer brings it in.

**Breaking change: the SIRI 2.0 utility package is renamed.**
`org.rutebanken.siri20.util` (`SiriXml`, `SiriJson`) is now `org.entur.siri20.util`, which matches `org.entur.siri21.util`. Update your imports. There are no deprecated forwarders in the old package.

The module name is now explicit (`org.entur.siri`). It was derived from the jar file name (`siri.java.model`) before.
30 changes: 6 additions & 24 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>11</jdk.version>
<slf4j.version>2.0.9</slf4j.version>
<jackson.version>2.18.11</jackson.version>
<slf4j.version>2.0.7</slf4j.version>
<jreleaser-maven-plugin.version>1.18.0</jreleaser-maven-plugin.version>
<cxf-xjc-plugin.version>4.2.0</cxf-xjc-plugin.version>
<maven-javadoc-plugin.version>3.11.2</maven-javadoc-plugin.version>
Expand Down Expand Up @@ -71,11 +69,6 @@
</organization>

<dependencies>
<dependency>
<groupId>com.googlecode.jaxb-namespaceprefixmapper-interfaces</groupId>
<artifactId>JAXBNamespacePrefixMapper</artifactId>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand All @@ -90,6 +83,12 @@
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jakarta-xmlbind-annotations</artifactId>
<version>${jackson.version}</version>
<exclusions>
<exclusion>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
Expand All @@ -108,30 +107,13 @@
<version>3.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<scope>test</scope>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
open module org.entur.siri {
requires transitive jakarta.xml.bind;
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.module.jakarta.xmlbind;

exports org.entur.siri;
exports org.entur.siri.adapter;
exports org.entur.siri.validator;
exports org.entur.siri21.util;
exports org.entur.siri20.util;
exports org.entur.siri.schema.v20.acsb;
exports org.entur.siri.schema.v20.datex2;
exports org.entur.siri.schema.v20.ifopt;
exports org.entur.siri.schema.v20.siri;
exports org.entur.siri.schema.v21.acsb;
exports org.entur.siri.schema.v21.datex2;
exports org.entur.siri.schema.v21.gml;
exports org.entur.siri.schema.v21.ifopt;
exports org.entur.siri.schema.v21.siri;
}
Empty file.
22 changes: 22 additions & 0 deletions src/main/java/org/entur/siri/adapter/ZonedDateTimeXmlAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.entur.siri.adapter;

import jakarta.xml.bind.annotation.adapters.XmlAdapter;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
* JAXB adapter for {@code xsd:dateTime} bound to {@link ZonedDateTime}. Written by hand so that XJC does not
* generate an adapter class into the {@code org.w3._2001.xmlschema} package, which would be a split package.
*/
public class ZonedDateTimeXmlAdapter extends XmlAdapter<String, ZonedDateTime> {

@Override
public ZonedDateTime unmarshal(String value) {
return ZonedDateTimeAdapter.parse(value);
}

@Override
public String marshal(ZonedDateTime value) {
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value);
}
}
22 changes: 22 additions & 0 deletions src/main/java/org/entur/siri/adapter/ZonedTimeXmlAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.entur.siri.adapter;

import jakarta.xml.bind.annotation.adapters.XmlAdapter;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
* JAXB adapter for {@code xsd:time} bound to {@link ZonedDateTime}. Written by hand so that XJC does not
* generate an adapter class into the {@code org.w3._2001.xmlschema} package, which would be a split package.
*/
public class ZonedTimeXmlAdapter extends XmlAdapter<String, ZonedDateTime> {

@Override
public ZonedDateTime unmarshal(String value) {
return ZonedDateTimeAdapter.parse(value);
}

@Override
public String marshal(ZonedDateTime value) {
return DateTimeFormatter.ISO_OFFSET_TIME.format(value);
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/entur/siri/validator/SiriValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ public class SiriValidator {

private static void init() throws JAXBException {
if (siri20jaxbContext == null) {
siri20jaxbContext = JAXBContext.newInstance(uk.org.siri.siri20.Siri.class);
siri20jaxbContext = JAXBContext.newInstance(org.entur.siri.schema.v20.siri.Siri.class);
}
if (siri21jaxbContext == null) {
siri21jaxbContext = JAXBContext.newInstance(uk.org.siri.siri21.Siri.class);
siri21jaxbContext = JAXBContext.newInstance(org.entur.siri.schema.v21.siri.Siri.class);
}
}
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* limitations under the Licence.
*/

package org.rutebanken.siri20.util;
package org.entur.siri20.util;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
Expand All @@ -23,7 +23,7 @@
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
import uk.org.siri.siri20.Siri;
import org.entur.siri.schema.v20.siri.Siri;

import java.io.IOException;
import java.io.OutputStream;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
* limitations under the Licence.
*/

package org.rutebanken.siri20.util;
package org.entur.siri20.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.function.Consumer;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
Expand All @@ -29,10 +30,9 @@
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import com.sun.xml.bind.marshaller.NamespacePrefixMapper;

import org.entur.siri.XmlInputFactoryProvider;
import uk.org.siri.siri20.Siri;
import org.entur.siri.schema.v20.siri.Siri;

public class SiriXml {

Expand Down Expand Up @@ -73,24 +73,24 @@ public static String toXml(Siri siri) throws JAXBException {
return toXml(siri, null);
}

public static String toXml(Siri siri, NamespacePrefixMapper customNamespacePrefixMapper) throws JAXBException {
public static String toXml(Siri siri, Consumer<Marshaller> marshallerCustomizer) throws JAXBException {

Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

if (customNamespacePrefixMapper != null) {
jaxbMarshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", customNamespacePrefixMapper);
if (marshallerCustomizer != null) {
marshallerCustomizer.accept(jaxbMarshaller);
}
StringWriter sw = new StringWriter();
jaxbMarshaller.marshal(siri, sw);

return sw.toString();
}

public static void toXml(Siri siri, NamespacePrefixMapper customNamespacePrefixMapper, OutputStream out) throws JAXBException, IOException {
public static void toXml(Siri siri, Consumer<Marshaller> marshallerCustomizer, OutputStream out) throws JAXBException, IOException {
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

if (customNamespacePrefixMapper != null) {
jaxbMarshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", customNamespacePrefixMapper);
if (marshallerCustomizer != null) {
marshallerCustomizer.accept(jaxbMarshaller);
}
jaxbMarshaller.marshal(siri, out);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/entur/siri21/util/SiriJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
import uk.org.siri.siri21.Siri;
import org.entur.siri.schema.v21.siri.Siri;

import java.io.IOException;
import java.io.OutputStream;
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/org/entur/siri21/util/SiriXml.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@

package org.entur.siri21.util;

import com.sun.xml.bind.marshaller.NamespacePrefixMapper;
import org.entur.siri.XmlInputFactoryProvider;
import uk.org.siri.siri21.Siri;
import org.entur.siri.schema.v21.siri.Siri;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
Expand All @@ -31,6 +30,7 @@
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.function.Consumer;

public class SiriXml {

Expand Down Expand Up @@ -71,24 +71,24 @@ public static String toXml(Siri siri) throws JAXBException {
return toXml(siri, null);
}

public static String toXml(Siri siri, NamespacePrefixMapper customNamespacePrefixMapper) throws JAXBException {
public static String toXml(Siri siri, Consumer<Marshaller> marshallerCustomizer) throws JAXBException {

Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

if (customNamespacePrefixMapper != null) {
jaxbMarshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", customNamespacePrefixMapper);
if (marshallerCustomizer != null) {
marshallerCustomizer.accept(jaxbMarshaller);
}
StringWriter sw = new StringWriter();
jaxbMarshaller.marshal(siri, sw);

return sw.toString();
}

public static void toXml(Siri siri, NamespacePrefixMapper customNamespacePrefixMapper, OutputStream out) throws JAXBException, IOException {
public static void toXml(Siri siri, Consumer<Marshaller> marshallerCustomizer, OutputStream out) throws JAXBException, IOException {
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

if (customNamespacePrefixMapper != null) {
jaxbMarshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", customNamespacePrefixMapper);
if (marshallerCustomizer != null) {
marshallerCustomizer.accept(jaxbMarshaller);
}
jaxbMarshaller.marshal(siri, out);
}
Expand Down
Loading
Loading