Wednesday, July 13, 2011

Auttomatic XML Schema generation for Jersey WADLs

I have been doing a little bit of work recently on Jersey implementation and one of the precursors to this has been to try to get the WADL that Jersey will generation by default to contain just a little bit more information with regards to the data being transferred. This makes it possible to help the user when generating client code and running testing tools against resources.

To this end I have put together a WADL generator decorator that examines all the JAX-B classes used by the -RS application and generates a bunch of XML Schema files. This is now in 1.9-SNAPSHOT which you can download from the Jersey web site in the normal way. (If you want to use the JResponse part of this you will need a build after the 13th of July)

This feature is not enabled by default in 1.9; but hopefully with some good feedback and a small amount of caching I might convince the Jersey bods to make this the default. For the moment you need to create and register a WsdlGeneratorConfig class to get this to work. So your class might look like this:

package examples;

import com.sun.jersey.api.wadl.config.WadlGeneratorConfig;
import com.sun.jersey.api.wadl.config.WadlGeneratorDescription;
import com.sun.jersey.server.wadl.generators.WadlGeneratorJAXBGrammarGenerator;

import java.util.List;

public class SchemaGenConfig extends WadlGeneratorConfig {

    @Override
    public List<WadlGeneratorDescription> configure() {
        return generator( 
                WadlGeneratorJAXBGrammarGenerator.class   ).descriptions();
    }
}

You then need to make this part of the initialization of the Jersey servlet, so your web.xml might looks like this:

