2012. 8. 24. 10:15 IT

[purpose]
- RESTful API Documentation 

- Using Spring framework 

- autogenerated


[Review solutions]
 SPRINGDOCLET : javadoc style로 rest api user document 로는 부족함

RESTDOCLET : 목적에 부합
maven plugin, web application type
param description not support, rest error code not support
controller에 정의한 mapping 미인식

WSDOC : simple 충분하지않음 rest error, param description nor support

SWAGGER : spring 지원 불충분, 복잡...

[try - restdoclet]
- fail maven setting
- use library
- process : java source -> xml using xmldoc -> xml using jibx -> html using template
- preperation
spring restful application source & libraries
library : jibx, spring, log4j, commons, restdoc, tools.jar, ant

retdoclet lib : restdoclet-doclet-2.2.0.jar, restdoclet-plugin-2.2.0.jar, jibx-run-1.2.1.jar, from https://oss.sonatype.org/index.html#nexus-search;classname~RESTdoclet

spring lib : spring-web-3.0.5.RELEASE.jar, spring-context-3.0.5.RELEASE.jar, commons-lang-2.6.jar,

javadoc lib : tools.jar, 

ant lib : ant-launcher.jar, ant.jar

etc : commons-collections-3.2.jar,  log4j-1.2.15.jar, 

 

- setting
srcpath
version
excludes method
reportdir

demo sample.

import groovy.text.SimpleTemplateEngine

import org.apache.commons.collections.CollectionUtils

import org.jibx.runtime.JiBXException

import com.iggroup.oss.restdoclet.doclet.XmlDoclet

import com.iggroup.oss.restdoclet.doclet.type.*

import com.iggroup.oss.restdoclet.doclet.util.*

import com.iggroup.oss.restdoclet.plugin.io.*

import com.iggroup.oss.restdoclet.plugin.util.ServiceUtils

import com.sun.tools.javac.util.*

import com.sun.tools.javadoc.*

