miercuri, 8 ianuarie 2014

HTML documentation for your REST services

Hello All

New year new me thing :).  I decided to start sharing some of the more interesting things I am doing so that they remain somewhere, plus that I could use them myself afterwards if the need arises.

So, here it goes.

I was looking into a way to document the REST services these days.   While there are some worthy tools such as enunciate, swagger and others, I found a more simple and effective way to do it.


We're using Jersey ATM, and this post refers to configuring Jersey in this regard.

In short the trick is to extend the generated WADL to contain the documentation (javadoc) taken from your service java classes.

Here's how it goes in a few short steps:

1. Document your services and request/responses (javadoc style)
EX:
    /**
     * Use this method to subscribe fx market rates from the price feed.
     * @param tradingBranchId branch ID
     * @param userId branch ID 2
     * @param customerId 33
     * @response.representation.200.qname {http://www.example.com}item
     * @response.representation.200.mediaType application/xml
     * @response.representation.200.doc This is the representation returned by default (if we have an even number of millis since 1970...:)
     * @response.representation.503.mediaType text/plain
     * @response.representation.503.example You'll get some explanation why this service is not available
     * @response.representation.503.doc You'll get some explanation why this service is not available - return
     * @return a JAX-RS Response containing the pricing information
     */
    @GET
    @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
    @Produces({ MediaType.TEXT_HTML, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
    @Path("/subscribe/branches/{trading-branch-id}/cust/{cust-id}/user/{user-id}/")
    public Response subscribeFXLimitOrder(
                    @PathParam("trading-branch-id") String tradingBranchId,
                    @PathParam("userId") String userId,
                    @PathParam("cust-id") String customerId,
                    @QueryParam("channel-id") String channelId,
                    @Context Request request,
                    @Context UriInfo uriInfo)
    { ...

2. Use the Jersey build doclet features to generate documentation file.
EX:
 <target name="generateRestDoc" description="Generates a resource desc file with the javadoc from resource classes">
  <antelope:call target="createPathReferences" />
  <javadoc classpathref="lbbw.service_api.alldeps.jars.path.id">
      <fileset dir="${lbbw.service_api.rest.dir}/src/java" defaultexcludes="yes">
          <include name="com/ibm/vd/otr/tradingapi/rest/resources/**" />
      </fileset>
      <doclet name="org.glassfish.jersey.wadl.doclet.ResourceDoclet" pathref="lbbw.service_api.alldeps.jars.path.id">
          <param name="-output" value="${lbbw.service_api.rest.dir}/${class.dir}/resourcedoc.xml" />
      </doclet>
  </javadoc>
 </target>

3. Configure the Jersey framework to generate an extended WADL to include the documentation file generated at build time.  Details about Jersey config:
Create a custom Jersey custom WADL generator config class, then add it to Jersey in the same manner with the rest of the services.  Make sure you have the 3 XML files in the classpath (they are going to be used by the Jersey custom generator, further below). 

application-doc.xml example:
<applicationDocs targetNamespace="http://wadl.dev.java.net/2009/02">

    <doc xml:lang="en" title="The doc for your API">
        This is a paragraph that is added to the start of the generated application.wadl
    </doc>

    <doc xml:lang="de" title="Die Dokumentation fuer Ihre API">
        Dies in ein Abschnitt, der dem Beginn der generierte application.wadl hinzugefugt wird - in deutscher Sprache.
    </doc>
</applicationDocs>

application-grammars.xml example:
<grammars xmlns="http://wadl.dev.java.net/2009/02"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xi="http://www.w3.org/1999/XML/xinclude">
<include href="schema.xsd" />
</grammars>

resource-doc.xml is generated by the doclet ant task above.

Custom Jersey WADL generator class example:
public class DocWadlGeneratorConfig extends WadlGeneratorConfig
{

    @Override
    public List<WadlGeneratorDescription> configure()
    {
                List<WadlGeneratorDescription> someList = generator(WadlGeneratorApplicationDoc.class)
                        .prop("applicationDocsStream", "application-doc.xml")
                        .generator(WadlGeneratorGrammarsSupport.class)
                        .prop("grammarsStream", "application-grammars.xml")
                        .prop("overrideGrammars", true)
                        .generator(WadlGeneratorResourceDocSupport.class)
                        .prop("resourceDocStream", "resourcedoc.xml")
                        .descriptions();
        
                return someList;
    }
}

Add the config for this class to the Jersey servlet in the web.xml file:
<init-param>
   <param-name>jersey.config.server.wadl.generatorConfig</param-name>
   <param-value>com.ibm.vd.otr.tradingapi.rest.DocWadlGeneratorConfig</param-value>
</init-param>


At this point, if you deploy and hit: http://localhost:9080/myapp/application.xml, you will see an extended WADL containing the documentation taken from your REST service java files.

EX of WADL file:



4. In order to see a pretty HTML with documentation in addition to the extended WADL xml file, you need to do the following:
- Download an XSL styleseet transformation file, one example is this: https://github.com/ipcsystems/wadl-stylesheet

- Configure Jersey to apply this XSL to the extended WADL it generates, which was configured in the previous steps:
Create a Jersey WADL resource generator class, used to direct the browser to apply the new XSL transformation to the extended WADL file, by inserting the preprocessing directive <?xml-stylesheet type="text/xsl" href="statics/wadl.xsl"?> (note that I am using the application2.wadl url in order to have both WADL files, the one without pretty styles and the one with styles available at the same time):

@Produces({ "application/vnd.sun.wadl+xml", "application/xml" })
@Singleton
@Path("application2.wadl")
public final class WadlResource
{

    private WadlApplicationContext wadlContext;

    private Application application;

    private byte[] wadlXmlRepresentation;

    public WadlResource(@Context WadlApplicationContext wadlContext)
    {
        this.wadlContext = wadlContext;
    }

    @GET
    public synchronized Response getWadl(@Context UriInfo uriInfo)
    {
        if (wadlXmlRepresentation == null)
        {
            // String styleSheetUrl = uriInfo.getBaseUri().toString() + "wadl.xsl";
            this.application = wadlContext.getApplication(uriInfo).getApplication();
            try
            {
                Marshaller marshaller = wadlContext.getJAXBContext().createMarshaller();
                marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
                ByteArrayOutputStream os = new ByteArrayOutputStream();

                Writer writer = new OutputStreamWriter(os);
                writer.write("<?xml version='1.0'?>\n");
                writer.write("<?xml-stylesheet type=\"text/xsl\" href=\"statics/wadl.xsl\"?>\n");

                marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
                marshaller.marshal(application, writer);

                writer.flush();
                writer.close();
                wadlXmlRepresentation = os.toByteArray();
                os.close();
            }
            catch (Exception e)
            {
                e.printStackTrace();
                return Response.ok(application).build();
            }
        }

        return Response.ok(new ByteArrayInputStream(wadlXmlRepresentation)).build();
    }
}
You configure this class in Jersey in the same manner as the rest of your services.  Also make sure the XSL file you downloaded is accessible at the URL indicated in the WADL.  This is because the browser itself will apply the XSL transformation to the WADL, so it needs to locate the XSL file used for it, as directed by the XML WADL file).

Now if you deploy and hit http://localhost:9080/myapp/application2.xml you should see something like this (this is just a fragment):


5. Congrats! Now you have a way to expose in a nice way your REST API javadoc to others :)

2 comentarii:

  1. Hi, thanks for the post and it was really easy and helpful. One question, I have been trying with Jersey2.22, tomcat and ant. wadl generated successfully with some of the java doc but not detail. I see the same in your case as well that parameters descriptions are missing in html output. Even if I want to add multiple response for example, it added in the resource-doc.xml only the last one from javadoc.

    However, I am using jersey-wadl-doclet-2.0 for the resourceDoclet in build.xml.

    It would be really helpful if you could provide any reason for that.

    Finally, I have found the example link: https://github.com/ipcsystems/wadl-stylesheet html output has same as I want but I don,t know why the completeness of doc are missing from Javadoc in the resource-doc.xml

    RăspundețiȘtergere
    Răspunsuri
    1. Hello Azad, I haven't checked the latest versions of jersey, but I am guessing the support is not complete. You might be able to enhance it if you need it. I suspect the code changes won't be huge. Good luck :)

      Ștergere