Skip to content

XJC plugin to restrict marshalled data according to the TolerantReader pattern

Notifications You must be signed in to change notification settings

virtual-machinist/jaxb2-tolerant-reader-plugin

 
 

Repository files navigation

jaxb2-tolerant-reader-plugin Build Status

Motivation

Sometimes you have an extensive system of schema files with lots of classes and attributes, at times so large that developers struggle with the richness of the schema. Furthermore, the schema might evolve in incompatible ways for reasons which are out of your control.

The client on the other hand uses only a tiny fraction of the types from the schema. As a developer, I do not want to bother with an enormous tree of classes I don’t understand and I want no changes to affect my code base which are irrelevant to my client.

The goal of this plugin is to handle this situation by applying ideas of TolerantReader to JAXB. In an attempt to eat the cake and have it, too, I want to generate beans from the schema, but I want to be able to depend only on the data I care about, and most importantly, I still want to avoid the serialization antipattern of enterprise integration by being able to decouple structurally.

I want to use the incoming xml as a business document defined in terms of a schema by creating a Java bean from it, but not by deserializing it as a remote binary object. I want to decouple my Java bean from the evolution of the schema as much as possible, thus at least partially avoiding the dangers of serialization.

Note that schema changes might still require regeneration and recompilation of the client code. The goal is that at least this should be all there is to do, and that I get immediate feedback when the schema change breaks my client, so I can fix the breaking change at the root.

The plugin configuration can also be handed to the service as an executable description of my client expectations. It could be used as a test by the service to see which client will break upon any given change.

Finally, the plugin annotates the generated beans with the @Expose annotation from hydra-jsonld, if hydra-jsonld is on the classpath.

Using the Plugin in a Maven Build

To set up the plugin for a maven build, use the maven-jaxb2-plugin. In the maven plugin configuration add the jaxb2-tolerant-reader xjc plugin and make sure you enable xjc extension mode, as shown below. The plugin is activated via the -Xtolerant-reader switch.

Important
Currently the tolerant-reader-plugin requires a patch to xjc classinfo (xjc-classinfo-patch). The patch is based upon the xjc version 2.2.11 used by the current maven-jaxb2-plugin. A pull request has been submitted to gf-metro. In order to introduce the patch, add it as dependency to the maven-jaxb2-plugin as shown below.

Since the tolerant-reader-plugin updates the Outline built from the schema, it must run before other plugins such as the jaxb2-basics plugin, so they can pick up the changes introduced by tolerant-reader-plugin.

The pom.xml of the it/person test project uses the tolerant-reader-plugin with jaxb2-basics plugin.

<plugin>
  <groupId>org.jvnet.jaxb2.maven2</groupId>
  <artifactId>maven-jaxb2-plugin</artifactId>
  <version>0.13.1</version>
  <dependencies>
    <dependency>
      <groupId>de.escalon.jaxb2</groupId>
      <artifactId>xjc-classinfo-patch</artifactId>
      <version>0.4.6</version>
    </dependency>
  </dependencies>
  <configuration>
    <extension>true</extension>
    <verbose>true</verbose>
  </configuration>
  <executions>
    <execution>
      <id>person</id>
      <goals>
        <goal>generate</goal>
      </goals>
      <configuration>
        <args>
          <arg>-Xtolerant-reader</arg>
        </args>
        <schemaDirectory>${basedir}/src/main/wsdl/example</schemaDirectory>
        <produces>
          <produce>com.example.person/**/*.java</produce>
        </produces>
        <episode>false</episode>
        <specVersion>2.0</specVersion>
        <plugins>
          <plugin>
            <groupId>de.escalon.jaxb2</groupId>
            <artifactId>jaxb2-tolerant-reader</artifactId>
            <version>0.4.6</version>
          </plugin>
        </plugins>
      </configuration>
    </execution>
  </executions>
</plugin>

Configuration: Include Required Data Only and Decouple Structurally