class MySample {

static def SERVICES_TEMPLATE = "src/main/resources/services.jsp";

static def SERVICE_TEMPLATE = "src/main/resources/service.jsp";

def reportDir;

def restsrcDir;

def packageNames = []

def appname = ""

def version = 1.0

static main(args) {

def me = new MySample();

me.restsrcDir = "../my-rest-web/src/main/java"

me.reportDir = "../my-rest-web/doc/restdoc"

me.packageNames = {"com.simple.rest"}

me.appname =  ["APPLICATION":"my-rest" ]

me.version = ["version":1.0, timestamp:new Date()]

me.setup()

me.makexmldoc()

me.build();

me.clean()

}

def ant = new AntBuilder()

def controllers = []// ArrayList<Controller> 

List<String> excludes = ["exceptionHandler"];

def outputDirectory = "my-rest";

def setup(){

clean();

ant.delete(dir:reportDir, quiet:true, verbose:false)

ant.mkdir(dir:reportDir);

}

def clean(){

ant.delete(dir:"com", quiet:true, verbose:false);

ant.delete(dir:"target/restdoc", quiet:true, verbose:false);

}

// gathering Controller.java files ( except Sample)

def getRestControllers(){

def list = []

new File(restsrcDir).eachFileRecurse {

if(it.name.endsWith("Controller.java")){

list.add(it)

}

}

return list;

}

def makexmldoc(){

Context context = new Context();

Options compOpts = Options.instance(context);

compOpts.put("-sourcepath", new File(restsrcDir).absolutePath);

compOpts.put("-classpath", getClassLibs());

ListBuffer<String> javaNames = new ListBuffer<String>();

for (File fileName : getRestControllers()) {

javaNames.append(fileName.getPath());

}

ListBuffer<String> subPackages = new ListBuffer<String>();

for (String packageName : packageNames) {

log.info("Adding sub-packages to documentation path: " + packageName);

subPackages.append(packageName);

}

new Messager(context,"application")

JavadocTool javadocTool = JavadocTool.make0(context);

def bQuiet = true

def rootDoc = javadocTool.getRootDocImpl(

                    "", "utf-8", new ModifierFilter(ModifierFilter.ALL_ACCESS),

                    javaNames.toList(), new ListBuffer<String[]>().toList(), false,

                    subPackages.toList(), new ListBuffer<String>().toList(),

                    false, false, bQuiet);

rootDoc.env.silent = true; // clear javadoc warning message

new XmlDoclet().start(rootDoc);

def myDir = "target/restdoc"

ant.mkdir(dir:myDir)

new ControllerUriAppender().add(rootDoc, myDir);

}

def build()   {

def myDir = "target/restdoc"

   javadocs(myDir);

   services(myDir);

}

def javadocs(rootDir){

new File(rootDir).eachFileRecurse {

if(it.name.endsWith(Controller.FILE_SUFFIX)){

final Controller cntrl = JiBXUtils.unmarshallController(it);

if (!controllers.contains(cntrl)) {

  controllers.add(cntrl);

}

}

}

}

def services(baseDirectory)  {

  List<Service> services = new ArrayList<Service>();

  def uriMethodMappings = [:]

  HashMap<String, Controller> uriControllerMappings =

 new HashMap<String, Controller>();

  HashMap<String, Collection<Uri>> multiUriMappings =

 new HashMap<String, Collection<Uri>>();

  for (Controller controller : controllers) {

 for (Method method : controller.getMethods()) {

if (excludeMethod(method)) { continue; }

Collection<Uri> uris = method.getUris();

if (!uris.isEmpty()) {

String multiUri = uris.join(", ")

multiUriMappings.put(multiUri, uris);

ArrayList<Method> methodList = uriMethodMappings.get(multiUri);

if (methodList == null) {

  methodList = new ArrayList<Method>();

  uriMethodMappings.put(multiUri, methodList);

}

methodList.add(method);

uriControllerMappings.put(multiUri, controller);

}

 }

  }

  int identifier = 1;

  for (String uri : uriControllerMappings.keySet()) {

 Controller controller = uriControllerMappings.get(uri);

 ArrayList<Method> matches = uriMethodMappings.get(uri);

 Service service =

new Service(identifier, multiUriMappings.get(uri), new Controller(

controller.getType(), controller.getJavadoc(), matches));

 services.add(service);

 service.assertValid();

 generateServiceHtml( appname, version, service)

 identifier++;

  }

  Services list = new Services();

  for (Service service : services) {

 org.apache.commons.collections.Predicate predicate =

new ControllerTypePredicate(service.getController().getType());

 if (CollectionUtils.exists(list.getControllers(), predicate)) {

ControllerSummary controller = (ControllerSummary) CollectionUtils.find(list.getControllers(), predicate);

controller.addService(service);

 } else {

ControllerSummary controller = new ControllerSummary(service.getController().getType(), service.getController().getJavadoc());

controller.addService(service);

list.addController(controller);

 }

  }

  generateServiceListHtml(  appname, version, list.getServices())

}

def generateServiceListHtml(  param, props, svcs){

def binding = ["param" : param,  "props":props, "services":svcs]

def toFile = reportDir+"/services.html"

generateHtml(toFile, binding, SERVICES_TEMPLATE);

}

def generateServiceHtml(  param, props, svc){

def binding = ["param" : param,  "props":props, "service":svc]

def toFile = reportDir+"/service-" + svc.identifier  + ".html"

generateHtml(toFile, binding, SERVICE_TEMPLATE);

}

def generateHtml(def toFile, def binding, def templateFile, encoding = "UTF-8"){

def reader = new File(templateFile).newReader();

def template = new SimpleTemplateEngine().createTemplate(reader).make(binding);

new File(toFile).write(template.toString(), encoding)

}

private boolean excludeMethod(Method method) {

return excludes.contains(method.getName().equalsIgnoreCase())

}

// for javadoc compile

def getClassLibs(){

return [

"../my-rest/WEB-INF/lib/commons-logging.jar",

"../my-rest/WEB-INF/lib/spring-XXXX-X.X.X.RELEASE.jar"

// ...  other libraries

].join(";")

}

}


