Sunday, July 29, 2012

Spring MVC WADL generation


As in other REST based frameworks in java, most of them provide an out of box support for genreation of WADL file ( web application description language). It's an XML file that is being composed of the description of all the resources that your REST based API is going to expose. This blog is a collection of the codes , that is being used in order to generate WADL through Spring. (Note: spring MVC does not provide an inbuilt way of generating this file and does not implements JSR-311 fully, so we can expect some mismatch here and there), however other frameworks like JERSEY (which is a full fledged REST implementation) provides complete support for this.

In order to generate application.wadl, we must have to understand the structure of the WADL (http://www.w3.org/Submission/wadl/wadl.xsd). This XSD contains a list of all XML elements and attributes that can be present within an WADL file).

There are simply two steps to achieve the generation:
Step 1: Via using the above XSD we have to generate  the classes, that will represent all the elements in the WADL XML file. The command is simple one. Just download the XSD onto your local machine and hit the command "xjc wadl.xsd" and you will get a number of java file in the working directory. In case you want to specify a specific package name for the generated java files, you can achieve this via a number of command line options provided by xjc. So for example i have generated the files in the following folder "com.mine.wadl.artifact" and here is a list of all the files present in that folder. ( I have renamed each file so that the name starts with WADL).
            1.


Step2: This step is all about writing a spring controller, that will map to the "application.wadl" path and will generate the XML. we have to make sure, that the JAXB marshall or any otther that we are using, must be onto the classpath as Spring will make use of it to generate the XML file. Here is the source code for generating this.
package com.mine.wadl.generator;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.xml.namespace.QName;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import com.mine.wadl.artifact.WadlApplication;
import com.mine.wadl.artifact.WadlDoc;
import com.mine.wadl.artifact.WadlMethod;
import com.mine.wadl.artifact.WadlParam;
import com.mine.wadl.artifact.WadlParamStyle;
import com.mine.wadl.artifact.WadlRepresentation;
import com.mine.wadl.artifact.WadlRequest;
import com.mine.wadl.artifact.WadlResource;
import com.mine.wadl.artifact.WadlResources;
import com.mine.wadl.artifact.WadlResponse;

/**
 * Type name:WadlController.java
 Description:  This Class
 * will be responsible for generation the Web application descriptor file based
 * upon the  
 References:
 * 
 * 

 * 
 * @author Pankaj Bhatt.
 * @version 1.0, June 2012
 */

@Controller
@RequestMapping
public class WadlController {

 // @Autowired
 private RequestMappingHandlerMapping handlerMapping;

 /**
  * Constructor for initializing the Wadl Controller
  * 
  * @param handlerMapping
  */
 @Autowired
 public WadlController(RequestMappingHandlerMapping handlerMapping) {
  this.handlerMapping = handlerMapping;
 }


 /**
  * This is a function which will be responsible for generating the WADL
  * file.
  * 
  * @param request : Represents the Request
  * @return WadlApplication : This object will be converted to the WADL File.
  */
 @RequestMapping(method = RequestMethod.GET, produces = { "application/xml" })
 public @ResponseBody WadlApplication generateWadl(HttpServletRequest request) {
  WadlApplication result = new WadlApplication();
  WadlDoc doc = new WadlDoc();
  doc.setTitle("REST Service WADL");
  result.getDoc().add(doc);
  WadlResources wadResources = new WadlResources();
  wadResources.setBase(getBaseUrl(request));

  Map handletMethods = handlerMapping
    .getHandlerMethods();
  for (Map.Entry entry : handletMethods
    .entrySet()) {
   WadlResource wadlResource = new WadlResource();

   HandlerMethod handlerMethod = entry.getValue();
   RequestMappingInfo mappingInfo = entry.getKey();

   Set pattern = mappingInfo.getPatternsCondition().getPatterns();
   Set httpMethods = mappingInfo.getMethodsCondition().getMethods();
   ProducesRequestCondition producesRequestCondition = mappingInfo
     .getProducesCondition();
   Set mediaTypes = producesRequestCondition
     .getProducibleMediaTypes();

   for (RequestMethod httpMethod : httpMethods) {
    WadlMethod wadlMethod = new WadlMethod();

    for (String uri : pattern) {
     wadlResource.setPath(uri);
    }

    wadlMethod.setName(httpMethod.name());
    Method javaMethod = handlerMethod.getMethod();
    wadlMethod.setId(javaMethod.getName());
    WadlDoc wadlDocMethod = new WadlDoc();
    wadlDocMethod.setTitle(javaMethod.getDeclaringClass().getName()+ "." + javaMethod.getName());
    wadlMethod.getDoc().add(wadlDocMethod);

    // Request
    WadlRequest wadlRequest = new WadlRequest();

    Annotation[][] annotations = javaMethod.getParameterAnnotations();
    Class[] paramTypes = javaMethod.getParameterTypes();
    int parameterCounter = 0;

    for (Annotation[] annotation : annotations) {
     for (Annotation annotation2 : annotation) {
      if (annotation2 instanceof RequestParam) {
       RequestParam param2 = (RequestParam) annotation2;

       WadlParam waldParam = new WadlParam();

       waldParam.setName(param2.value());

       waldParam.setStyle(WadlParamStyle.QUERY);
       waldParam.setRequired(param2.required());

       if (paramTypes != null
         && paramTypes.length > parameterCounter) {
        if (paramTypes.length > parameterCounter
          && (paramTypes[parameterCounter] == javax.servlet.http.HttpServletRequest.class || paramTypes[parameterCounter] == javax.servlet.http.HttpServletResponse.class))
         parameterCounter++;
        if (paramTypes.length > parameterCounter
          && (paramTypes[parameterCounter] == javax.servlet.http.HttpServletRequest.class || paramTypes[parameterCounter] == javax.servlet.http.HttpServletResponse.class))
         parameterCounter++;

        if (paramTypes.length > parameterCounter) {

         waldParam
           .setType(getQNameForType(paramTypes[parameterCounter]));
         parameterCounter++;
        }
       }

       String defaultValue = cleanDefault(param2
         .defaultValue());
       if (!defaultValue.equals("")) {
        waldParam.setDefault(defaultValue);
       }
       wadlRequest.getParam().add(waldParam);
      } else if (annotation2 instanceof PathVariable) {
       PathVariable param2 = (PathVariable) annotation2;

       WadlParam waldParam = new WadlParam();
       waldParam.setName(param2.value());
       waldParam.setStyle(WadlParamStyle.TEMPLATE);
       waldParam.setRequired(true);
       if (paramTypes != null
         && paramTypes.length > parameterCounter) {
        if (paramTypes.length > parameterCounter
          && (paramTypes[parameterCounter] == javax.servlet.http.HttpServletRequest.class || paramTypes[parameterCounter] == javax.servlet.http.HttpServletResponse.class))
         parameterCounter++;
        if (paramTypes.length > parameterCounter
          && (paramTypes[parameterCounter] == javax.servlet.http.HttpServletRequest.class || paramTypes[parameterCounter] == javax.servlet.http.HttpServletResponse.class))
         parameterCounter++;

        if (paramTypes.length > parameterCounter) {

         waldParam
           .setType(getQNameForType(paramTypes[parameterCounter]));
         parameterCounter++;
        }
       }

       wadlRequest.getParam().add(waldParam);
      } else
       parameterCounter++;
     }
    }
    if (!wadlRequest.getParam().isEmpty()) {
     wadlMethod.setRequest(wadlRequest);
    }

    // Response
    if (!mediaTypes.isEmpty()) {
     WadlResponse wadlResponse = new WadlResponse();
     wadlResponse.getStatus().add(200l);
     for (MediaType mediaType : mediaTypes) {
      WadlRepresentation wadlRepresentation = new WadlRepresentation();
      wadlRepresentation.setMediaType(mediaType.toString());
      wadlResponse.getRepresentation()
        .add(wadlRepresentation);
     }
     wadlMethod.getResponse().add(wadlResponse);
    }

    wadlResource.getMethodOrResource().add(wadlMethod);

   }

   wadResources.getResource().add(wadlResource);

  }
  result.getResources().add(wadResources);

  return result;
 }

 private String getBaseUrl(HttpServletRequest request) {

  return request.getScheme() + "://" + request.getServerName() + ":"
    + request.getServerPort() + "" + request.getContextPath() + "/"
    + request.getServletPath().substring(1);
 }

 private String cleanDefault(String value) {
  value = value.replaceAll("\t", "");
  value = value.replaceAll("\n", "");
  value = value.replaceAll("?", "");
  value = value.replaceAll("?", "");
  value = value.replaceAll("?", "");
  return value;
 }

/**
  * This is an private function, which will return the QName based upon the
  * Java Type.
  * 
  * @param classType
  *            : Represent the type of class
  * @return QName
  */
  private QName getQNameForType(Class classType) {
  QName qName = null;

  /**
   * Check whether the thing that is coming is an Array of a data type or
   * not.
   */
  if (classType.isArray()) {
   classType = classType.getComponentType();
  }

  if (classType == java.lang.Long.class)
   qName = new QName("http://www.w3.org/2001/XMLSchema", "long");
  else if (classType == java.lang.Integer.class)
   qName = new QName("http://www.w3.org/2001/XMLSchema", "integer");
  else if (classType == java.lang.Double.class)
   qName = new QName("http://www.w3.org/2001/XMLSchema", "double");
  else if (classType == java.lang.String.class)
   qName = new QName("http://www.w3.org/2001/XMLSchema", "string");
  else if (classType == java.util.Date.class)
   qName = new QName("http://www.w3.org/2001/XMLSchema", "date");

  return qName;
 }

}

I know this is a long stuff, but let me go one by one & line by line( I will only explain those stuff, which will help you to customize your implementation).

Line 61-64: This is the most important part of the generation, as it initialized the contains the initialization of RequestMappingHandlerMapping object, which is present within spring and contains all the details of all the URI's that we have exposed through Spring MVC. In addtion, to it, it also contains details of the methods that have those Spring MVC Rest based annotations. later on we will see how we will make use of this to find out the information in which we are in terested in.
Line 74:
We are simply annotating a function so that it will be invoked once we type "http://blah.com/springserveletmapping/application.wadl".
Line 75 - 113: As you can see here we are creating the foundationg for generating XML and invoking functions of requ>estmappinghandlermapping to find out the set of functions which have the Spring MVC Rest based annotations. We are also looking for the media types that are being supported by the functions, ( if any present in the defintions of the functions). This is continued till line no 113.
Line 114: This is the section, in which we are being interested in, Here as we know, every function that ismapped to some URI via spring MVC , can have any type of parameters.
E.g. public DataToBeReturned getLoginData(@PathVariable int id, HttpServeltRequest req, @RequestParam(value="name" , required=true) String userName)
However, in the WADL we only want those parameters to be listed which we are collecting from the URI e.g. either from header, requst parameters or through path variables, any other parameters beyond them is need not be included in the WADL. So here we are removing the inclusing of HttpServletRequest and HttpServletResponse from inclusion in WADL.
     Based on the type of annotation on the parameter it will be included either as a path variable or request parameter (type QUERY). For all @RequestParameter it is mandatory to include (value and required attribute) otherwise we wont be able to include the corressponding information in the WADL. value attribute reflects the name (e.g what is the request parameter name) and required  tells us whether that parameter is necessary for processing for the request or not, otherwise you will bound to get a BAD Request ( Http 400 error code)
.


Line 212: The function at this line, will help us in calculating the Base URI on which all the resources are being mapped. This has to be modified as per your own requirements.
Line 236: Here we are specifying a function, that will return the type of QName for the type of parameters that we are passing in the function that is mapped to an URI. since here, I am only using Long,Integer,Double,String,Date. In, case you need to add more, please change this function to include the type of parameter of your choice).

