diff --git a/pom.xml b/pom.xml index cc18a75..35c6dcf 100644 --- a/pom.xml +++ b/pom.xml @@ -1,190 +1,192 @@ - + - - 4.0.0 - Web Service Documentation Generator - Generates HTML documentation from Spring Web Services REST endpoints - - org.versly - versly-wsdoc - jar - 1.1.006 - http://github.com/versly/wsdoc - - - - mulesoft-repo - https://repository-master.mulesoft.org/nexus/content/repositories/public - - - - - - - org.springframework - spring-web - 3.2.13.RELEASE - - - - - javax.ws.rs - jsr311-api - 1.1.1 - - - - com.beust - jcommander - 1.4 - - - - - joda-time - joda-time - 2.2 - - - org.freemarker - freemarker - 2.3.16 - - - - org.apache.commons - commons-lang3 - 3.1 - - - - - javax.servlet - servlet-api - 2.5 - compile - - - com.fasterxml.jackson.core - jackson-core - 2.4.2 - - - com.fasterxml.jackson.core - jackson-annotations - 2.4.1 - - - com.fasterxml.jackson.datatype - jackson-datatype-joda - 2.4.2 - - - com.fasterxml.jackson.core - jackson-databind - 2.4.2 - - - org.testng - testng - 6.1.1 - test - - - com.fasterxml.jackson.module - jackson-module-jsonSchema - 2.4.2 - - - org.apache.commons - commons-lang3 - 3.3.2 - - - - - org.raml - raml-parser - 0.9-SNAPSHOT - test - - - org.yaml - snakeyaml - 1.13 - - - - - - - services - - - - src/main/resources - - META-INF/services/* - - - - - - - - - - - - src/main/resources - - META-INF/services/* - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - -proc:none - 1.6 - 1.6 - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - - test-jar - - - - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + 4.0.0 + Web Service Documentation Generator + Generates HTML documentation from Spring Web Services REST endpoints + + org.versly + versly-wsdoc + jar + 1.1.006 + http://github.com/versly/wsdoc + + + + mulesoft-repo + https://repository-master.mulesoft.org/nexus/content/repositories/public + + + + + + + org.springframework + spring-web + 3.2.13.RELEASE + + + + + javax.ws.rs + jsr311-api + 1.1.1 + + + + + javax + javaee-api + 7.0 + + + + com.beust + jcommander + 1.4 + + + + + joda-time + joda-time + 2.2 + + + org.freemarker + freemarker + 2.3.16 + + + + org.apache.commons + commons-lang3 + 3.1 + + + + + javax.servlet + servlet-api + 2.5 + compile + + + com.fasterxml.jackson.core + jackson-core + 2.4.2 + + + com.fasterxml.jackson.core + jackson-annotations + 2.4.1 + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + 2.4.2 + + + com.fasterxml.jackson.core + jackson-databind + 2.4.2 + + + org.testng + testng + 6.1.1 + test + + + com.fasterxml.jackson.module + jackson-module-jsonSchema + 2.4.2 + + + org.apache.commons + commons-lang3 + 3.3.2 + + + + + org.raml + raml-parser + 0.9-SNAPSHOT + test + + + org.yaml + snakeyaml + 1.13 + + + + + + + services + + + + src/main/resources + + META-INF/services/* + + + + + + + + + + + + src/main/resources + + META-INF/services/* + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + -proc:none + 1.6 + 1.6 + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + test-jar + + + + + + diff --git a/src/main/java/org/versly/rest/wsdoc/AnnotationProcessor.java b/src/main/java/org/versly/rest/wsdoc/AnnotationProcessor.java index 294df0a..8aaba39 100644 --- a/src/main/java/org/versly/rest/wsdoc/AnnotationProcessor.java +++ b/src/main/java/org/versly/rest/wsdoc/AnnotationProcessor.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.joda.JodaModule; import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper; + import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RequestMethod; @@ -33,10 +34,12 @@ import javax.lang.model.type.*; import javax.lang.model.util.AbstractTypeVisitor6; import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import javax.tools.FileObject; import javax.tools.StandardLocation; + import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -86,6 +89,8 @@ public boolean process(Set supportedAnnotations, RoundEnv Collection processedPackageNames = new LinkedHashSet(); processElements(roundEnvironment, processedPackageNames, new SpringMVCRestImplementationSupport()); processElements(roundEnvironment, processedPackageNames, new JaxRSRestImplementationSupport()); + processElements(roundEnvironment, processedPackageNames, new JavaEEWebsocketImplementationSupport()); + _docs.postProcess(); if (_docs.getApis().size() > 0) { @@ -229,10 +234,35 @@ private void processRequestMappingMethod(ExecutableElement executableElement, Re buildParameterData(executableElement, method, implementationSupport); // set response entity data information on method - buildResponseFormat(executableElement.getReturnType(), method); + buildResponseFormat(unwrapReturnType(executableElement.getReturnType()), method); } } } + + private TypeMirror unwrapReturnType(TypeMirror originalReturnType) { + if(originalReturnType.getKind() != TypeKind.DECLARED) { + return originalReturnType; + } + + DeclaredType declaredType = (DeclaredType)originalReturnType; + if(declaredType.getTypeArguments().size() == 0) { + return originalReturnType; + } + + TypeElement element = (TypeElement) declaredType.asElement(); + + // For Spring's Async Support + if("org.springframework.web.context.request.async.DeferredResult".equalsIgnoreCase(element.getQualifiedName().toString())) { + return declaredType.getTypeArguments().get(0); + } + + // For Spring's Async Support + if("java.util.concurrent.Callable".equalsIgnoreCase(element.getQualifiedName().toString())) { + return declaredType.getTypeArguments().get(0); + } + + return originalReturnType; + } private void buildParameterData(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc, RestImplementationSupport implementationSupport) { @@ -244,6 +274,7 @@ private void buildParameterData(ExecutableElement executableElement, RestDocumen // for each entry listed in this list. I expect that this might be the same for @RequestMapping.headers scanForSpringMVCMultipart(executableElement, doc); + scanForWebsocket(executableElement, doc); buildPathVariables(executableElement, doc, implementationSupport); buildUrlParameters(executableElement, doc, implementationSupport); buildPojoQueryParameters(executableElement, doc, implementationSupport); @@ -262,6 +293,16 @@ private void scanForSpringMVCMultipart(ExecutableElement executableElement, Rest } } } + + private void scanForWebsocket(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc) { + for (VariableElement var : executableElement.getParameters()) { + TypeMirror varType = var.asType(); + if (varType.toString().equalsIgnoreCase("org.atmosphere.cpr.AtmosphereResource")) { + doc.setWebsocket(true); + return; + } + } + } private void buildRequestBodies(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc, RestImplementationSupport implementationSupport) { @@ -553,10 +594,24 @@ public JsonType visitDeclared(DeclaredType declaredType, Void o) { primitive.setRestrictions(enumConstants); return primitive; } else { + JsonType mappedType = mapDeclaredType(declaredType, element); + if(mappedType != null) { + return mappedType; + } return buildType(declaredType, element); } } } + + private JsonType mapDeclaredType(DeclaredType declaredType, TypeElement element) { + + // built-in non-primitive types are typically serialized to string + if(element.getQualifiedName().toString().startsWith("java.")) { + return new JsonPrimitive(String.class.getName()); + } + + return null; + } private JsonType acceptOrRecurse(Void o, TypeMirror type) { return type instanceof DeclaredType ? recurseForJsonType((DeclaredType) type) : type.accept(this, o); @@ -655,8 +710,13 @@ private JsonType recurseForJsonType(DeclaredType type) { for (TypeMirror generic : type.getTypeArguments()) { if (generic instanceof DeclaredType) concreteTypes.add((DeclaredType) generic); - else + else if(generic.getKind() == TypeKind.ARRAY) { + ArrayType arrayType = (ArrayType) generic; + return new JsonArray(acceptOrRecurse(null, arrayType.getComponentType())); + } + else { concreteTypes.add(_typeArguments.get(((TypeVariable) generic).asElement().getSimpleName())); + } } _typeRecursionDetector.add(_type.toString()); Collection types = new HashSet(_typeRecursionDetector); @@ -736,7 +796,7 @@ public JsonType visitUnknown(TypeMirror typeMirror, Void o) { public interface RestImplementationSupport { Class getMappingAnnotationType(); - String[] getRequestPaths(ExecutableElement executableElement, TypeElement contextClass); + String[] getRequestPaths(ExecutableElement executableElement, TypeElement contextClass); String[] getRequestPaths(TypeElement cls); diff --git a/src/main/java/org/versly/rest/wsdoc/impl/JavaEEWebsocketImplementationSupport.java b/src/main/java/org/versly/rest/wsdoc/impl/JavaEEWebsocketImplementationSupport.java new file mode 100644 index 0000000..85d70c5 --- /dev/null +++ b/src/main/java/org/versly/rest/wsdoc/impl/JavaEEWebsocketImplementationSupport.java @@ -0,0 +1,87 @@ +package org.versly.rest.wsdoc.impl; + +import java.lang.annotation.Annotation; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.websocket.server.ServerEndpoint; + +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.versly.rest.wsdoc.AnnotationProcessor; + +public class JavaEEWebsocketImplementationSupport implements AnnotationProcessor.RestImplementationSupport { + + @Override + public Class getMappingAnnotationType() { + return RequestMapping.class; + } + + @Override + public String[] getRequestPaths(ExecutableElement executableElement, TypeElement contextClass) { + return new String[0]; + } + + @Override + public String[] getRequestPaths(TypeElement cls) { + ServerEndpoint clsAnno = cls.getAnnotation(ServerEndpoint.class); + return requestPathsForAnnotation(clsAnno); + } + + private String[] requestPathsForAnnotation(ServerEndpoint clsAnno) { + if (clsAnno == null) + return new String[0]; + else + return new String[]{ clsAnno.value() }; + } + + @Override + public String getRequestMethod(ExecutableElement executableElement, TypeElement contextClass) { + return "GET"; + } + + @Override + public String getPathVariable(VariableElement var) { + PathVariable pathVar = var.getAnnotation(PathVariable.class); + return pathVar == null ? null : pathVar.value(); + } + + @Override + public String getRequestParam(VariableElement var) { + RequestParam reqParam = var.getAnnotation(RequestParam.class); + return reqParam == null ? null : reqParam.value(); + } + + /** + * Return whether this variable is an un-annotated POJO. + * This catches the case where a model is bound to directly. We explicitly exclude spring classes so that we don't look at the + * magical spring bound parameters like Errors or ModelMap + */ + @Override + public String getPojoRequestParam(VariableElement var) { + if (getRequestParam(var) == null && getPathVariable(var) == null && !ignorePojo(var)) { + return ""; // No annotations to parse + } else { + return null; + } + + } + + private boolean ignorePojo(VariableElement var) + { + // Atmosphere Types + if(var.asType().toString().startsWith("org.atmosphere")) + { + return true; + } + + return false; + } + + @Override + public boolean isRequestBody(VariableElement var) { + return !var.asType().toString().equalsIgnoreCase("javax.websocket.Session"); + } +} \ No newline at end of file diff --git a/src/main/java/org/versly/rest/wsdoc/impl/JaxRSRestImplementationSupport.java b/src/main/java/org/versly/rest/wsdoc/impl/JaxRSRestImplementationSupport.java index ab1fcd8..4a1217b 100644 --- a/src/main/java/org/versly/rest/wsdoc/impl/JaxRSRestImplementationSupport.java +++ b/src/main/java/org/versly/rest/wsdoc/impl/JaxRSRestImplementationSupport.java @@ -4,6 +4,8 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import javax.lang.model.element.ExecutableElement; diff --git a/src/main/java/org/versly/rest/wsdoc/impl/RestDocumentation.java b/src/main/java/org/versly/rest/wsdoc/impl/RestDocumentation.java index 3982cbd..be7c240 100644 --- a/src/main/java/org/versly/rest/wsdoc/impl/RestDocumentation.java +++ b/src/main/java/org/versly/rest/wsdoc/impl/RestDocumentation.java @@ -286,6 +286,7 @@ public class Method implements Serializable { private JsonType _responseBody; private String _commentText; private boolean _isMultipartRequest; + private boolean _isWebsocket; private String _requestSchema; private String _responseSchema; private String _responseExample; @@ -453,6 +454,14 @@ public boolean isMultipartRequest() { public void setMultipartRequest(boolean multipart) { _isMultipartRequest = multipart; } + + public boolean isWebsocket() { + return _isWebsocket; + } + + public void setWebsocket(boolean websocket) { + _isWebsocket = websocket; + } /** * An HTML-safe, textual key that uniquely identifies this endpoint. diff --git a/src/main/java/org/versly/rest/wsdoc/impl/SpringMVCRestImplementationSupport.java b/src/main/java/org/versly/rest/wsdoc/impl/SpringMVCRestImplementationSupport.java index eda410d..5400a9c 100644 --- a/src/main/java/org/versly/rest/wsdoc/impl/SpringMVCRestImplementationSupport.java +++ b/src/main/java/org/versly/rest/wsdoc/impl/SpringMVCRestImplementationSupport.java @@ -1,16 +1,19 @@ package org.versly.rest.wsdoc.impl; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.versly.rest.wsdoc.AnnotationProcessor; - import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.versly.rest.wsdoc.AnnotationProcessor; + public class SpringMVCRestImplementationSupport implements AnnotationProcessor.RestImplementationSupport { @Override @@ -67,13 +70,30 @@ public String getRequestParam(VariableElement var) { */ @Override public String getPojoRequestParam(VariableElement var) { - if (getRequestParam(var) == null && getPathVariable(var) == null && !var.asType().toString().startsWith("org.springframework")) { + if (getRequestParam(var) == null && getPathVariable(var) == null && !ignorePojo(var)) { return ""; // No annotations to parse } else { return null; } } + + private boolean ignorePojo(VariableElement var) + { + // Spring Types + if(var.asType().toString().startsWith("org.springframework")) + { + return true; + } + + // Atmosphere Types + if(var.asType().toString().startsWith("org.atmosphere")) + { + return true; + } + + return false; + } @Override diff --git a/src/main/resources/org/versly/rest/wsdoc/RestDocumentation.ftl b/src/main/resources/org/versly/rest/wsdoc/RestDocumentation.ftl index 4ab98f7..3c5504e 100644 --- a/src/main/resources/org/versly/rest/wsdoc/RestDocumentation.ftl +++ b/src/main/resources/org/versly/rest/wsdoc/RestDocumentation.ftl @@ -90,6 +90,12 @@ Note: this endpoint expects a multipart request body. + + <#if methodDoc.websocket> +
+ Note: this is a websocket endpoint. +
+ <#assign subs=methodDoc.urlSubstitutions.fields> <#if (subs?keys?size > 0)>