What’s the best way to stop developers breaking the WADLs generated by CXF for Java-first JAX-RS services? Make breaking the WADLs break the build of course.
I have waxed lyrical previously about my fondness for CXF’s automatically generated WADLs for Java-first JAX-RS services. For service consumers they provide a service contract which can jump-start their client code development with a WADL code generator. As for myself, it makes it really straightforward to hit a service with ad hoc requests as I can point a GUI tool like SoapUI at the WADL, generate template request payloads with a single click, and invoke those requests with ease.
A usable auto-generated WADL depends on a set of model objects that are JAXB friendly though, and herein lies a problem if your services are JSON based and your JSON processor, like Jackson, isn’t particularly tightly bound to JAXB. If you’re not otherwise exercising your model objects via XML they can easily become JAXB unfriendly and stay that way until the next time someone actually tries to use the service WADL.
Having recently tackled a CXF upgrade and found over half the service’s WADLs broken in this manner, and having painfully trawled through the code to find the breaks, I was keen to find a way of getting an early heads-up when it happens again. Automated, WADL-driven integration tests would be an ideal solution but neither trivial to write nor easy to justify when the WADLs are for internal use only. What I was looking for was a lightweight way of breaking the build when a change is made which breaks the WADLs, forcing the guilty developer to clean up their code there and then.
Unbreak my WADL
There are two distinct parts to automated WADL generation. The first is the creation of a JAXB schema from the value objects used by the service. The second is the assembly of the schema and endpoint information into a single XML document by CXF’s WadlGenerator class.
Both parts of this process can be derailed by underlying code issues. The former will yield a WADL with no grammar information in it, whilst the latter will yield an apparently valid and complete WADL which throws errors when imported into a client tool.
I’m going to look at both types of break and how to trap them in unit tests by way of an example User Registration service:-
@Path("/") public class UserService { @PUT @Consumes({"application/json"}) @Path("/users") public boolean storeUserDetails(UserDetails userDetails) { System.out.println(userDetails); return true; } }
This service accepts a PUT of a UserDetails model object in a JSON payload:-
@XmlRootElement(namespace= "https://devsumo.com/examples/cxf/java/userservice") public class UserDetails { private String userName = null; private String fullName = null; // Public onstructor, getters, setters… }
This is enough to spin-up a CXF JAX-RS service with an auto-generated WADL that imports successfully into SoapUI. SoapUI in turn will generate template JSON requests matching the UserDetails object and PUT them to the service.
In the remainder of this post I’ll break the model to reproduce both types of problem and provide lightweight unit test cases to trap them.
The code for this post is available on GitHub.
One Small WADL
First, and easiest, we’ll modify the model objects in a way that violates JAXB’s rules. We’ll extend UserDetails to include a set of UserNote objects:-
@XmlRootElement(namespace= "https://devsumo.com/examples/cxf/java/userservice") public class UserDetails { private String userName = null; private String fullName = null; private Set<UserNote> notes = null; // Public constructor, getters, setters… }
And we’ll define UserNote in a way that breaks the JAXB context by having public properties with the same name as getter/setter based properties:-
public class UserNote { public String label = null; public String comment = null; // Public constructor, getters, setters… }
The WADL returned for this version of the service is a shadow of its former self, denuded of XML schema information:-
<application xmlns="http://wadl.dev.java.net/2009/02" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <doc title="User Management Service"/> <grammars/> <resources base="http://localhost:8080/wadl-tests/totallybroken"> <resource path="/" id="com.devsumo.examples...totallybroken.UserService"> ... </resource> </resources> </application>
On the server side we also get a stack trace helpfully explaining why:-
WARNING: No JAXB context can be created com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 2 counts of IllegalAnnotationExceptions Class has two properties of the same name "comment" ... Class has two properties of the same name "label" ... at com.sun.xml.internal.bind.v2.runtime. IllegalAnnotationsException$Builder.check( IllegalAnnotationsException.java:91) at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl. getTypeInfoSet(JAXBContextImpl.java:445) at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl. <init>(JAXBContextImpl.java:277) ... at org.apache.cxf.jaxrs.model.wadl.WadlGenerator. generateWADL(WadlGenerator.java:285) at org.apache.cxf.jaxrs.model.wadl.WadlGenerator.doFilter( WadlGenerator.java:238) ...
The stack trace also suggests a unit-test solution; simply generate a JAXB context for our model objects in a unit test case:-
@Test public void testCanCreateJAXBContext() { try { JAXBContext.newInstance( "com.devsumo.examples.cxf.java.userservice.totallybroken"); } catch(JAXBException eJaxb) { fail("Unable to create a JAXB context for the value objects: " + eJaxb.toString()); } }
This is a reasonable, quick and simple approach but has the drawback of requiring developers to keep the jaxb.index files up-to-date with a list of all relevant objects, and also keep the package list in the test-case up-to-date too. Remember we’re not using JAXB elsewhere in this service so nothing is going to make them do this; it isn’t therefore an awful lot more reliable than expecting them to manually test the WADL when making code changes.
An alternative which doesn’t depend on developer diligence is to request the complete WADL from within a unit test case and check for the presence of XML schema elements in its <grammar/> section-
@Test public void testWadlHasGrammars() throws Exception { // Determine the HTTP URL of our service WADL final String wadlUrl = "http://localhost:" + BeanUtils.getProperty(ctx.getBean(serviceBeanName), "server.destination.engine.connector.localPort") + "totallybroken/?_wadl"; // Request and parse the WADL into a DOM model DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); Document wadlDoc = docBuilder.parse(new URL(wadlUrl).openStream()); // Locate the XML grammar schemas via XPath XPath grammarSchemaXPath = XPathFactory.newInstance().newXPath(); NodeList grammarSchemas = (NodeList)grammarSchemaXPath.evaluate( "/application/grammars/schema", wadlDoc.getDocumentElement(), XPathConstants.NODESET); // Assert that we have at least 1 grammar schema present assertTrue("No grammar schema located in service WADL", grammarSchemas.getLength() > 0); }
This test case presumes an embedded instance of the service has been launched in a Jetty container as part of the test run. To make it Continuous Integration Server Safe it has been started on port 0 rather than a hard-coded port, so the first thing we do is find the actual port being used in order to build the full HTTP URL of the WADL.
We then simply feed that WADL into a DOM parser, use XPath to generate a list of <schema/> nodes in the grammar section and fail the test if the count is 0.
Either test case fails with the above, invalid model object and making the fields private gets both tests to pass:-
public class UserNote { private String label = null; private String comment = null; // Public constructor, getters, setters… }
Something’s gotten hold of my WADL
Now for the slightly trickier case, an essentially valid JAXB model that nonetheless yields an invalid WADL.
Let’s change our UserNote object and make it an XML root element within the same namespace:-
@XmlRootElement(namespace= "https://devsumo.com/examples/cxf/java/userservice") public class UserNote { private String label = null; private String comment = null; // Public constructor, getters, setters… }
Our JAXB tests still pass, but if we try to feed the WADL for this version of the service into SoapUI we get a stream of error messages in the log and the nifty “recreate a default representation from the schema” button on the request dialog is disabled.
ERROR:An error occurred [http://localhost:8080/wadl-tests/slightlybroken/?_wadl:0: error: src-resolve.a: Could not find type 'userNote@http://wadl.dev.java.net/2009/02'. Do you mean to refer to the element named userNote@https://devsumo.com/examples/cxf/java/userservice (in _3F_5Fwadl)?], see error log for details ERROR:An error occurred [com.eviware.soapui.impl.wsdl.support.xsd. SchemaException], see error log for details ERROR:Error loading schema types from http://localhost:8080/wadl-tests/slightlybroken/?_wadl, see log for details ERROR:An error occurred [com.eviware.soapui.impl.support.definition. support.InvalidDefinitionException], see error log for details
Comparing the WADL with the version generated without the additional @XmlRootElement annotation shows a small but subtle difference in how the generated schema are cross-referenced.
<grammars> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns1="https://devsumo.com/examples/cxf/java/userservice" attributeFormDefault="unqualified" elementFormDefault="unqualified" targetNamespace="https://devsumo.com/examples/cxf/java/userservice"> <xs:import namespace="https://devsumo.com/examples/cxf/java/userservice"/> <xs:complexType name="userDetails"> … </xs:complexType> <xs:complexType name="userNote"> … </xs:complexType> </xs:schema> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="https://devsumo.com/examples/cxf/java/userservice" attributeFormDefault="unqualified" elementFormDefault="unqualified" targetNamespace="https://devsumo.com/examples/cxf/java/userservice"> <xs:import/> <xs:element name="userDetails" type="userDetails"/> <xs:element name="userNote" type="userNote"/> </xs:schema> </grammars>
A good test here isn’t one that digs into this specific scenario, but one that asserts that the overall WADL is valid. The easiest way for us to do this, as test developers, would be to feed the WADL into a dynamic client like SoapUI and at a minimum verify that no errors occurred when it was imported.
My search for easily embeddable Java WADL consumers did not turn up too many options, however the Wadl2Java class underpinning the Glassfish wadl2java Maven plugin is pretty painless to drive from a unit test.
We just need to add it to our project POM:-
<dependency> <groupId>org.jvnet.ws.wadl</groupId> <artifactId>wadl-client-plugin</artifactId> <version>1.1.6</version> <scope>test</scope> </dependency>
And then we can script it in a unit test case:-
@Test public void testWadlCanGenerate() throws Exception { // Determine the HTTP URL of our service WADL final String wadlUrl = "http://localhost:" + BeanUtils.getProperty(ctx.getBean(serviceBeanName), "server.destination.engine.connector.localPort") + "slightlybroken/?_wadl"; // Create a scratch directory to generate the wadl2java client in final String clientTargetDirectoryName = "target/test-client"; final File clientTargetDirectory = new File(clientTargetDirectoryName); clientTargetDirectory.mkdirs(); // Define a message listener to collect events from wadl2java. final List<String> errorMessages = new ArrayList<>(); final MessageListener wadl2JavaMessageListener = new MessageListener() { @Override public void warning(String message, Throwable throwable) {} @Override public void info(String message) {} @Override public void error(String message, Throwable throwable) { System.out.println(message + ": " + throwable.toString()); errorMessages.add(message); } }; // Configure the wadl2java parameters Parameters wadl2JavaParams = new Parameters(); wadl2JavaParams.setRootDir(new URI( "file:" + clientTargetDirectoryName)); wadl2JavaParams.setCustomizations(Collections.emptyList()); wadl2JavaParams.setMessageListener(wadl2JavaMessageListener); wadl2JavaParams.setXjcArguments(Collections.emptyList()); wadl2JavaParams.setPkg("test"); wadl2JavaParams.setCodeWriter( new FileCodeWriter(clientTargetDirectory)); // Execute wadl2java new Wadl2Java(wadl2JavaParams).process(new URI(wadlUrl)); // Assert that no errors occurred assertTrue("Unable to generate a client for the service WADL", errorMessages.isEmpty()); }
This test case configures the parameters for a Wadl2Java run using a scratch directory in our Maven build’s target/ folder to output the generated Java source code. The key is the MessageListener implementation which adds any encountered errors to a collection which we can assert at the end must be empty for a run to be considered successful.
Don’t go breaking my WADL
Unfortunately a WADL with no grammar information is still technically valid so this latter test won’t trap the former issue. To completely cover both error scenarios we need both the wadl2java based test and one of the JAXB model tests.
These are not exhaustive by any means, but then they aren’t meant to be. They provide a lightweight pair of test cases that can be added to the unit tests for a JAX-RS CXF service that will trap most (if probably not all) cases of developers making code changes that break the auto-generated WADL.