[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/