The idea is to require only the Java beans and bean attributes your client really needs and be tolerant about the rest. For this, you define a binding file with an include element on the schema level where you describe beans that should be generated.

If the service provider renames a property or introduces an otherwise incompatible structural change, I want to keep my representation of the data intact. To cope with situations like that, it is possible to rename properties and to use XmlAdapters or computed fields as a mechanism to handle a structural change.

Schemas which use version numbers in their targetNamespace are a particular challenge. See packageRoot attribute (optional) for possibilities to suppress versioned package names for your beans.

Example bindings file

The configuration of the tolerant-reader-plugin uses the standard customization options of the xml-to-java compiler xjc. Below you see an example of an external binding customization file, i.e a bindings.xjb file which you put into your schemadirectory. In the sample binding below we use the extension binding prefix tr for the tolerant-reader plugin namespace.

<jxb:bindings version="2.1" xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:tr="http://jaxb2-commons.dev.java.net/tolerant-reader"
  xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
  xmlns:person="http://example.com/person"
  jxb:extensionBindingPrefixes="xjc tr">

  <jxb:globalBindings>
    <xjc:simple />
    <xjc:serializable uid="1" />
  </jxb:globalBindings>

  <jxb:bindings schemaLocation="Person.xsd">
    <tr:include packageRoot="com.example" prefix="cust">

      <!-- bean with required properties only -->
      <tr:bean name="USAddress" />

      <!-- bean alias name (e.g. to translate bean names) -->
      <tr:bean name="BaseAddress" alias="AddrBase" />

      <!-- bean with required and some optional properties -->
      <tr:bean name="Name" properties="firstName middleInitial lastName" />

      <!-- bean with property alias -->
      <tr:bean name="GlobalAddress" alias="Address">
        <tr:alias property="postalCode">postCode</tr:alias>
      </tr:bean>

      <!-- bean with an adapted and a computed property -->
      <tr:bean name="Person" alias="Individuum" properties="age name">
        <tr:alias property="role" alias="function">
          <tr:adapter class="com.example.ValueWrapperXmlAdapter"
            to="java.lang.String" />
        </tr:alias>
        <tr:alias alias="displayName">
          <tr:compute to="java.lang.String"
             expr="T(org.apache.commons.lang3.StringUtils).trimToNull(
                 (name?.firstName?:'')  + ' ' + (name?.lastName?:''))" />
        </tr:alias>
      </tr:bean>

    </tr:include>
  </jxb:bindings>
</jxb:bindings>

tr:include element

Add at least one tr:include element as customization root. If you need to define beans from multiple packages, have one include element per package.

packageRoot attribute (optional)

You may add a packageRoot attribute to an include element if you have to select beans from specific packages. The package root does not have to be the entire package name, it uses startsWith to match packages and falls back to regex matching. That way you can be tolerant about particular versions of a schema if the schema provider uses version numbers in namespaces. I.e. if the schema uses a target namespace com.example.namespace.fun.v5_7, you can use a packageRoot com.example.namespace.fun to select your beans.

Tip
In situations where the schema uses a versioned targetNamespace, apply custom java packages to avoid having to fix lots of import statements for every version change. If you do this, the original versioned namespace will still be preserved during marshalling via the package definition in package-info.java. Note that each version of a schema with versioned targetNamespace requires you to generate a different set of JAXB beans, i.e. your client still speaks only one version of the schema.

The section References lists some blog entries on XML versioning.

prefix attribute (optional)

Allows to specify a prefix to be used for the target namespace URI when used with hydra-jsonld. When the plugin detects hydra-jsonld on the classpath, it annotates the beans with @Term(define="pers", as="http://example.com/person/")

tr:bean element

Describes an expected bean. Super classes will be included automatically. If an expected bean is not defined by the schema, an error is thrown. This allows you to detect and fix breaking changes early.

name attribute

Simple name of the expected bean.

alias attribute

Alias bean name to be used instead of the name.

properties attribute (optional)

