XJC plugin to restrict marshalled data according to the TolerantReader pattern.
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.
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>
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.
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>
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.
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.
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.
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.
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.
Original property name of a tr:alias element which will be renamed. Must be omitted when defining a computed property.
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" />
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>
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.
|
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 ? "" : name.firstName)
+ (name.firstName != null && name.lastName != null ? " " : "")
+ (name.lastName == null? "" : name.lastName)
</tr:alias>
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;
}
}
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.
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
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"
-
Papers on XML Versioning and Extensibility by David Orchard
-
XML Schema Versioning by XFront
-
XML Versioning vs Extensibility Subbu Allamaraju: "My conclusion is that extensibility and versioning are two different beasts and require different solutions"
-
Versioning XML Schemas "Once you publish an interface, it is set in stone, and you should not introduce incompatible changes"
-
Processing Versioned XML Documents discusses possibilities to let multiple versions of instance documents look like the version supported by the consumer of an instance document.
-
David Tiller, Make a Surgical Strike with a Custom XJC Plugin and Extending XJC Functionality With a Custom Plugin
-
Dr. Aleksei Valikov, whose answer on stackoverflow encouraged me to write this plugin
-
Nicolas Fraenkel’s blog entry Customize your JAXB bindings shows additional ways to customize your JAXB classes, e.g. with base classes and converters.