<?xml version = '1.0' encoding = 'windows-1252'?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">
  <servlet>
    <servlet-name>jersey</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>com.sun.jersey.config.property.WadlGeneratorConfig</param-name>
      <param-value>examples.SchemaGenConfig</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>jersey</servlet-name>
    <url-pattern>/jersey/*</url-pattern>
  </servlet-mapping>
</web-app>

For the purposes of this blog I am just going to show the three basic references to entities that the code supports. So at the moment if will obviously process classes that are directly referenced and either a return value or as the entity parameter on a method. The code also supports the Jersey specific class JResponse, which is a subclass of Response that can have a generic parameter. (Hopefully this oversight will be fixed in JAX-RS 2.0)

package examples;

import com.sun.jersey.api.JReponse;

import examples.types.IndirectReturn;
import examples.types.SimpleParam;
import examples.types.SimpleReturn;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/root")
public class RootResource {
    
    @GET
    @Produces("application/simple+xml")
    public SimpleReturn get() {
        return new SimpleReturn();
    }
    
    @GET
    @Produces("application/indrect+xml")
    public JResponse<IndirectReturn> getIndirect() {
        return JResponse.ok(new IndirectReturn() )
            .type( "application/indrect+xml" ).build();
    }
    
    @PUT
    @Consumes("application/simple+xml")
    public void put(SimpleParam param) {
        
    }
        
}

The type classes are all pretty trivial so I won't show them here. The only important factor is that they have the @XmlRootElement annotation on them. Although not shown here you can also use the JAX-B annotation @XmlSeeAlso on the resource classes to reference other classes that are not directly or indirectly referenced from the resource files. The most common use case for this is when you have a subtype of a class.

So enough of the java code, lets see what the WADL that is generated looks like:

<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://wadl.dev.java.net/2009/02">
    <doc xmlns:jersey="http://jersey.java.net/" jersey:generatedBy="Jersey: 1.9-SNAPSHOT 07/13/2011 11:13 AM"/>
    <grammars>
        <include href="application.wadl/xsd0.xsd">
            <doc xml:lang="en" title="Generated"/>
        </include>
    </grammars>
    <resources base="http://localhost:7101/JerseySchemaGen-Examples-context-root/jersey/">
        <resource path="/root">
            <method name="GET" id="get">
                <response>
                    <representation xmlns:ns2="urn:example"
                        mediaType="application/simple+xml" element="ns2:simpleReturn"/>
                </response>
            </method>
            <method name="PUT" id="put">
                <request>
                    <representation xmlns:ns2="urn:example"
                        mediaType="application/simple+xml" element="ns2:simpleParam"/>
                </request>
            </method>
            <method name="GET" id="getIndirect">
                <response>
                    <representation xmlns:ns2="urn:example"
                        mediaType="application/indrect+xml" element="ns2:indirectReturn"/>
                </response>
            </method>
        </resource>
    </resources>
</application>

In this example there is only one schema in the grammar section; but the code supports multiple schemas being generated with references between them. Let look at the schema for this example, note I did say the classes were trivial!

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema version="1.0" targetNamespace="urn:example"
    xmlns:tns="urn:example" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="indirectReturn" type="tns:indirectReturn"/>
    <xs:element name="simpleParam" type="tns:simpleParam"/>
    <xs:element name="simpleReturn" type="tns:simpleReturn"/>
    <xs:complexType name="indirectReturn">
        <xs:sequence/>
    </xs:complexType>
    <xs:complexType name="simpleReturn">
        <xs:sequence/>
    </xs:complexType>
    <xs:complexType name="simpleParam">
        <xs:sequence/>
    </xs:complexType>
</xs:schema>

There is still some internal work to be done on caching; but the basics are in place. Feedback would be appreciated, particularly in cases where the code doesn't see the referenced classes. Finally thanks to Pavel Bucek for being patient as I learned the ropes.

Update 5th September 2011 This feature has been enabled by default so you no longer have to perform any of the WadlGeneration configuration when working with 1.9 final release of Jersey.

13 comments:

Christopher Piggott said...

This is good work. I don't want to comment extensively until I play with it a little, but I like the concept a lot. I would really like to see richer automatically generated WADLs, and I'm unhappy with both the idea of having external XML descriptor files, or javadoc annotations. My opinion is that there's good cause for a richer set of annotations in the resource classes and methods themselves that give the wadl generator the best hints possible as to the author's intention.

Keep up the good work.

--Chris

Gerard Davison said...

Interesting you should say that, I found and old tranaction from Marc Hadley that did work along that lines. I wil try it dig it out and see if it is stil useful.....

Thanks for your kind words!

Gerard

Reinhard Pötz said...

Thanks! AFAICT it doesn't work with the Jersey Maven plugin. Maybe you find some time to review http://java.net/jira/browse/JERSEY-756

Gerard Davison said...

Reinhard,

Thanks for your patch, I will take a look over the next few days. Hopefully we can get this into 1.9. Just need to write some unit tests first.

Gerard

Ilias Tsagklis said...

Hey Gerard,

Nice blog! Is there an email address I can contact you in private?

Gerard Davison said...

Ilias Tsagklis,

Oracle email addresses tend to be firstname.surname@oracle.com so you can guess mind as gerard.davison@oracle.com.

Gerard

oggie said...

do you have a complete source code example for getting this to work?

We'd love to be able to generate the xsd from the POJOs and have the xsd available in the wadl.

I don't want to have to have the xsd in a context folder. Just more management I'd rather not have.

Gerard Davison said...

Oggie,

This has been the default for a little while in Jersey 1.x and now 2.x. You need only have @XmlRootElement on the POJO classes for the XML Schema to be rendered. Is this not working for you?

Gerard

Unknown said...

How do you rename the generated XSD file (xsd0.xsd) from the WADL? It is more user-friendly for the WADL to generate a name you want.

Gerard Davison said...

Colin,

You know that is a good idea, can you raise a Jersey bug for this? Perhaps even if we cannot control the name we can have a better default.

Gerard

davidNet said...

I've testing this and in a project were I'm using JAXB generated pojos, this class needs to consider XmlType.class also:

if (
(clazz.getAnnotation(XmlRootElement.class) != null)
|| (clazz.getAnnotation(XmlType.class) != null)
){
classSet.add(clazz);


Just if anybodyels needs it...

davidNet said...

Thanks a lot for your effort...

Just for those like me using JAXB generated classes from external jar as POJOs, this class needs to take care also of XmlType.class


if (
(clazz.getAnnotation(XmlRootElement.class) != null)
|| (clazz.getAnnotation(XmlType.class) != null)
){
classSet.add(clazz);

Gerard Davison said...

David,

Can you raise a bug on the Jersey project for this - and I will take a look. @XmlType is problematic because it doesn't have enough information to tell you what the element in the xml should be called.

There might be something I can do in your usecase.

Gerard