ControllerUriAppender ( for controller's uri appending)

import static com.iggroup.oss.restdoclet.doclet.util.AnnotationUtils.*

import static com.iggroup.oss.restdoclet.doclet.util.JiBXUtils.marshallController

import groovy.util.slurpersupport.GPathResult

import groovy.xml.*

import org.springframework.web.bind.annotation.RequestMapping

import com.iggroup.oss.restdoclet.doclet.type.Controller

import com.sun.javadoc.*

class ControllerUriAppender {

def add(RootDoc rootDoc, outputpath){

for (ClassDoc classDoc : rootDoc.classes()) {

if (isAnnotated(classDoc, org.springframework.stereotype.Controller.class)) {

  AnnotationValue value = elementValue( classDoc, RequestMapping.class, "value")

  def rooturi  = value.toString().replaceAll("\"","");

  def xmlname = classDoc.qualifiedName().replace('.' as char, File.separatorChar) + Controller.FILE_SUFFIX

  def root = new XmlSlurper().parse(new File(xmlname));

  if(value!=null){

  root.method.each{ mtd ->

  def mtdname = mtd.name.toString()

  if(mtdname.endsWith("Null")||mtdname.endsWith("Error")){ // remove null filter mapping

  mtd.replaceNode{}

  }else{

  if(mtd.uri==""){

  mtd.javadoc + { 

  uri {

  uri(rooturi)

  deprecated(false)

  type("java.lang.String")

  }

  }

  }else{

  mtd.uri.uri = rooturi + mtd.uri.uri

  }

  //// add parameter comment

  def mtddoc = classDoc.methods().find {

  it.name().equals(mtdname) 

  }

  mtd."request-param".each{ rqparam ->

  if(rqparam.javadoc==""){

  def paramdoc = mtddoc.paramTags().find{ paramtag ->

  paramtag.parameterName().equals(rqparam.name)

  }

  rqparam.type + {

  javadoc(paramdoc?.parameterComment() )

  }

  }

  }

  // add response-param exception javadoc

  mtd."response-param".each{ rpparam ->

  if(rpparam.name.equals("Exception")){

  def exceptiondoc = mtddoc.throwsTags().find{ paramtag ->

  paramtag.exceptionName() .equals(rpparam.name.toString())

  }

  if(rpparam.javadoc==""){

  rpparam.type + {

  javadoc(exceptiondoc?.exceptionComment() )

  }

  }

  }

  }

  }

  }

  }

  def outputBuilder = new StreamingMarkupBuilder()

  outputBuilder.encoding ="UTF-8"

  def result = outputBuilder.bind{ 

  mkp.xmlDeclaration()

  mkp.yield root 

  }

  def ant = new AntBuilder()

  def myDir = outputpath + "/"+xmlname

  ant.touch(file:myDir, mkdirs:true, verbose:false)

  def fwriter = new FileWriter(myDir  );// .write(result);

  XmlUtil.serialize(result, fwriter)

  fwriter.close();

}

}

}

}

services.jsp ( template 1 )

<div class="projectDetails">

   <h1 class="mainHeading">

      <span class="title">RESTdoclet for:</span>

      <span class="application">

         <em><span class="application">${param["APPLICATION"]}</span></em>

      </span>

   </h1>

   <h3>

      <span class="version">Version: <em>${props.version}</em></span>

      <span class="timestamp">Creation: <em>${props.timestamp}</em></span>

   </h3>

</div>

<div class="services">

   <table class="topLevel">

      <thead>

      <tr>

         <th>URI</th>

         <th>Actions</th>

      </tr>

      </thead>

      <tbody><% services.each{ service -> %>

            <tr>

               <td class="uri"><%  service.uris.each{ urix -> %>

                     <div class="active">

                        <a href="service-${service.identifier}.html">${urix.uri}</a>

                     </div><% } %></td>

               <td>

                  <table class="methods">

                     <% service.methods.each{ method-> %>

                        <tr>

                           <td class="requestMethod">${method.requestMethod}</td class="javadoc">

                           <td class="javadoc">${method.javadoc}</td>

                        </tr><% } %>

                  </table>

               </td>

            </tr>

      <% }  %>

      </tbody>

   </table>

</div>

service.jsp ( template 2 )

<div class="projectDetails">

   <h1 class="mainHeading">

      <span class="title">REST for:</span>

      <em><a href="services.html">${param["APPLICATION"]}</a></em>

      <% service.uris.each{ urix -> %><span class="path"><em>${urix.uri}</em></span><% } %>

   </h1>

   <h3>

      <span class="version">Version: <em>${props.version}</em></span>

       <span class="timestamp">Creation: <em>${props.timestamp}</em></span>

   </h3>

</div>

<% service.controller.methods.each{ method-> %>

   <div class="method">

      <h3 class="httpMethod">${method.requestMethod}</h3>

      <p class="methodJsDoc">${method.javadoc}</p>

      <% if(method.pathParams.size()>0 || method.restParams.size() >0 || method.requestParams.size()>0 ){ %>

         <div class="input">

            <h3>Request input</h3>

            <table class="topLevel methodDetails">

               <thead><tr><th>Name</th><th>Description</th><th>Param Type</th></tr></thead>

               <tbody>

               <% method.pathParams.each{ parameter-> %><tr>

                     <td class="name">${parameter.type} ${parameter.name}</td>

                     <td class="javadoc">${parameter.javadoc}</td>

                     <td class="path">Path (Mandatory)</td>

                  </tr><% } %>

               <% method.restParams.each{ parameter-> %><tr>

                     <td class="name">${parameter.type} ${parameter.name}</td>

                     <td class="javadoc">${parameter.javadoc}</td><td class="path">REST</td>

                  </tr><% } %>

<% method.requestParams.each{ parameter-> %><tr>

                     <td class="name">${parameter.type} ${parameter.name}</td>

                     <td class="javadoc">${parameter.javadoc}</td>

                     <td class="path">Request<% if(parameter.required){ %>(Mandatory)<% }else{ %>(Optional)<% } %>

                        <% if(parameter.defaultValue){ %>(Default=${parameter.defaultValue})<% } %></td>

                  </tr><% } %>

               </tbody>

            </table>

         </div>

      <% } %>

<% if( method.responseParams.size()>0){ %>

         <div class="response">

            <h3>Response contents</h3>

            <table class="topLevel methodDetails">

               <thead><tr><th class="type">Response Type</th><th class="description">Description</th></tr></thead>

               <tbody>

               <% method.responseParams.each{ parameter-> %><tr>

                     <td class="type">${parameter.type}</td>

                     <td class="javadoc">${parameter.javadoc}</td>

                  </tr><% } %>

               </tbody>

            </table>

         </div>

      <% } %>

   </div>

<% } %>


ref) http://mestachs.wordpress.com/2012/08/06/rest-api-documentation/


posted by smplnote