And that's all. Once you will hit at /application.wadl you will get the XML File, mentioning your resources. I have tested the file consumption by SOAP UI and it all blends well.

Here i have specifically used Spring 3.1.0.Release, however i will suggest to go for 3.1.1Release as it has some nice little improvements.

At the last, I am thankful to lot to tomasz nurkiewicz & GrĂ©gory OLIVER, it is only because of their direction and help with code, I am able to do this. So all the appreciation goes to them directly. Thanks tomasz and gregory.

here are some of the links, that you may find useful.




Here is Tomasz GitHub url for this project : https://github.com/nurkiewicz/spring-rest-wadl
Hope, it helps the developer community. 
If get time, or if there is a need i will upload the maven pom for this project. 

Thanks. 


   

4 comments:

  1. Hi.. Very good post
    It resolved half of my problems :-)
    Have you got any chance to extend it for POST requests which have @RequestBody as object parameters.
    I have lot of webservices which take Custom object as input and give different custom object as Response Body.
    Any clue or other code to use ?
    Thanks
    Sourbah

    ReplyDelete
    Replies
    1. Hi Sourabh,
      Can you please let me know, wat you want to do this those custom @RequestBody objects. I have a number of them, but i dint exposed it to REST WADL, as it has already been defined in my API Design guide. So ii will just output the fully qualified classname of the @RequestBody objects class to the XML.

      Delete
  2. Instead of going through all the stuffs. WADL for spring mvc rest service can be generated using SOAP UI.
    I managed to create one project using soap ui for my rest service and then exported wadl file form the project.
    Steps are here:
    1) click on the project name.
    2) click on the first element under root project.
    3) Here you will see one widget opened having three tabs Overview, Service endpoint and WADL content.
    4) click on WADL content tab.
    When opened WADL content tab, you will see two panels. In left panel you will see all detail about service like resources, methods and representation and in right side panel you will find the generated WADL content.

    ReplyDelete