List of expected bean properties as space-separated strings. Required properties are included automatically, i.e. you only need to define elements having minOccurs=0 and attributes without required=true. If an included property has a complex type, the bean for that type will be included automatically.

In cases where you do not simply expect a property, but you also want to rename it, use a tr:alias element instead.

tr:alias element

Describes a property which should be generated with an alias name, one tr:alias element per property. The generated property will be renamed either to the content of the alias element, or to the value of the alias attribute of the tr:alias element. See the explanation of the alias attribute below for examples.

The property you want to rename is given with the property attribute (see below).

May be used in combination with the properties attribute of the tr:bean element, i.e. you may have some properties you expect with their original name and some other, aliased properties.

property attribute

Original property name of a tr:alias element which will be renamed. Must be omitted when defining a computed property.

alias attribute

A tr:alias element can define the alias name to be used as content of the element:

<tr:alias property="foo">bar</tr:alias>

As an alternative, it is also valid to define the alias name with an alias attribute. Must be used with tr:adapter and tr:compute.

<tr:alias property="foo" alias="bar" />

tr:adapter element (optional)

Adapter specification to adapt a field, for use inside of a tr:alias element. Will annotate the property with an @XmlJavaTypeAdapter annotation. If an adapter is applied, the alias name must be given with an alias attribute, not as content of the tr:alias element.

In the example below, a ValueWrapperXmlAdapter adapts the field role of complex type ValueWrapper to a simple String by extracting the wrapped value.

<tr:alias property="role" alias="function">
  <tr:adapter class="com.example.ValueWrapperXmlAdapter"
    to="java.lang.String" />
</tr:alias>
class attribute

Fully qualified class name of the XmlAdapter implementation to apply to the field.

to attribute

Fully qualified class name of the type to which the adapter adapts the field. By default, this is java.lang.String.

Note
The TolerantReaderPlugin cannot determine this type automatically for adapters from the adapter class. At the time of schema compilation the class of an XmlAdapter implementation cannot be available, since the XmlAdapter implementation requires the JAXB type for compilation.

tr:compute element (optional)

Specifies a computed field which will be generated as @XmlTransient, for use inside of a tr:alias element. A computed field requires you to provide an expression inside the expr attribute; furthermore, if the expression does not evaluate to String, the type to which the expression evaluates in the to attribute. Consider the examples in the supported expression languages below.

The expression can be written with SpringEL, javax.el 3.0 or as plain java expression.

Include SpringEL as plugin dependency of the maven-jaxb2-plugin to use SpringEL:

<!-- inside plugin configuration -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-expression</artifactId>
  <version>4.3.5.RELEASE</version>
</dependency>

That allows you to use expressions with Spring EL’s safe navigation ?. and Elvis ?: operators, and you have access to static utilities, too:

<tr:alias alias="displayName">
  <tr:compute to="java.lang.String"
     expr="T(org.apache.commons.lang3.StringUtils).trimToNull(
         (name?.firstName?:'')  + ' ' + (name?.lastName?:''))" />
</tr:alias>

You can also use javax.el 3.0 (starting from Java 1.7) if you add it as dependency to the maven-jaxb2-plugin:

<!-- inside plugin configuration -->
<dependency>
  <groupId>org.glassfish</groupId>
  <artifactId>javax.el</artifactId>
  <version>3.0.0</version>
</dependency>

The generated code for javax.el 3.0 always addresses the current jaxb bean by the name bean:

<tr:alias alias="displayName">
  <tr:compute to="java.lang.String"
    expr="((not empty bean.name.firstName ? bean.name.firstName : '')
      += ' ' += (not empty bean.name.lastName ? bean.name.lastName : '')).trim()"
</tr:alias>

If you include no EL dependencies, you can still write Java expressions, which requires you to use the xml entities for double quotes, ampersand etc., and you have to handle null explicitly.

