Wednesday, 18 October 2017

Use JAXB to generate classes from FHIR XSD schema

Running the FHIR XSD schemas through JAXB throws a bunch of exceptions, for example:

com.sun.istack.SAXParseException2; systemId: file:../xsd/fhir-xhtml.xsd; lineNumber: 283; columnNumber: 52; Property "Lang" is already defined. Use <jaxb:property> to resolve this conflict.

com.sun.istack.SAXParseException2; systemId: file:../xsd/fhir-xhtml.xsd; lineNumber: 1106; columnNumber: 58; Property "Lang" is already defined. Use <jaxb:property> to resolve this conflict.

org.xml.sax.SAXParseException; systemId: file:../xsd/fhir-single.xsd; lineNumber: 81; columnNumber: 31; A class/interface with the same name "org.adrianwalker.fhir.resources.Code" is already in use. Use a class customization to resolve this conflict.

org.xml.sax.SAXParseException; systemId: file:../xsd/fhir-single.xsd; lineNumber: 1173; columnNumber: 34; A class/interface with the same name "org.adrianwalker.fhir.resources.Address" is already in use. Use a class customization to resolve this conflict.

Without modifying the original FHIR XSD files, the JAXB conflicts can be resolved using JAXB bindings:

fhir-xhtml.xjb

<bindings xmlns="http://java.sun.com/xml/ns/jaxb"
          xmlns:xsi="http://www.w3.org/2000/10/XMLSchema-instance"
          xmlns:xs="http://www.w3.org/2001/XMLSchema"
          version="2.1">
  <bindings schemaLocation="../xsd/fhir-xhtml.xsd" version="1.0">

    <!--
    Fixes:-

    com.sun.istack.SAXParseException2; systemId: file:../xsd/fhir-xhtml.xsd;
    lineNumber: 283; columnNumber: 52; Property "Lang" is already defined. Use
    <jaxb:property> to resolve this conflict.
    -->
    <bindings node="//xs:attributeGroup[@name='i18n']">
      <bindings node=".//xs:attribute[@name='lang']">
        <property name="xml:lang"/>
      </bindings>
    </bindings>

    <!--
    Fixes:-

    com.sun.istack.SAXParseException2; systemId: file:../xsd/fhir-xhtml.xsd;
    lineNumber: 1106; columnNumber: 58; Property "Lang" is already defined. Use
    <jaxb:property> to resolve this conflict.
    -->
    <bindings node="//xs:element[@name='bdo']">
      <bindings node=".//xs:attribute[@name='lang']">
        <property name="xml:lang"/>
      </bindings>
    </bindings>
  </bindings>
</bindings>

fhir-single.xjb

<bindings xmlns="http://java.sun.com/xml/ns/jaxb"
          xmlns:xsi="http://www.w3.org/2000/10/XMLSchema-instance"
          xmlns:xs="http://www.w3.org/2001/XMLSchema"
          version="2.1">
  <bindings schemaLocation="../xsd/fhir-single.xsd" version="1.0">

    <!--
    Fixes:-

    org.xml.sax.SAXParseException; systemId: file:../xsd/fhir-single.xsd;
    lineNumber: 81; columnNumber: 31; A class/interface with the same name
    "org.adrianwalker.fhir.Code" is already in use. Use a class customization to
    resolve this conflict.
    -->
    <bindings node="//xs:complexType[@name='code']">
      <class name="CodeString" />
    </bindings>

    <!--
    Fixes:-

    org.xml.sax.SAXParseException; systemId: file:../xsd/fhir-single.xsd;
    lineNumber: 1173; columnNumber: 34; A class/interface with the same name
    "org.adrianwalker.fhir.Address" is already in use. Use a class customization
    to resolve this conflict.
    -->
    <bindings node="//xs:complexType[@name='Address']">
      <class name="PostalAddress" />
    </bindings>

  </bindings>
</bindings>

I've used the org.jvnet.jaxb2.maven2 jaxb2-maven-plugin Maven plugin, configured with the net.java.dev.jaxb2-commons jaxb-fluent-api plugin to generate the resource classes, with fluent API mutators for method chaining.

pom.xml

...
<build>
  <plugins>
    <plugin>
      <groupId>org.jvnet.jaxb2.maven2</groupId>
      <artifactId>maven-jaxb2-plugin</artifactId>
      <version>0.13.2</version>
      <configuration>
        <extension>true</extension>
        <args>
          <arg>-Xfluent-api</arg>
        </args>
        <schemaDirectory>src/main/xsd</schemaDirectory>
        <bindingDirectory>src/main/xjb</bindingDirectory>
        <generatePackage>org.adrianwalker.fhir.resources</generatePackage>
        <plugins>
          <plugin>
            <groupId>net.java.dev.jaxb2-commons</groupId>
            <artifactId>jaxb-fluent-api</artifactId>
            <version>2.1.8</version>
          </plugin>
        </plugins>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>generate</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
...

For example usage of generated classes and minimal unit testing see PatientExampleTest.java:

PatientExampleTest.java

package org.adrianwalker.fhir.resources;

import java.io.ByteArrayOutputStream;
import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

/*
 * Patient Example xml from: https://www.hl7.org/fhir/patient-example.xml.html
 */
public final class PatientExampleTest {

  private static Unmarshaller unmarshaller;
  private static Marshaller marshaller;

  @BeforeClass
  public static void setUp() throws JAXBException {

    JAXBContext context = JAXBContext.newInstance(Patient.class);
    unmarshaller = context.createUnmarshaller();
    marshaller = context.createMarshaller();
  }

  @Test
  public void testXmlToPatient() throws JAXBException {

    Patient patient = unmarshalPatient("src/test/resources/patient-example.xml");

    Assert.assertEquals("example", patient.getId().getValue());
    Assert.assertEquals("Chalmers", patient.getName().get(0).getFamily().getValue());
    Assert.assertEquals("Peter", patient.getName().get(0).getGiven().get(0).getValue());
    Assert.assertEquals("James", patient.getName().get(0).getGiven().get(1).getValue());
  }

  @Test
  public void testPatientToXml() throws JAXBException {

    Patient patient = new Patient()
            .withId(new Id().withValue("test"))
            .withName(new HumanName()
                    .withGiven(new String().withValue("Adrian"))
                    .withFamily(new String().withValue("Walker")));

    Assert.assertEquals(
            "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
            + "<Patient xmlns=\"http://hl7.org/fhir\" xmlns:ns2=\"http://www.w3.org/1999/xhtml\">"
            + "<id value=\"test\"/>"
            + "<name>"
            + "<family value=\"Walker\"/>"
            + "<given value=\"Adrian\"/>"
            + "</name>"
            + "</Patient>",
            marshalPatient(patient));
  }

  private Patient unmarshalPatient(final java.lang.String filename) throws JAXBException {

    JAXBElement<Patient> element = unmarshaller.unmarshal(
            new StreamSource(new File(filename)), Patient.class);

    return element.getValue();
  }

  private java.lang.String marshalPatient(final Patient patient) throws JAXBException {

    JAXBElement<Patient> element = new ObjectFactory().createPatient(patient);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    marshaller.marshal(element, baos);

    return baos.toString();
  }
}

Source Code

Build and Test

The project is a standard Maven project which can be built with:

mvn clean install