<tr:alias alias="displayName">
  <tr:compute to="java.lang.String"
    expr="(name.firstName == null ? &quot;&quot; : name.firstName)
      + (name.firstName != null &amp;&amp; name.lastName  != null ? &quot; &quot; : &quot;&quot;)
      + (name.lastName == null? &quot;&quot; : name.lastName)
</tr:alias>

Configuring Annotation for hydra-jsonld

The @Expose annotation of hydra-jsonld can be applied automatically to generate JSON-LD directly from the JAXB beans. In order to annotate your beans with @Expose have the following plugin dependency in your pom.xml.

<plugin>
  <groupId>org.jvnet.jaxb2.maven2</groupId>
  <artifactId>maven-jaxb2-plugin</artifactId>
  <version>0.13.1</version>
  <dependencies>
    ...
    <dependency>
      <groupId>de.escalon.hypermedia</groupId>
      <artifactId>hydra-jsonld</artifactId>
      <version>0.3.1</version>
    </dependency>
  </dependencies>
  ...

The plugin detects the presence of hydra-jsonld and annotates the beans with @Expose. Sample Person:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Person")
@Term(define = "cust", as = "http://example.com/person#")
@Expose("cust:Person")
public class Person {

    @Expose("cust:Person.name")
    public Name getName() {
        return name;
    }
}

Plugin Developers

Building the Plugin Project

This section is for people who do not want to use the plugin, but who want to build the plugin themselves.

Normally it should be sufficient to invoke mvn clean install on the plugin project.

If you run the maven build of the plugin project with embedded maven (e.g. inside Eclipse), make sure you have an environment variable M2_HOME pointing to a standalone maven installation which can be picked up by the maven invoker plugin during integration test.

Manual execution

As a plugin developer you may want to execute the plugin manually, but you want its output in the same place where maven puts it.

In launch and debug configurations you can execute the plugin via the com.sun.tools.xjc.Driver Java main class, with the tolerant-reader-plugin and the xjc-classinfo-patch on the classpath (make sure the xjc-classinfo-patch comes before tolerant-reader-plugin on the classpath in your launch configuration).

One way to achieve this in Eclipse is to create a Java Application launch configuration for com.sun.tools.xjc.Driver while the jaxb2-tolerant-reader project is selected, so that it becomes the launch configuration’s project. Then switch to the Classpath tab, highlight User Entries and add the xjc-classinfo-patch project to the classpath. Finally, hit Up to move it above the jaxb2-tolerant-reader entry.

In Idea, create a run configuration for com.sun.tools.xjc.Driver and configure it to Use classpath of module: tolerant-reader-plugin. In order to adjust the classpath to apply the xjc-classinfo-patch, choose Open Module Settings for the tolerant-reader-plugin module while it is highlighted (hit F4). On the Dependencies tab, hit the green + icon on the right hand side and choose Module Dependency…​ to add xjc-classinfo-patch. Then select xjc-classinfo-patch in the dependencies list and hit the up arrow icon until it is at the top of all dependencies.

Build the plugin project with Maven. This is necessary to create an executable maven test project in target/it/person.

Use the target/it/person project as current working directory of the launch configuration and pass the following arguments:

-extension -no-header -d target/generated-sources/xjc -Xtolerant-reader -b src/main/wsdl/example/bindings.xjb src/main/wsdl/example/Person.xsd

Running the Sample Project in Eclipse

The sample project in src/it makes use of placeholders for the maven invoker plugin. Therefore it cannot run as-is; you have to import the project created by maven-invoker-plugin in target/it.

  • Import the parent project as Maven project

  • Execute a maven build on the parent (with standalone maven; or make sure you have an M2_HOME environment variable) so that the invoker plugin creates a runnable project in target/it.

  • Open the parent project

  • Open the module tolerant-reader-plugin

  • Navigate to target/it/person

  • Right click the person folder and select "Import as Project"

  • Right click the newly imported project and select "Run As - Maven build"

References

Useful JAXB Resources
XML Versioning and Extensibility

Acknowledgements

About

XJC plugin to restrict marshalled data according to the TolerantReader pattern

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Java 100.0%