diff --git a/README.md b/README.md
index 278f945..2564dfa 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ To include swagger4spring-web in your project, you need to include the jar in yo
com.knappsack
swagger4spring-web
- 0.3.3
+ 0.3.1
@@ -35,15 +35,15 @@ servlet context. For example:
If you are using version 0.3.0 or above, you'll also need to add the following to the appropriate Spring context file in your application:
-
-
+
+
-
-
+
+
Once the ApiDocumentationController is wired, you may call go to your base path + /api/resourceList (ex: http://localhost/swagger4spring-web-example/api/resourceList) in order to retrieve an inventory of your APIs. For an example JSP see this [page](https://github.com/wkennedy/swagger4spring-web-example/blob/master/src/main/webapp/WEB-INF/views/documentation.jsp).
@@ -75,7 +75,6 @@ To see a working example, please take a look at [swagger4spring-web-example](htt
The following Spring-Web annotations are supported:
* @Controller
-* @RestController
* @RequestMapping
* @ResponseBody
* @RequestBody
diff --git a/pom.xml b/pom.xml
index dff74d9..944d7a9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,14 +5,14 @@
org.sonatype.oss
oss-parent
- 9
+ 7
swagger4spring-web
com.knappsack
swagger4spring-web
jar
- 0.3.5
+ 0.3.16-SNAPSHOT-ALERTME
Swagger implementation for the Spring-Web framework
https://github.com/wkennedy/swagger4spring-web
@@ -36,23 +36,21 @@
- 4.0.6.RELEASE
- 1.3.7
+ 3.2.5.RELEASE
+ 1.3.1
1.6.4
1.0.1
4.9
- 1.8
- 2.4.1
- 0.9.9-RC2
- 3.1.0
- 2.6.1
+ 1.6
+ 2.3.0
+ 0.9.10
org.scala-lang
scala-library
- 2.10.4
+ 2.10.3
@@ -92,38 +90,47 @@
+
+ com.wordnik
+ swagger-annotations_2.10
+ 1.3.0
+
com.wordnik
swagger-core_2.10
${org.wordnik-swagger-version}
- jackson-databind
- com.fasterxml.jackson.core
+ guava
+ com.google.guava
- jackson-core
- com.fasterxml.jackson.core
+ paranamer
+ com.thoughtworks.paranamer
- jackson-annotations
- com.fasterxml.jackson.core
+ slf4j-api
+ org.slf4j
scala-library
org.scala-lang
- paranamer
- com.thoughtworks.paranamer
+ jackson-databind
+ com.fasterxml.jackson.core
- scala-reflect
- org.scala-lang
+ jackson-core
+ com.fasterxml.jackson.core
- slf4j-api
- org.slf4j
+ jackson-annotations
+ com.fasterxml.jackson.core
+
+
+ jackson-module-scala_2.10
+ com.fasterxml.jackson.module
@@ -131,19 +138,23 @@
org.reflections
reflections
${org.reflections.version}
+
+
+ guava
+ com.google.guava
+
+
javax.servlet
- javax.servlet-api
- ${servlet-api-version}
- provided
+ servlet-api
+ 2.5
-
com.thoughtworks.paranamer
paranamer
- ${paranamer-version}
+ 2.6
@@ -202,7 +213,7 @@
net.alchim31.maven
scala-maven-plugin
- 3.1.6
+ 3.1.5
false
@@ -227,11 +238,10 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.1
+ 2.5.1
${java.version}
${java.version}
- -parameters
@@ -245,7 +255,7 @@
org.apache.maven.plugins
maven-source-plugin
- 2.3
+ 2.1.2
attach-sources
@@ -258,7 +268,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 2.9.1
+ 2.9
attach-javadocs
diff --git a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java
index 7055ed6..2a11662 100644
--- a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java
+++ b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java
@@ -1,5 +1,7 @@
package com.knappsack.swagger4springweb.controller;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
import com.knappsack.swagger4springweb.filter.Filter;
import com.knappsack.swagger4springweb.parser.ApiParser;
import com.knappsack.swagger4springweb.parser.ApiParserImpl;
@@ -15,14 +17,14 @@
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
@Controller
@RequestMapping(value = "/api")
public class ApiDocumentationController {
+ public static final String REQUEST_FILTERS = ApiDocumentationController.class.getName() + ".requestFilters";
+
private String baseControllerPackage = "";
private List additionalControllerPackages = new ArrayList();
@@ -37,14 +39,15 @@ public class ApiDocumentationController {
private List additionalModelPackages = new ArrayList();
private String basePath = "";
private String apiVersion = "v1";
- private Map documentation;
private List ignorableAnnotations = new ArrayList();
private boolean ignoreUnusedPathVariables = true;
private boolean basePathFromReferer = false;
- private ResourceListing resourceList;
private ApiInfo apiInfo;
private List filters;
+ private final Map, Map> documentationCache = Maps.newHashMap();
+ private final Map, ResourceListing> resourceListingCache = Maps.newHashMap();
+
@RequestMapping(value = "/resourceList", method = RequestMethod.GET, produces = "application/json")
public
@ResponseBody
@@ -73,51 +76,67 @@ ApiListing getDocumentation(HttpServletRequest request) {
@SuppressWarnings("unused")
public String getBasePath() {
- if (basePath == null || basePath.isEmpty()) {
- //If no base path was specified, attempt to get the base path from the request URL
- HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
- .getRequestAttributes()).getRequest();
- if (request != null) {
- // requested from
- String referer = request.getHeader("Referer");
-
- if (basePathFromReferer && referer != null) {
- basePath = referer.substring(0, referer.lastIndexOf("/"));
- } else {
- String mapping = request.getServletPath();
- basePath = request.getRequestURL().toString();
- basePath = basePath.substring(0, basePath.indexOf(mapping));
- }
+ if (basePath != null && !basePath.isEmpty()) {
+ return basePath;
+ }
+
+ //If no base path was specified, attempt to get the base path from the request URL
+ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
+ .getRequestAttributes()).getRequest();
+ if (request != null) {
+ // requested from
+ String referer = request.getHeader("Referer");
+
+ if (basePathFromReferer && referer != null && !referer.isEmpty()) {
+ return referer.substring(0, referer.lastIndexOf("/"));
+ } else {
+ String mapping = request.getServletPath();
+ String requestURL = request.getRequestURL().toString();
+ return requestURL.substring(0, requestURL.indexOf(mapping));
}
}
- return basePath;
+ throw new RuntimeException("Base path can't be constructed");
}
private Map getDocs(HttpServletRequest request) {
- if (documentation == null || (filters != null && !filters.isEmpty())) {
- String servletPath = null;
- if (request != null) {
- servletPath = request.getServletPath();
- }
- ApiParser apiParser = new ApiParserImpl(apiInfo, getControllerPackages(), getBasePath(),
- servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, filters);
- documentation = apiParser.createApiListings();
+ Set fullSetOfFilters = mergeFilters(getRequestFilters(request));
+
+ Map documentation = documentationCache.get(fullSetOfFilters);
+ if (documentation != null) {
+ return documentation;
+ }
+
+ String servletPath = null;
+ if (request != null) {
+ servletPath = request.getServletPath();
}
+ ApiParser apiParser = new ApiParserImpl(apiInfo, getControllerPackages(), getBasePath(),
+ servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, fullSetOfFilters);
+ documentation = apiParser.createApiListings();
+ documentationCache.put(fullSetOfFilters, documentation);
+
return documentation;
}
private ResourceListing getResourceList(HttpServletRequest request) {
- if (resourceList == null || (filters != null && !filters.isEmpty())) {
- String servletPath = null;
- if (request != null) {
- servletPath = request.getServletPath();
- servletPath = servletPath.replace("/resourceList", "");
- }
- ApiParser apiParser = new ApiParserImpl(apiInfo, getControllerPackages(), getBasePath(),
- servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, filters);
- resourceList = apiParser.getResourceListing(getDocs(request));
+ Set fullSetOfFilters = mergeFilters(getRequestFilters(request));
+
+ ResourceListing resourceListing = resourceListingCache.get(fullSetOfFilters);
+ if (resourceListing != null) {
+ return resourceListing;
}
- return resourceList;
+
+ String servletPath = null;
+ if (request != null) {
+ servletPath = request.getServletPath();
+ servletPath = servletPath.replace("/resourceList", "");
+ }
+ ApiParser apiParser = new ApiParserImpl(apiInfo, getControllerPackages(), getBasePath(),
+ servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, fullSetOfFilters);
+ resourceListing = apiParser.getResourceListing(getDocs(request));
+ resourceListingCache.put(fullSetOfFilters, resourceListing);
+
+ return resourceListing;
}
private List getControllerPackages() {
@@ -133,9 +152,15 @@ private List getControllerPackages() {
return controllerPackages;
}
- @SuppressWarnings("unused")
- public void setResourceList(ResourceListing resourceList) {
- this.resourceList = resourceList;
+ private Set mergeFilters(final Collection requestFilters) {
+ final Set allFilters = Sets.newHashSet();
+ if (filters != null) {
+ allFilters.addAll(filters);
+ }
+ if (requestFilters != null) {
+ allFilters.addAll(requestFilters);
+ }
+ return allFilters;
}
@SuppressWarnings("unused")
@@ -178,16 +203,6 @@ public void setAdditionalModelPackages(List additionalModelPackages) {
this.additionalModelPackages = additionalModelPackages;
}
- @SuppressWarnings("unused")
- public Map getDocumentation() {
- return documentation;
- }
-
- @SuppressWarnings("unused")
- public void setDocumentation(Map documentation) {
- this.documentation = documentation;
- }
-
@SuppressWarnings("unused")
public List getIgnorableAnnotations() {
return ignorableAnnotations;
@@ -237,4 +252,12 @@ public void setBasePath(final String basePath) {
public void setApiVersion(final String apiVersion) {
this.apiVersion = apiVersion;
}
+
+ public static void setRequestFilters(final HttpServletRequest request, final Collection requestFilters) {
+ request.setAttribute(REQUEST_FILTERS, requestFilters);
+ }
+
+ public static Collection getRequestFilters(final HttpServletRequest request) {
+ return (Collection) request.getAttribute(REQUEST_FILTERS);
+ }
}
diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiModelParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiModelParser.java
index 23de80a..bb77091 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiModelParser.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiModelParser.java
@@ -2,10 +2,7 @@
import com.knappsack.swagger4springweb.util.ModelUtils;
import com.wordnik.swagger.model.Model;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
@@ -13,29 +10,14 @@
public class ApiModelParser {
- private static final Logger LOGGER = LoggerFactory.getLogger(ApiModelParser.class);
-
private final Map models;
public ApiModelParser(final Map models) {
this.models = models;
}
- /**
- * @param method Method for which to evaluate the return type
- */
public void parseResponseBodyModels(Method method) {
- //In Spring 4, the RestController annotation specifies that the class is a Controller and the return type
- //is automatically assumed to be the ResponseBody, therefore no ResponseBody annotation is needed when marked
- //as a RestController
- boolean isRestController = false;
- try {
- isRestController = method.getDeclaringClass().getAnnotation(RestController.class) != null;
- } catch (NoClassDefFoundError e) {
- //Check for NoClassDefFoundError in the case that this is being used in a Spring 3 project where the RestController does not exist.
- LOGGER.debug("No RestController found. RestController is found in Spring 4. This is potentially an earlier version of Spring", e);
- }
- if (method.getAnnotation(ResponseBody.class) != null || isRestController) {
+ if (method.getAnnotation(ResponseBody.class) != null) {
Type type = method.getGenericReturnType();
ModelUtils.addModels(type, models);
diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
index 535dc8c..8f696d6 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
@@ -2,27 +2,28 @@
import com.knappsack.swagger4springweb.util.JavaToScalaUtil;
import com.wordnik.swagger.annotations.*;
-import com.wordnik.swagger.converter.ModelConverters;
-import com.wordnik.swagger.model.*;
-import com.wordnik.swagger.model.Authorization;
-import com.wordnik.swagger.model.AuthorizationScope;
-import org.apache.commons.lang.ArrayUtils;
-import org.springframework.http.HttpMethod;
+import com.wordnik.swagger.converter.CustomModelConverters;
+import com.wordnik.swagger.core.ApiValues;
+import com.wordnik.swagger.model.Model;
+import com.wordnik.swagger.model.Operation;
+import com.wordnik.swagger.model.Parameter;
+import com.wordnik.swagger.model.ResponseMessage;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import scala.Option;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
-import java.lang.reflect.WildcardType;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static java.lang.String.format;
+import static java.util.Arrays.asList;
+import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED;
public class ApiOperationParser {
@@ -50,8 +51,13 @@ public Operation parseDocumentationOperation(Method method) {
if (parameterizedType.getActualTypeArguments().length == 1) {
final Type type = parameterizedType.getActualTypeArguments()[0];
- documentationOperation.setResponseClass(getResponseClass(type));
- documentationOperation.setResponseContainer(((Class>) parameterizedType.getRawType()));
+ if (type instanceof ParameterizedType) {
+ documentationOperation.setResponseClass((Class>) ((ParameterizedType) type).getRawType());
+ } else {
+ documentationOperation.setResponseClass((Class>) type);
+ }
+ documentationOperation
+ .setResponseContainer(((Class>) parameterizedType.getRawType()));
} else {
// TODO what to do here?
// not supporting generic with several values
@@ -72,13 +78,9 @@ public Operation parseDocumentationOperation(Method method) {
for (RequestMethod requestMethod : methodRequestMapping.method()) {
httpMethod += requestMethod.name() + " ";
}
- httpMethod = httpMethod.trim();
- if(StringUtils.isEmpty(httpMethod) || " ".equals(httpMethod)) {
- httpMethod = HttpMethod.GET.toString();
- }
- documentationOperation.setHttpMethod(httpMethod);
- documentationOperation.addConsumes(getConsumes(method, methodRequestMapping));
- documentationOperation.addProduces(getProduces(method, methodRequestMapping));
+ documentationOperation.setHttpMethod(httpMethod.trim());
+ documentationOperation.addConsumes(asList(methodRequestMapping.consumes()));
+ documentationOperation.addProduces(asList(methodRequestMapping.produces()));
// get ApiOperation information
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
@@ -111,7 +113,7 @@ public Operation parseDocumentationOperation(Method method) {
ApiParameterParser apiParameterParser = new ApiParameterParser(ignorableAnnotations, models);
List documentationParameters = apiParameterParser.parseApiParametersAndArgumentModels(method);
documentationOperation.setParameters(documentationParameters);
- addUnusedPathVariables(documentationOperation, methodRequestMapping.value());
+ addUnusedPathVariables(documentationOperation, methodRequestMapping.value(), method);
return documentationOperation.toScalaOperation();
}
@@ -123,72 +125,60 @@ private void addResponse(DocumentationOperation documentationOperation, ApiRespo
documentationOperation.addResponseMessage(responseMessage);
}
- private void addUnusedPathVariables(DocumentationOperation documentationOperation, String[] methodPath) {
+ private void addUnusedPathVariables(DocumentationOperation documentationOperation, String[] methodPath,
+ Method method) {
if (ignoreUnusedPathVariables) {
return;
}
for (Parameter documentationParameter : new ApiPathParser().getPathParameters(resourcePath, methodPath)) {
if (!isParameterPresented(documentationOperation, documentationParameter.name())) {
- documentationOperation.addParameter(documentationParameter);
+ documentationOperation.addParameter(addApiImplicitParams(documentationParameter, method));
}
}
}
- private boolean isParameterPresented(DocumentationOperation documentationOperation, String parameter) {
- if (documentationOperation.getParameters().isEmpty()) {
- return false;
- }
- for (Parameter documentationParameter : documentationOperation.getParameters()) {
- if (parameter.equals(documentationParameter.name())) {
- return true;
- }
- }
- return false;
- }
+ private Parameter addApiImplicitParams(Parameter documentationParameter, Method method) {
+ Annotation[] annotations = method.getDeclaredAnnotations();
- private List getConsumes(Method method, RequestMapping methodRequestMapping) {
- List consumes = Arrays.asList(methodRequestMapping.consumes());
- if(consumes.isEmpty()) {
- RequestMapping controllerRequestMapping = method.getDeclaringClass().getAnnotation(RequestMapping.class);
- if(controllerRequestMapping != null) {
- consumes = Arrays.asList(controllerRequestMapping.consumes());
- }
+ ApiImplicitParam annotation = getApiImplicitParam(documentationParameter, annotations);
+ if (annotation != null) {
+ return new Parameter(documentationParameter.name(), Option.apply(annotation.value()), null,
+ annotation.required(), annotation.allowMultiple(),
+ annotation.dataType().toLowerCase(), null,
+ ApiValues.TYPE_PATH(), null);
}
-
- return consumes;
+ return documentationParameter;
}
- private List getProduces(Method method, RequestMapping methodRequestMapping) {
- List produces = Arrays.asList(methodRequestMapping.produces());
- if(produces.isEmpty()) {
- RequestMapping controllerRequestMapping = method.getDeclaringClass().getAnnotation(RequestMapping.class);
- if(controllerRequestMapping != null) {
- produces = Arrays.asList(controllerRequestMapping.produces());
+ private ApiImplicitParam getApiImplicitParam(Parameter documentationParameter, Annotation[] annotations) {
+ for (Annotation annotation : annotations) {
+ if (annotation instanceof ApiImplicitParam
+ && ((ApiImplicitParam) annotation).name().equalsIgnoreCase(documentationParameter.name())) {
+ return (ApiImplicitParam)annotation;
+ } else if (annotation instanceof ApiImplicitParams) {
+ for (ApiImplicitParam item : ((ApiImplicitParams)annotation).value()) {
+ if (item.name().equalsIgnoreCase(documentationParameter.name())) {
+ return item;
+ }
+ }
}
}
- return produces;
+ return null;
}
- private Class> getResponseClass(Type type) {
- Class> responseClass = null;
- if (type instanceof ParameterizedType) {
- responseClass = (Class>) ((ParameterizedType) type).getRawType();
- } else if(type instanceof WildcardType) {
- Type[] lowerBounds = ((WildcardType) type).getLowerBounds();
- Type[] upperBounds = ((WildcardType) type).getUpperBounds();
- if(lowerBounds.length > 0) {
- responseClass = (Class>) lowerBounds[0];
- } else if(upperBounds.length > 0) {
- responseClass = (Class>) upperBounds[0];
+ private boolean isParameterPresented(DocumentationOperation documentationOperation, String parameter) {
+ if (documentationOperation.getParameters().isEmpty()) {
+ return false;
+ }
+ for (Parameter documentationParameter : documentationOperation.getParameters()) {
+ if (parameter.equals(documentationParameter.name()) &&
+ "path".equals(documentationParameter.paramType())) {
+ return true;
}
-
- } else {
- responseClass = (Class>) type;
}
-
- return responseClass;
+ return false;
}
//This class is used as a temporary solution to create a Swagger Operation object, since the Operation is immutable
@@ -205,9 +195,11 @@ class DocumentationOperation {
private List produces = new ArrayList();
private List consumes = new ArrayList();
private List protocols = new ArrayList();
- private List authorizations = new ArrayList();
+ private List authorizations = new ArrayList();
Operation toScalaOperation() {
+ final List consumes = getConsumes();
+
return new Operation(httpMethod,
summary,
notes,
@@ -232,7 +224,7 @@ void setResponseClass(Class> responseClass) {
return;
}
- Option model = ModelConverters.read(responseClass, ModelConverters.typeMap());
+ Option model = CustomModelConverters.read(responseClass);
if (model.nonEmpty()) {
this.responseClass = model.get().name();
} else {
@@ -283,19 +275,11 @@ public void addParameter(final Parameter parameter) {
this.parameters.add(parameter);
}
- public void addAuthorizations(final com.wordnik.swagger.annotations.Authorization[] authorizations) {
- if (ArrayUtils.isEmpty(authorizations)) {
+ public void addAuthorizations(final String authorizations) {
+ if (StringUtils.isEmpty(authorizations)) {
return;
}
-
- for (com.wordnik.swagger.annotations.Authorization authorization : authorizations) {
- AuthorizationScope[] authorizationScopes = new AuthorizationScope[authorization.scopes().length];
- for(int i = 0;i < authorization.scopes().length;i++) {
- com.wordnik.swagger.annotations.AuthorizationScope authScope = authorization.scopes()[i];
- authorizationScopes[i] = new AuthorizationScope(authScope.scope(), authScope.description());
- }
- this.authorizations.add(new Authorization(authorization.value(), authorizationScopes));
- }
+ this.authorizations.add(authorizations);
}
void addProtocols(final String protocols) {
@@ -327,7 +311,7 @@ public void setResponseContainer(final String container) {
}
public void setResponseContainer(final Class> type) {
- Option model = ModelConverters.read(type, ModelConverters.typeMap());
+ Option model = CustomModelConverters.read(type);
if (model.nonEmpty()) {
setResponseContainer(model.get().name());
} else {
@@ -339,5 +323,9 @@ public String getResponseClass() {
return responseClass;
}
+ List getConsumes() {
+ return !consumes.isEmpty() || "GET".equalsIgnoreCase(httpMethod)
+ ? consumes : asList(APPLICATION_FORM_URLENCODED);
+ }
}
}
diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java
index 755a730..d9557ae 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java
@@ -4,6 +4,7 @@
import com.knappsack.swagger4springweb.util.AnnotationUtils;
import com.knappsack.swagger4springweb.util.ModelUtils;
import com.knappsack.swagger4springweb.util.JavaToScalaUtil;
+import com.wordnik.swagger.annotations.ApiModelProperty;
import com.wordnik.swagger.annotations.ApiParam;
import com.wordnik.swagger.core.ApiValues;
import com.wordnik.swagger.model.AllowableListValues;
@@ -61,6 +62,10 @@ public List parseApiParametersAndArgumentModels(Method method) {
if (annotation instanceof ApiParam) {
addApiParams((ApiParam) annotation, documentationParameter);
}
+
+ if (annotation instanceof ApiModelProperty) {
+ addApiModelProperty((ApiModelProperty) annotation,documentationParameter);
+ }
}
Option descriptionOption = Option.apply(documentationParameter.getDescription());
@@ -76,6 +81,8 @@ public List parseApiParametersAndArgumentModels(Method method) {
documentationParameters.add(parameter);
}
+ documentationParameters.addAll(AnnotationUtils.getImplicitParameters(method));
+
return documentationParameters;
}
@@ -102,7 +109,7 @@ private void addModelAttribute(final ModelAttribute modelAttribute,
if (ModelUtils.isSet(modelAttribute.value())) {
documentationParameter.setName(modelAttribute.value());
}
- documentationParameter.setParamType(ApiValues.TYPE_FORM());
+ documentationParameter.setParamType(ApiValues.TYPE_BODY());
}
private void addRequestBody(DocumentationParameter documentationParameter) {
@@ -173,6 +180,34 @@ private void addApiParams(ApiParam apiParam, DocumentationParameter documentatio
}
}
+ private void addApiModelProperty(final ApiModelProperty apiModelProperty,
+ final DocumentationParameter documentationParameter) {
+ if (ModelUtils.isSet(apiModelProperty.allowableValues())) {
+ List allowableValues = Arrays.asList(apiModelProperty.allowableValues().split("\\s*,\\s*"));
+ documentationParameter
+ .setAllowableValues(new AllowableListValues(JavaToScalaUtil.toScalaList(allowableValues), "LIST"));
+ }
+
+ if (ModelUtils.isSet(apiModelProperty.notes())) {
+ documentationParameter.setDescription(apiModelProperty.notes());
+ }
+
+ if (ModelUtils.isSet(apiModelProperty.dataType())) {
+ documentationParameter.setDataType(apiModelProperty.dataType());
+ }
+
+ documentationParameter.setDescription(apiModelProperty.value());
+
+ documentationParameter.setParamAccess(apiModelProperty.access());
+
+ if (!documentationParameter.isRequired()) {
+ documentationParameter.setRequired(apiModelProperty.required());
+ }
+
+
+
+ }
+
private boolean hasIgnorableAnnotations(List annotations) {
for (Annotation annotation : annotations) {
if (ignorableAnnotations.contains(annotation.annotationType().getCanonicalName())) {
diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParser.java
index 4ce1f1a..babc242 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParser.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParser.java
@@ -7,7 +7,7 @@
public interface ApiParser {
/**
- * @param documentationMap Map - A map of different API declarations for which you want to
+ * @param documentationMap Map - A map of different API declarations for which you want to
* create a resource listing.
* @return ResourceListing - This returns a resource listing which is an inventory of all APIs
*/
diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java
index be87a2f..983ec18 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java
@@ -4,26 +4,23 @@
import com.knappsack.swagger4springweb.filter.ApiExcludeFilter;
import com.knappsack.swagger4springweb.filter.Filter;
import com.knappsack.swagger4springweb.util.AnnotationUtils;
-import com.knappsack.swagger4springweb.util.ApiListingUtil;
import com.knappsack.swagger4springweb.util.JavaToScalaUtil;
import com.knappsack.swagger4springweb.util.ScalaToJavaUtil;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.config.SwaggerConfig;
import com.wordnik.swagger.model.*;
+import org.reflections.ReflectionUtils;
import org.reflections.Reflections;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
import scala.Option;
import java.lang.reflect.Method;
import java.util.*;
-public class ApiParserImpl implements ApiParser {
+import static org.reflections.ReflectionUtils.withAnnotation;
- private static final Logger LOGGER = LoggerFactory.getLogger(ApiParserImpl.class);
+public class ApiParserImpl implements ApiParser {
private static final String swaggerVersion = com.wordnik.swagger.core.SwaggerSpec.version();
@@ -40,7 +37,7 @@ public class ApiParserImpl implements ApiParser {
public ApiParserImpl(ApiInfo apiInfo, List baseControllerPackage, String basePath, String servletPath,
String apiVersion, List ignorableAnnotations, boolean ignoreUnusedPathVariables,
- List filters) {
+ Collection filters) {
this.controllerPackages = baseControllerPackage;
this.ignorableAnnotations = ignorableAnnotations;
@@ -65,33 +62,21 @@ public ApiParserImpl(ApiInfo apiInfo, List baseControllerPackage, String
}
public ResourceListing getResourceListing(Map apiListingMap) {
+ int count = 0;
List apiListingReferences = new ArrayList();
for (String key : apiListingMap.keySet()) {
ApiListing apiListing = apiListingMap.get(key);
String docPath = "/doc"; //servletPath + "/doc"; //"/api/doc";
+ if (!key.startsWith("/")) {
+ docPath = docPath + "/";
+ }
ApiListingReference apiListingReference = new ApiListingReference(docPath + key, apiListing.description(),
- apiListing.position());
+ count);
apiListingReferences.add(apiListingReference);
+ count++;
}
- Collections.sort(apiListingReferences, new Comparator() {
- @Override
- public int compare(ApiListingReference o1, ApiListingReference o2) {
- if (o1.position() == o2.position())
- return 0;
- else if(o1.position() == 0)
- return 1;
- else if(o2.position() == 0)
- return -1;
- else if (o1.position() < o2.position())
- return -1;
- else if (o1.position() > o2.position())
- return 1;
- return 0;
- }
- });
-
return new ResourceListing(apiVersion, swaggerVersion, JavaToScalaUtil.toScalaList(apiListingReferences), null,
swaggerConfig.info());
}
@@ -101,12 +86,6 @@ public Map createApiListings() {
for (String controllerPackage : controllerPackages) {
Reflections reflections = new Reflections(controllerPackage);
controllerClasses.addAll(reflections.getTypesAnnotatedWith(Controller.class));
- try {
- controllerClasses.addAll(reflections.getTypesAnnotatedWith(RestController.class));
- } catch (NoClassDefFoundError e) {
- //Check for NoClassDefFoundError in the case that this is being used in a Spring 3 project where the RestController does not exist.
- LOGGER.debug("No RestController found. RestController is found in Spring 4. This is potentially an earlier version of Spring", e);
- }
}
return processControllers(controllerClasses);
@@ -119,7 +98,8 @@ private Map processControllers(Set> controllerClass
continue;
}
- Set requestMappingMethods = AnnotationUtils.getAnnotatedMethods(controllerClass, RequestMapping.class);
+ Set requestMappingMethods =
+ ReflectionUtils.getAllMethods(controllerClass, withAnnotation(RequestMapping.class));
ApiListing apiListing = processControllerApi(controllerClass);
String description = "";
Api controllerApi = controllerClass.getAnnotation(Api.class);
@@ -127,14 +107,8 @@ private Map processControllers(Set> controllerClass
description = controllerApi.description();
}
- if (apiListing.apis().size() == 0) {
- apiListing = processMethods(requestMappingMethods, controllerClass, apiListing, description);
- }
-
- //Allow for multiple controllers having the same resource path.
- ApiListing existingApiListing = apiListingMap.get(apiListing.resourcePath());
- if (existingApiListing != null) {
- apiListing = ApiListingUtil.mergeApiListing(existingApiListing, apiListing);
+ if (apiListing.apis() == null) {
+ apiListing = processMethods(requestMappingMethods, apiListing, description);
}
// controllers without any operations are excluded from the apiListingMap list
@@ -162,29 +136,29 @@ private ApiListing processControllerApi(Class> controllerClass) {
resourcePath = controllerClass.getName();
}
}
- if (!resourcePath.startsWith("/")) {
- resourcePath = "/" + resourcePath;
- }
- String docRoot = resourcePath;
- if(docRoot.contains(controllerClass.getName())) {
- docRoot = docRoot.replace(controllerClass.getName(), "");
- }
SpringApiReader reader = new SpringApiReader();
- Option apiListingOption = reader.read(docRoot, controllerClass, swaggerConfig);
+ Option apiListingOption = reader.read(resourcePath, controllerClass, swaggerConfig);
ApiListing apiListing = null;
if (apiListingOption.nonEmpty()) {
apiListing = apiListingOption.get();
}
+ //Allow for multiple controllers having the same resource path.
+ ApiListing existingApiListing = apiListingMap.get(resourcePath);
+ if (existingApiListing != null) {
+ return existingApiListing;
+ }
+
if (apiListing != null) {
return apiListing;
}
- return ApiListingUtil.baseApiListing(apiVersion, swaggerVersion, basePath, resourcePath);
+ return new ApiListing(apiVersion, swaggerVersion, basePath, resourcePath, null, null, null, null, null, null,
+ null, 0);
}
- private ApiListing processMethods(Collection methods, Class> controllerClass, ApiListing apiListing, String description) {
+ private ApiListing processMethods(Collection methods, ApiListing apiListing, String description) {
Map endpoints = new HashMap();
Map models = new HashMap();
@@ -195,13 +169,6 @@ private ApiListing processMethods(Collection methods, Class> controlle
ApiModelParser apiModelParser = new ApiModelParser(models);
- //This is for the case where there is no request mapping at the class level. When this occurs, the resourcePath
- //is the class name, which we don't want to be appended to the path of the operation. Therefore, we replace
- //the class name.
- String resourcePath = apiListing.resourcePath();
- if(resourcePath.contains(controllerClass.getName())) {
- resourcePath = resourcePath.replace("/" + controllerClass.getName(), "");
- }
for (Method method : methods) {
if (ignore(method)) {
continue;
@@ -210,7 +177,7 @@ private ApiListing processMethods(Collection methods, Class> controlle
String value = AnnotationUtils.getMethodRequestMappingValue(method);
ApiDescriptionParser documentationEndPointParser = new ApiDescriptionParser();
ApiDescription apiDescription = documentationEndPointParser
- .parseApiDescription(method, description, resourcePath);
+ .parseApiDescription(method, description, apiListing.resourcePath());
if (!endpoints.containsKey(value)) {
endpoints.put(value, apiDescription);
}
@@ -221,7 +188,7 @@ private ApiListing processMethods(Collection methods, Class> controlle
operations.put(value, ops);
}
- ApiOperationParser apiOperationParser = new ApiOperationParser(resourcePath,
+ ApiOperationParser apiOperationParser = new ApiOperationParser(apiListing.resourcePath(),
ignorableAnnotations, ignoreUnusedPathVariables, models);
Operation operation = apiOperationParser.parseDocumentationOperation(method);
ops.add(operation);
@@ -264,4 +231,5 @@ private boolean ignore(Method method) {
}
return false;
}
+
}
diff --git a/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java
index 42a06c7..d773ae2 100644
--- a/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java
+++ b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java
@@ -4,17 +4,26 @@
import com.knappsack.swagger4springweb.model.AnnotatedParameter;
import com.thoughtworks.paranamer.BytecodeReadingParanamer;
import com.thoughtworks.paranamer.Paranamer;
+import com.wordnik.swagger.annotations.ApiImplicitParam;
+import com.wordnik.swagger.annotations.ApiImplicitParams;
+import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
+import com.wordnik.swagger.model.AllowableListValues;
+import com.wordnik.swagger.model.AllowableValues;
+import com.wordnik.swagger.model.Parameter;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import scala.Option;
import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
import java.lang.reflect.Method;
-import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Arrays;
import java.util.List;
-import java.util.Set;
public class AnnotationUtils {
@@ -60,41 +69,32 @@ public static String getMethodRequestMappingValue(Method method) {
/**
* @param method
* Method
- * @return List - if the method contains parameters
+ * @return List - if the method contains parameters
* annotated with ApiParam, RequestMapping, PathVariable, or
* RequestBody, this method will return a list of AnnotatedParameter
* objects based on the values of the parameter annotations
*/
public static List getAnnotatedParameters(Method method) {
- List annotatedParameters = new ArrayList<>();
+ List annotatedParameters = new ArrayList();
Paranamer paranamer = new BytecodeReadingParanamer();
- String[] parameterNames;
- //Attempt to use Paranamer to look up the parameter names for those not using Java 8+.
- //This will fail if trying to evaluate a class using Lambdas, in which case fall back and look up using
- //standard java reflections. This will provide the paramter name if using the -parameters javac argument.
- try {
- parameterNames = paranamer.lookupParameterNames(method);
- } catch(Exception e) {
- Parameter[] parameters = method.getParameters();
- parameterNames = new String[parameters.length];
- for (int i = 0; i < parameters.length; i++) {
- Parameter parameter = parameters[i];
- parameterNames[i] = parameter.getName();
- }
- }
+ String[] parameterNames = paranamer.lookupParameterNames(method);
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
+ final ApiOperation apiOperation = findApiOperation(Lists.newArrayList(method.getDeclaredAnnotations()));
int i = 0;
for (Annotation[] annotations : parameterAnnotations) {
if (annotations.length > 0) {
- AnnotatedParameter annotatedParameter = new AnnotatedParameter();
- annotatedParameter.setParameterClass(parameterTypes[i]);
- annotatedParameter.setParameterName(parameterNames[i]);
- annotatedParameter.setParameterType(genericParameterTypes[i]);
- annotatedParameter.addAnnotations(Lists.newArrayList(annotations));
- annotatedParameters.add(annotatedParameter);
+ if (containsModelAttribute(annotations)
+ && !apiOperation.consumes().equals(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
+ addModelAttributeParameters(parameterTypes[i], annotatedParameters);
+ } else {
+ AnnotatedParameter annotatedParameter = createParameter(parameterNames[i], parameterTypes[i],
+ genericParameterTypes[i], Lists.newArrayList(annotations));
+
+ annotatedParameters.add(annotatedParameter);
+ }
}
i++;
}
@@ -102,19 +102,125 @@ public static List getAnnotatedParameters(Method method) {
}
/**
- *
- * @param clazz Class - scan this class for methods with the specified annotation
- * @param annotationClass Class - return all methods with this annotation
- * @return Set - all methods of this class with the specified annotation
+ * @param method
+ * Method
+ * @return List - if the method is annotated with ApiImplicitParams that contain parameters definitions
*/
- public static Set getAnnotatedMethods(Class> clazz, Class extends Annotation> annotationClass) {
- Method[] methods = clazz.getDeclaredMethods();
- Set annotatedMethods = new HashSet<>(methods.length);
- for (Method method : methods) {
- if( method.isAnnotationPresent(annotationClass)){
- annotatedMethods.add(method);
+ public static List getImplicitParameters(Method method) {
+ final List parameters = Lists.newArrayList();
+ final ApiImplicitParams apiImplicitParams = findApiImplicitParams(
+ Lists.newArrayList(method.getDeclaredAnnotations()));
+ if (apiImplicitParams != null) {
+ for (final ApiImplicitParam apiImplicitParam : apiImplicitParams.value()) {
+ parameters.add(new Parameter(apiImplicitParam.name(), Option.apply(apiImplicitParam.value()),
+ Option.apply(apiImplicitParam.defaultValue()),
+ apiImplicitParam.required(), apiImplicitParam.allowMultiple(),
+ getDataType(apiImplicitParam.dataType()), getAllowableValues(apiImplicitParam),
+ apiImplicitParam.paramType(), Option.apply(apiImplicitParam.access())));
+ }
+ }
+ return parameters;
+ }
+
+ private static String getDataType(String dataType) {
+ return dataType == null ? null : dataType.toLowerCase();
+ }
+
+ private static AllowableValues getAllowableValues(ApiImplicitParam apiImplicitParam) {
+ if (ModelUtils.isSet(apiImplicitParam.allowableValues())) {
+ List allowableValues = Arrays.asList(apiImplicitParam.allowableValues().split("\\s*,\\s*"));
+ return new AllowableListValues(JavaToScalaUtil.toScalaList(allowableValues), "LIST");
+ }
+ return null;
+ }
+
+ private static void addModelAttributeParameters(final Class type,
+ final List annotatedParameters) {
+ List fields = new ArrayList();
+ fields = getAllFields(fields, type);
+
+ for (final Field field : fields) {
+ List annotations = new ArrayList(Arrays.asList(field.getDeclaredAnnotations()));
+ annotations.add(createRequestParamAnnotation());
+ annotatedParameters.add(createParameter(field.getName(), field.getType(),
+ field.getGenericType(), annotations));
+ }
+ }
+
+ private static Annotation createRequestParamAnnotation() {
+ return new RequestParam() {
+ @Override
+ public String value() {
+ return "";
+ }
+
+ @Override
+ public boolean required() {
+ return false;
+ }
+
+ @Override
+ public String defaultValue() {
+ return "";
}
+
+ @Override
+ public Class extends Annotation> annotationType() {
+ return RequestParam.class;
+ }
+ };
+ }
+
+ private static List getAllFields(List fields, Class> type) {
+ for (Field field : type.getDeclaredFields()) {
+ fields.add(field);
+ }
+
+ if (type.getSuperclass() != null && type.getSuperclass() != Object.class) {
+ fields = getAllFields(fields, type.getSuperclass());
}
- return annotatedMethods;
+
+ return fields;
+ }
+
+
+ private static boolean containsModelAttribute(final Annotation[] annotations) {
+ for (final Annotation item : annotations) {
+ if (item instanceof ModelAttribute) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static ApiOperation findApiOperation(final List annotations) {
+ for (final Annotation item : annotations) {
+ if (item instanceof ApiOperation) {
+ return (ApiOperation) item;
+ }
+ }
+ return null;
+ }
+
+ private static ApiImplicitParams findApiImplicitParams(final List annotations) {
+ for (final Annotation item : annotations) {
+ if (item instanceof ApiImplicitParams) {
+ return (ApiImplicitParams) item;
+ }
+ }
+ return null;
+ }
+
+
+
+ private static AnnotatedParameter createParameter(final String paramName, final Class paramType,
+ final Type genericParameterType,
+ final List annotations) {
+ AnnotatedParameter annotatedParameter = new AnnotatedParameter();
+ annotatedParameter.setParameterClass(paramType);
+ annotatedParameter.setParameterName(paramName);
+ annotatedParameter.setParameterType(genericParameterType);
+ annotatedParameter.addAnnotations(annotations);
+ return annotatedParameter;
}
}
diff --git a/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java
index 9b48aa0..96e8727 100644
--- a/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java
+++ b/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java
@@ -1,11 +1,9 @@
package com.knappsack.swagger4springweb.util;
-import com.wordnik.swagger.converter.ModelConverters;
+import com.wordnik.swagger.converter.CustomModelConverters;
import com.wordnik.swagger.model.Model;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.multipart.MultipartFile;
-import scala.Option;
-import scala.collection.immutable.HashMap;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -68,11 +66,11 @@ static boolean isIgnorableModel(String name) {
}
public static void addModels(final Class> clazz, final Map models) {
- Option modelOption = ModelConverters.read(clazz, new HashMap<>());
- Model model;
- if(!modelOption.isEmpty()) {
- model = modelOption.get();
- if(!isIgnorableModel(model.name())) {
+ scala.collection.immutable.List modelOption = CustomModelConverters.readAll(clazz);
+ scala.collection.Iterator iterator = modelOption.iterator();
+ while (iterator.hasNext()) {
+ Model model = iterator.next();
+ if (!isIgnorableModel(model.name())) {
models.put(model.name(), model);
}
}
diff --git a/src/main/scala/com/knappsack/swagger4springweb/parser/SpringApiReader.scala b/src/main/scala/com/knappsack/swagger4springweb/parser/SpringApiReader.scala
index a813cbd..2a875a1 100644
--- a/src/main/scala/com/knappsack/swagger4springweb/parser/SpringApiReader.scala
+++ b/src/main/scala/com/knappsack/swagger4springweb/parser/SpringApiReader.scala
@@ -8,7 +8,6 @@ import com.wordnik.swagger.annotations._
import com.wordnik.swagger.core.ApiValues._
import java.lang.annotation.Annotation
-import java.lang.reflect.Method
class SpringApiReader() extends SpringMVCApiReader {
@@ -57,25 +56,20 @@ class SpringApiReader() extends SpringMVCApiReader {
else None
}
- def findSubresourceType(method: Method): Class[_] = {
- method.getReturnType
- }
-
- // def parseHttpMethod(method: Method, apiOperation: ApiOperation): String = {
- // if (apiOperation.httpMethod() != null && apiOperation.httpMethod().trim().length() > 0)
- // apiOperation.httpMethod().trim()
- // else {
- // val requestMapping = method.getAnnotation(classOf[org.springframework.web.bind.annotation.RequestMapping])
- // val requestMethod = requestMapping.method()(0)
- // if(requestMethod.equals(RequestMethod.GET)) ApiMethodType.GET
- // if(requestMethod.equals(RequestMethod.POST)) ApiMethodType.POST
- // if(requestMethod.equals(RequestMethod.PUT)) ApiMethodType.PUT
- // if(requestMethod.equals(RequestMethod.DELETE)) ApiMethodType.DELETE
- // if(requestMethod.equals(RequestMethod.HEAD)) ApiMethodType.HEAD
- //
- // null
- // }
- // }
- // Finds the type of the subresource this method produces, in case it's a subresource locator
+// def parseHttpMethod(method: Method, apiOperation: ApiOperation): String = {
+// if (apiOperation.httpMethod() != null && apiOperation.httpMethod().trim().length() > 0)
+// apiOperation.httpMethod().trim()
+// else {
+// val requestMapping = method.getAnnotation(classOf[org.springframework.web.bind.annotation.RequestMapping])
+// val requestMethod = requestMapping.method()(0)
+// if(requestMethod.equals(RequestMethod.GET)) ApiMethodType.GET
+// if(requestMethod.equals(RequestMethod.POST)) ApiMethodType.POST
+// if(requestMethod.equals(RequestMethod.PUT)) ApiMethodType.PUT
+// if(requestMethod.equals(RequestMethod.DELETE)) ApiMethodType.DELETE
+// if(requestMethod.equals(RequestMethod.HEAD)) ApiMethodType.HEAD
+//
+// null
+// }
+// }
}
diff --git a/src/main/scala/com/knappsack/swagger4springweb/parser/SpringMVCApiReader.scala b/src/main/scala/com/knappsack/swagger4springweb/parser/SpringMVCApiReader.scala
index 287f799..6d5218d 100644
--- a/src/main/scala/com/knappsack/swagger4springweb/parser/SpringMVCApiReader.scala
+++ b/src/main/scala/com/knappsack/swagger4springweb/parser/SpringMVCApiReader.scala
@@ -1,18 +1,26 @@
package com.knappsack.swagger4springweb.parser
-import java.lang.annotation.Annotation
-import java.lang.reflect.{Field, Method, Type}
-
-import com.knappsack.swagger4springweb.annotation.ApiExclude
import com.wordnik.swagger.annotations._
import com.wordnik.swagger.config._
+import com.wordnik.swagger.reader.{ ClassReader, ClassReaderUtils }
import com.wordnik.swagger.core._
import com.wordnik.swagger.core.util._
+import com.wordnik.swagger.core.ApiValues._
import com.wordnik.swagger.model._
-import com.wordnik.swagger.reader.{ClassReader, ClassReaderUtils}
+
import org.slf4j.LoggerFactory
+
+import java.lang.reflect.{ Method, Type, Field }
+import java.lang.annotation.Annotation
import org.springframework.web.bind.annotation._
+import com.wordnik.swagger.model.Parameter
+import scala.Some
+import scala.Tuple3
+import com.wordnik.swagger.model.ApiDescription
+import com.wordnik.swagger.model.Operation
+import com.wordnik.swagger.model.ResponseMessage
+import com.wordnik.swagger.model.ApiListing
import scala.collection.mutable.ListBuffer
@@ -24,10 +32,6 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
// decorates a Parameter based on annotations, returns None if param should be ignored
def processParamAnnotations(mutable: MutableParameter, paramAnnotations: Array[Annotation]): Option[Parameter]
- // Finds the type of the subresource this method produces, in case it's a subresource locator
- // In case it's not a subresource locator the entity type is returned
- def findSubresourceType(method: Method): Class[_]
-
def processDataType(paramType: Class[_], genericParamType: Type) = {
paramType.getName match {
case "[I" => "Array[int]"
@@ -76,23 +80,16 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
parentParams: List[Parameter],
parentMethods: ListBuffer[Method]
) = {
-// val api = method.getAnnotation(classOf[Api])
+ val api = method.getAnnotation(classOf[Api])
val responseClass = {
- if(apiOperation != null){
- val baseName = apiOperation.response.getName
- val output = apiOperation.responseContainer match {
- case "" => baseName
- case e: String => "%s[%s]".format(e, baseName)
- }
- output
- }
- else {
- if(!"javax.ws.rs.core.Response".equals(method.getReturnType.getCanonicalName))
- method.getReturnType.getName
- else
- "void"
+ val baseName = apiOperation.response.getName
+ val output = apiOperation.responseContainer match {
+ case "" => baseName
+ case e: String => "%s[%s]".format(e, baseName)
}
+ output
}
+
var paramAnnotations: Array[Array[java.lang.annotation.Annotation]] = null
var paramTypes: Array[java.lang.Class[_]] = null
var genericParamTypes: Array[java.lang.reflect.Type] = null
@@ -107,41 +104,27 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
genericParamTypes = parentMethods.map(pm => pm.getGenericParameterTypes).reduceRight(_ ++ _) ++ method.getGenericParameterTypes
}
- val (nickname, produces, consumes, protocols, authorizations) = {
- if(apiOperation != null) {
- (
- (if(apiOperation.nickname != null && apiOperation.nickname != "")
- apiOperation.nickname
- else
- method.getName
- ),
- Option(apiOperation.produces) match {
- case Some(e) if(e != "") => e.split(",").map(_.trim).toList
- case _ => method.getAnnotation(classOf[RequestMapping]).produces() match {
- case e: Array[String] => e.toList
- case _ => List()
- }
- },
- Option(apiOperation.consumes) match {
- case Some(e) if(e != "") => e.split(",").map(_.trim).toList
- case _ => method.getAnnotation(classOf[RequestMapping]).consumes() match {
- case e: Array[String] => e.toList
- case _ => List()
- }
- },
- Option(apiOperation.protocols) match {
- case Some(e) if(e != "") => e.split(",").map(_.trim).toList
- case _ => List()
- },
- Option(apiOperation.authorizations) match {
- case Some(e) => (for(a <- e) yield {
- val scopes = (for(s <- a.scopes) yield com.wordnik.swagger.model.AuthorizationScope(s.scope, s.description)).toArray
- new com.wordnik.swagger.model.Authorization(a.value, scopes)
- }).toList
- case _ => List()
- })
+ val produces = Option(apiOperation.produces) match {
+ case Some(e) if(e != "") => e.split(",").map(_.trim).toList
+ case _ => method.getAnnotation(classOf[RequestMapping]).produces() match {
+ case e: Array[String] => e.toList
+ case _ => List()
}
- else(method.getName, List(), List(), List(), List())
+ }
+ val consumes = Option(apiOperation.consumes) match {
+ case Some(e) if(e != "") => e.split(",").map(_.trim).toList
+ case _ => method.getAnnotation(classOf[RequestMapping]).consumes() match {
+ case e: Array[String] => e.toList
+ case _ => List()
+ }
+ }
+ val protocols = Option(apiOperation.protocols) match {
+ case Some(e) if(e != "") => e.split(",").map(_.trim).toList
+ case _ => List()
+ }
+ val authorizations = Option(apiOperation.authorizations) match {
+ case Some(e) if(e != "") => e.split(",").map(_.trim).toList
+ case _ => List()
}
val params = parentParams ++ (for((annotations, paramType, genericParamType) <- (paramAnnotations, paramTypes, genericParamTypes).zipped.toList) yield {
if(annotations.length > 0) {
@@ -149,6 +132,17 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
param.dataType = processDataType(paramType, genericParamType)
processParamAnnotations(param, annotations)
}
+ else if(paramTypes.size > 0) {
+ // it's a body param w/o annotations, which means POST. Only take the first one!
+ val p = paramTypes.head
+
+ val param = new MutableParameter
+ param.dataType = p.getName
+ param.name = TYPE_BODY
+ param.paramType = TYPE_BODY
+
+ Some(param.asParameter)
+ }
else None
}).flatten.toList
@@ -161,43 +155,38 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
LOGGER.debug("processing " + param)
val allowableValues = toAllowableValues(param.allowableValues)
Parameter(
- name = param.name,
- description = Option(readString(param.value)),
- defaultValue = Option(param.defaultValue).filter(_.trim.nonEmpty),
- required = param.required,
- allowMultiple = param.allowMultiple,
- dataType = param.dataType,
- allowableValues = allowableValues,
- paramType = param.paramType,
- paramAccess = Option(param.access).filter(_.trim.nonEmpty))
+ param.name,
+ Option(readString(param.value)),
+ Option(param.defaultValue).filter(_.trim.nonEmpty),
+ param.required,
+ param.allowMultiple,
+ param.dataType,
+ allowableValues,
+ param.paramType,
+ Option(param.access).filter(_.trim.nonEmpty))
}).toList
}
case _ => List()
}
}
- val (summary, notes, position) = {
- if(apiOperation != null) (apiOperation.value, apiOperation.notes, apiOperation.position)
- else ("","",0)
- }
-
Operation(
- method = parseHttpMethod(method, apiOperation),
- summary = summary,
- notes = notes,
- responseClass = responseClass,
- nickname = nickname,
- position = position,
- produces = produces,
- consumes = consumes,
- protocols = protocols,
- authorizations = authorizations,
- parameters = params ++ implicitParams,
- responseMessages = apiResponses,
- `deprecated` = Option(isDeprecated))
+ parseHttpMethod(method, apiOperation),
+ apiOperation.value,
+ apiOperation.notes,
+ responseClass,
+ method.getName,
+ apiOperation.position,
+ produces,
+ consumes,
+ protocols,
+ authorizations,
+ params ++ implicitParams,
+ apiResponses,
+ Option(isDeprecated))
}
- def readMethod(method: Method, parentParams: List[Parameter], parentMethods: ListBuffer[Method]): Option[Operation] = {
+ def readMethod(method: Method, parentParams: List[Parameter], parentMethods: ListBuffer[Method]) = {
val apiOperation = method.getAnnotation(classOf[ApiOperation])
val responseAnnotation = method.getAnnotation(classOf[ApiResponses])
val apiResponses = {
@@ -213,13 +202,7 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
}
val isDeprecated = Option(method.getAnnotation(classOf[Deprecated])).map(m => "true").getOrElse(null)
- //Don't process methods that are marked with ApiExclude or are synthetic (in the case of Java lambda expressions)
- val hidden = if(method.getAnnotation(classOf[ApiExclude]) != null || method.isSynthetic) true
- else if(apiOperation != null) apiOperation.hidden
- else false
-
- if(!hidden) Some(parseOperation(method, apiOperation, apiResponses, isDeprecated, parentParams, parentMethods))
- else None
+ parseOperation(method, apiOperation, apiResponses, isDeprecated, parentParams, parentMethods)
}
def appendOperation(endpoint: String, path: String, op: Operation, operations: ListBuffer[Tuple3[String, String, ListBuffer[Operation]]]) = {
@@ -233,10 +216,6 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
readRecursive(docRoot, "", cls, config, new ListBuffer[Tuple3[String, String, ListBuffer[Operation]]], new ListBuffer[Method])
}
- var ignoredRoutes: Set[String] = Set()
-
- def ignoreRoutes = ignoredRoutes
-
def readRecursive(
docRoot: String,
parentPath: String, cls: Class[_],
@@ -244,49 +223,30 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
operations: ListBuffer[Tuple3[String, String, ListBuffer[Operation]]],
parentMethods: ListBuffer[Method]): Option[ApiListing] = {
val api = cls.getAnnotation(classOf[Api])
- if(api == null || cls.getAnnotation(classOf[ApiExclude]) != null) return None
- val pathAnnotation = cls.getAnnotation(classOf[RequestMapping])
-
- val r = Option(api) match {
- case Some(api) => api.value
- case None => Option(pathAnnotation) match {
- case Some(p) => p.value()(0)
- case None => null
+ // must have @Api annotation to process!
+ if(api != null) {
+ val consumes = Option(api.consumes) match {
+ case Some(e) if(e != "") => e.split(",").map(_.trim).toList
+ case _ => cls.getAnnotation(classOf[RequestMapping]).consumes() match {
+ case e: Array[String] => e.toList
+ case _ => List()
+ }
}
- }
- if(r != null && !ignoreRoutes.contains(r)) {
-// var resourcePath = addLeadingSlash(r)
- val position = Option(api) match {
- case Some(api) => api.position
- case None => 0
+ val produces = Option(api.produces) match {
+ case Some(e) if(e != "") => e.split(",").map(_.trim).toList
+ case _ => cls.getAnnotation(classOf[RequestMapping]).produces() match {
+ case e: Array[String] => e.toList
+ case _ => List()
+ }
}
- val (consumes, produces, protocols, description) = {
- if(api != null){
- (Option(api.consumes) match {
- case Some(e) if(e != "") => e.split(",").map(_.trim).toList
- case _ => cls.getAnnotation(classOf[RequestMapping]).consumes() match {
- case e: Array[String] => e.toList
- case _ => List()
- }
- },
- Option(api.produces) match {
- case Some(e) if(e != "") => e.split(",").map(_.trim).toList
- case _ => cls.getAnnotation(classOf[RequestMapping]).produces() match {
- case e: Array[String] => e.toList
- case _ => List()
- }
- },
- Option(api.protocols) match {
- case Some(e) if(e != "") => e.split(",").map(_.trim).toList
- case _ => List()
- },
- api.description match {
- case e: String if(e != "") => Some(e)
- case _ => None
- }
- )}
- else ((List(), List(), List(), None))
+ val protocols = Option(api.protocols) match {
+ case Some(e) if(e != "") => e.split(",").map(_.trim).toList
+ case _ => List()
+ }
+ val description = api.description match {
+ case e: String if(e != "") => Some(e)
+ case _ => None
}
// look for method-level annotated properties
val parentParams: List[Parameter] = (for(field <- getAllFields(cls))
@@ -308,8 +268,9 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
}
).flatten.toList
- for(method <- cls.getDeclaredMethods) {
- val returnType = findSubresourceType(method)
+ for(method <- cls.getMethods) {
+ val returnType = method.getReturnType
+ method.getAnnotations.size
var path = ""
if(method.getAnnotation(classOf[RequestMapping]) != null) {
val paths = method.getAnnotation(classOf[RequestMapping]).value()
@@ -317,19 +278,17 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
path = paths(0)
}
}
-// val endpoint = (parentPath + pathFromMethod(method)).replace("//", "/")
val endpoint = parentPath + api.value + pathFromMethod(method)
Option(returnType.getAnnotation(classOf[Api])) match {
case Some(e) => {
val root = docRoot + api.value + pathFromMethod(method)
parentMethods += method
readRecursive(root, endpoint, returnType, config, operations, parentMethods)
- parentMethods -= method
}
case _ => {
- readMethod(method, parentParams, parentMethods) match {
- case Some(op) => appendOperation(endpoint, path, op, operations)
- case None => None
+ if(method.getAnnotation(classOf[ApiOperation]) != null) {
+ val op = readMethod(method, parentParams, parentMethods)
+ appendOperation(endpoint, path, op, operations)
}
}
}
@@ -356,7 +315,6 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
apiVersion = config.apiVersion,
swaggerVersion = config.swaggerVersion,
basePath = config.basePath,
-// resourcePath = addLeadingSlash(api.value),
resourcePath = addLeadingSlash(docRoot),
apis = ModelUtil.stripPackages(apis),
models = models,
@@ -364,7 +322,7 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
produces = produces,
consumes = consumes,
protocols = protocols,
- position = position)
+ position = api.position)
)
}
else None
@@ -413,14 +371,13 @@ trait SpringMVCApiReader extends ClassReader with ClassReaderUtils {
}
def parseHttpMethod(method: Method, op: ApiOperation): String = {
- if (op != null && op.httpMethod() != null && op.httpMethod().trim().length() > 0)
+ if (op.httpMethod() != null && op.httpMethod().trim().length() > 0)
op.httpMethod().trim
else {
- val requestMappingAnnotation = method.getAnnotation(classOf[RequestMapping])
- val requestMapping = if(requestMappingAnnotation != null && !requestMappingAnnotation.method().isEmpty) requestMappingAnnotation.method()(0) else "GET"
+ val requestMapping = method.getAnnotation(classOf[RequestMapping]).method()(0)
if(RequestMethod.GET.equals(requestMapping)) "GET"
else if(RequestMethod.DELETE.equals(requestMapping)) "DELETE"
- else if(RequestMethod.PATCH.equals(requestMapping)) "PATCH"
+// else if(method.getAnnotation(classOf[PATCH]) != RequestMethod.) "PATCH"
else if(RequestMethod.POST.equals(requestMapping)) "POST"
else if(RequestMethod.PUT.equals(requestMapping)) "PUT"
else if(RequestMethod.HEAD.equals(requestMapping)) "HEAD"
diff --git a/src/main/scala/com/knappsack/swagger4springweb/util/ApiListingUtil.scala b/src/main/scala/com/knappsack/swagger4springweb/util/ApiListingUtil.scala
deleted file mode 100644
index f8df1d6..0000000
--- a/src/main/scala/com/knappsack/swagger4springweb/util/ApiListingUtil.scala
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.knappsack.swagger4springweb.util
-
-import com.wordnik.swagger.model.{Authorization, ApiDescription, Model, ApiListing}
-
-object ApiListingUtil {
-
- def baseApiListing(apiVersion: String, swaggerVersion: String, basePath: String, resourcePath: String): ApiListing = {
- new ApiListing(apiVersion, swaggerVersion, basePath, resourcePath,
- List.empty[String], List.empty[String], List.empty[String], List.empty[Authorization], List.empty[ApiDescription], None, None, 0)
- }
-
- def mergeApiListing(apiListing1: ApiListing, apiListing2: ApiListing): ApiListing = {
-
- val mergedModels: Map[String, Model] = (apiListing1.models, apiListing2.models) match {
- case (Some(x), Some(y)) => apiListing1.models.get ++ apiListing2.models.get
- case (Some(x), None) => apiListing1.models.get
- case (None, Some(y)) => apiListing2.models.get
- case (None, None) => Map.empty[String, Model]
- }
-
- new ApiListing(apiListing1.apiVersion, apiListing1.swaggerVersion, apiListing1.basePath, apiListing1.resourcePath,
- (apiListing1.produces ++ apiListing2.produces).distinct, (apiListing1.consumes ++ apiListing2.consumes).distinct,
- (apiListing1.protocols ++ apiListing2.protocols).distinct, (apiListing1.authorizations ++ apiListing2.authorizations).distinct,
- (apiListing1.apis ++ apiListing2.apis).distinct, Option(mergedModels), apiListing1.description, apiListing1.position)
- }
-
-}
diff --git a/src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala b/src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala
new file mode 100644
index 0000000..6aba150
--- /dev/null
+++ b/src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala
@@ -0,0 +1,137 @@
+package com.wordnik.swagger.converter
+
+import com.wordnik.swagger.core._
+import com.wordnik.swagger.model._
+import org.slf4j.LoggerFactory
+
+import scala.collection.mutable.{HashMap, HashSet, ListBuffer}
+
+object CustomModelConverters {
+ private val LOGGER = LoggerFactory.getLogger(CustomModelConverters.getClass)
+ val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-\\$]*)\\].*".r // TODO fix for generics
+
+ val converters = new ListBuffer[ModelConverter]() ++ List(
+ new JodaDateTimeConverter,
+ new CustomSwaggerSchemaConverter
+ )
+
+ def addConverter(c: ModelConverter, first: Boolean = false) = {
+ if(first) {
+ val reordered = List(c) ++ converters
+ converters.clear
+ converters ++= reordered
+ }
+ else converters += c
+ }
+
+ def read(cls: Class[_]): Option[Model] = {
+ var model: Option[Model] = None
+ val itr = converters.iterator
+ while(model == None && itr.hasNext) {
+ itr.next.read(cls) match {
+ case Some(m) => {
+ val filteredProperties = m.properties.filter(prop => skippedClasses.contains(prop._2.qualifiedType) == false)
+ model = Some(m.copy(properties = filteredProperties))
+ }
+ case _ => model = None
+ }
+ }
+ model
+ }
+
+ def readAll(cls: Class[_]): List[Model] = {
+ val output = new HashMap[String, Model]
+ val model = read(cls)
+
+ // add subTypes
+ model.map(_.subTypes.map(typeRef => {
+ try{
+ LOGGER.debug("loading subtype " + typeRef)
+ val cls = SwaggerContext.loadClass(typeRef)
+ read(cls) match {
+ case Some(model) => output += cls.getName -> model
+ case _ =>
+ }
+ }
+ catch {
+ case e: Exception => LOGGER.error("can't load class " + typeRef)
+ }
+ }))
+
+ // add properties
+ model.map(m => {
+ output += cls.getName -> m
+ val checkedNames = new HashSet[String]
+ addRecursive(m, checkedNames, output)
+ })
+ output.values.toList
+ }
+
+ def addRecursive(model: Model, checkedNames: HashSet[String], output: HashMap[String, Model]): Unit = {
+ if(!checkedNames.contains(model.name)) {
+ val propertyNames = new HashSet[String]
+ for((name, property) <- model.properties) {
+ val propertyName = property.items match {
+ case Some(item) => item.qualifiedType.getOrElse(item.`type`)
+ case None => property.qualifiedType
+ }
+ val name = propertyName match {
+ case ComplexTypeMatcher(containerType, basePart) => basePart
+ case e: String => e
+ }
+ propertyNames += name
+ }
+ for(typeRef <- propertyNames) {
+ if(ignoredPackages.contains(getPackage(typeRef))) None
+ else if(ignoredClasses.contains(typeRef)) {
+ None
+ }
+ else if(!checkedNames.contains(typeRef)) {
+ try{
+ checkedNames += typeRef
+ val cls = SwaggerContext.loadClass(typeRef)
+ LOGGER.debug("reading class " + cls)
+ CustomModelConverters.read(cls) match {
+ case Some(model) => {
+ output += typeRef -> model
+ addRecursive(model, checkedNames, output)
+ }
+ case None =>
+ }
+ }
+ catch {
+ case e: ClassNotFoundException =>
+ }
+ }
+ }
+ }
+ }
+
+ def toName(cls: Class[_]): String = {
+ var name: String = null
+ val itr = converters.iterator
+ while(name == null && itr.hasNext) {
+ name = itr.next.toName(cls)
+ }
+ name
+ }
+
+ def getPackage(str: String): String = {
+ str.lastIndexOf(".") match {
+ case -1 => ""
+ case e: Int => str.substring(0, e)
+ }
+ }
+
+ def ignoredPackages: Set[String] = {
+ (for(converter <- converters) yield converter.ignoredPackages).flatten.toSet
+ }
+
+ def ignoredClasses: Set[String] = {
+ (for(converter <- converters) yield converter.ignoredClasses).flatten.toSet
+ }
+
+ def skippedClasses: Set[String] = {
+ (for(converter <- converters) yield converter.skippedClasses).flatten.toSet
+ }
+}
diff --git a/src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala b/src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala
new file mode 100644
index 0000000..b001791
--- /dev/null
+++ b/src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala
@@ -0,0 +1,115 @@
+package com.wordnik.swagger.converter
+
+import java.lang.annotation.Annotation
+import java.lang.reflect.Type
+
+import com.wordnik.swagger.model._
+import org.slf4j.LoggerFactory
+
+import scala.collection.mutable.LinkedHashMap
+
+class CustomModelPropertyParser(cls: Class[_])(implicit properties: LinkedHashMap[String, ModelProperty]) extends ModelPropertyParser(cls) {
+ private val LOGGER = LoggerFactory.getLogger(classOf[ModelPropertyParser])
+
+ override def parsePropertyAnnotations(returnClass: Class[_], propertyName: String, propertyAnnotations: Array[Annotation], genericReturnType: Type, returnType: Type): Any = {
+ val e = extractGetterProperty(propertyName)
+ var name = e._1
+ val isGetter = e._2
+
+ var isFieldExists = false
+ var isJsonProperty = false
+ val hasAccessorNoneAnnotation = false
+ val processedAnnotations = processAnnotations(name, propertyAnnotations)
+ var required = processedAnnotations("required").asInstanceOf[Boolean]
+ var position = processedAnnotations("position").asInstanceOf[Int]
+
+ var description = {
+ if (processedAnnotations.contains("description") && processedAnnotations("description") != null)
+ Some(processedAnnotations("description").asInstanceOf[String])
+ else None
+ }
+ var isTransient = processedAnnotations("isTransient").asInstanceOf[Boolean]
+ var isXmlElement = processedAnnotations("isXmlElement").asInstanceOf[Boolean]
+ val isDocumented = processedAnnotations("isDocumented").asInstanceOf[Boolean]
+ var allowableValues = {
+ if (returnClass.isEnum)
+ Some(AllowableListValues((for (v <- returnClass.getEnumConstants) yield v.toString).toList))
+ else
+ processedAnnotations("allowableValues").asInstanceOf[Option[AllowableValues]]
+ }
+
+ try {
+ val fieldAnnotations = getDeclaredField(this.cls, name).getAnnotations()
+ val propAnnoOutput = processAnnotations(name, fieldAnnotations)
+ val propPosition = propAnnoOutput("position").asInstanceOf[Int]
+
+ if (allowableValues == None)
+ allowableValues = propAnnoOutput("allowableValues").asInstanceOf[Option[AllowableValues]]
+ if (description == None && propAnnoOutput.contains("description") && propAnnoOutput("description") != null)
+ description = Some(propAnnoOutput("description").asInstanceOf[String])
+ if (propPosition != 0) position = propAnnoOutput("position").asInstanceOf[Int]
+ if (required == false) required = propAnnoOutput("required").asInstanceOf[Boolean]
+ isFieldExists = true
+ if (!isTransient) isTransient = propAnnoOutput("isTransient").asInstanceOf[Boolean]
+ if (!isXmlElement) isXmlElement = propAnnoOutput("isXmlElement").asInstanceOf[Boolean]
+ isJsonProperty = propAnnoOutput("isJsonProperty").asInstanceOf[Boolean]
+ } catch {
+ //this means there is no field declared to look for field level annotations.
+ case e: java.lang.NoSuchFieldException => isTransient = false
+ }
+
+ //if class has accessor none annotation, the method/field should have explicit xml element annotations, if not
+ // consider it as transient
+ if (!isXmlElement && hasAccessorNoneAnnotation)
+ isTransient = true
+
+ if (!(isTransient && !isXmlElement && !isJsonProperty) && name != null && (isFieldExists || isGetter || isDocumented)) {
+ var paramType = getDataType(genericReturnType, returnType, false)
+ LOGGER.debug("inspecting " + paramType)
+ var simpleName = getDataType(genericReturnType, returnType, true)
+
+ if (!"void".equals(paramType) && null != paramType && !processedFields.contains(name)) {
+ if (!excludedFieldTypes.contains(paramType)) {
+ val items = {
+ val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-\\$0-9_]*)\\].*".r // TODO fix for generics
+ paramType match {
+ case ComplexTypeMatcher(containerType, basePart) => {
+ LOGGER.debug("containerType: " + containerType + ", basePart: " + basePart + ", simpleName: " + simpleName)
+ paramType = containerType
+ val ComplexTypeMatcher(t, simpleTypeRef) = simpleName
+ val typeRef = {
+ if (simpleTypeRef.indexOf(",") > 0) // it's a map, use the value only
+ simpleTypeRef.split(",").last
+ else simpleTypeRef
+ }
+ simpleName = containerType
+ if (isComplex(simpleTypeRef)) {
+ Some(ModelRef(null, Some(simpleTypeRef), Some(basePart)))
+ }
+ else Some(ModelRef(simpleTypeRef, None, Some(basePart)))
+ }
+ case _ => None
+ }
+ }
+ val param = ModelProperty(
+ validateDatatype(simpleName),
+ paramType,
+ position,
+ required,
+ description,
+ allowableValues.getOrElse(AnyAllowableValues),
+ items)
+ LOGGER.debug("added param type " + paramType + " for field " + name)
+ properties += name -> param
+ }
+ else {
+ LOGGER.debug("field " + paramType + " is has been explicitly excluded")
+ }
+ }
+ else {
+ LOGGER.debug("skipping " + name)
+ }
+ processedFields += name
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala b/src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala
new file mode 100644
index 0000000..7f30882
--- /dev/null
+++ b/src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala
@@ -0,0 +1,65 @@
+package com.wordnik.swagger.converter
+
+import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
+import com.wordnik.swagger.annotations.ApiModel
+import com.wordnik.swagger.model._
+
+import scala.collection.mutable.LinkedHashMap
+
+class CustomSwaggerSchemaConverter
+ extends ModelConverter
+ with BaseConverter {
+
+ def read(cls: Class[_]): Option[Model] = {
+ Option(cls).flatMap({
+ cls => {
+ implicit val properties = new LinkedHashMap[String, ModelProperty]()
+ new CustomModelPropertyParser(cls).parse
+
+ val p = (for((key, value) <- properties)
+ yield (value.position, key, value)
+ ).toList
+
+ val sortedProperties = new LinkedHashMap[String, ModelProperty]()
+ p.sortWith(_._1 < _._1).foreach(e => sortedProperties += e._2 -> e._3)
+
+ val parent = Option(cls.getAnnotation(classOf[ApiModel])) match {
+ case Some(e) => Some(e.parent.getName)
+ case _ => None
+ }
+ val discriminator = {
+ val v = {
+ val apiAnno = cls.getAnnotation(classOf[ApiModel])
+ if(apiAnno != null && apiAnno.discriminator != null)
+ apiAnno.discriminator
+ else if(cls.getAnnotation(classOf[JsonTypeInfo]) != null)
+ cls.getAnnotation(classOf[JsonTypeInfo]).property
+ else ""
+ }
+ if(v != null && v != "") Some(v)
+ else None
+ }
+ val subTypes = {
+ if(cls.getAnnotation(classOf[ApiModel]) != null)
+ cls.getAnnotation(classOf[ApiModel]).subTypes.map(_.getName).toList
+ else if(cls.getAnnotation(classOf[JsonSubTypes]) != null)
+ (for(subType <- cls.getAnnotation(classOf[JsonSubTypes]).value) yield (subType.value.getName)).toList
+ else List()
+ }
+ sortedProperties.size match {
+ case 0 => None
+ case _ => Some(Model(
+ toName(cls),
+ toName(cls),
+ cls.getName,
+ sortedProperties,
+ toDescriptionOpt(cls),
+ parent,
+ discriminator,
+ subTypes
+ ))
+ }
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java b/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java
index 203d71e..81282d1 100644
--- a/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java
+++ b/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java
@@ -9,6 +9,6 @@ public abstract class AbstractTest {
public static final String BASE_CONTROLLER_PACKAGE = "com.knappsack.swagger4springweb.testController";
public static final String EXCLUDE_LABEL = "exclude";
- public static final List END_POINT_PATHS = Arrays.asList("/doc/api/v1/partialExclude", "/doc/api/v1/test", "/doc/api/v1/exclude2", "/doc/com.knappsack.swagger4springweb.testController.NoClassLevelMappingController", "/doc/api/v1/empty");
+ public static final List END_POINT_PATHS = Arrays.asList("/doc/api/v1/partialExclude", "/doc/api/v1/test", "/doc/api/v1/exclude2");
public static final ApiInfo API_INFO = new ApiInfo("swagger4spring-web example app", "This is a basic web app for demonstrating swagger4spring-web", "http://localhost:8080/terms", "http://localhost:8080/contact", "MIT", "http://opensource.org/licenses/MIT");
}
diff --git a/src/test/java/com/knappsack/swagger4springweb/AnnotationUtilsTest.java b/src/test/java/com/knappsack/swagger4springweb/AnnotationUtilsTest.java
index 8a05fe2..719f63c 100644
--- a/src/test/java/com/knappsack/swagger4springweb/AnnotationUtilsTest.java
+++ b/src/test/java/com/knappsack/swagger4springweb/AnnotationUtilsTest.java
@@ -6,13 +6,11 @@
import com.wordnik.swagger.annotations.ApiParam;
import org.junit.Test;
import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;
-import java.util.Set;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
@@ -86,10 +84,4 @@ public void testMethodHasNoApiParamAnnotation() throws NoSuchMethodException {
assertFalse(hasApiParam);
}
- @Test
- public void testGetMethodsWithAnnotation() {
- Set methods = AnnotationUtils.getAnnotatedMethods(MockController.class, RequestMapping.class);
- assertTrue(methods.size() == 4);
- }
-
}
diff --git a/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java b/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java
index 085b2c4..ad64ead 100644
--- a/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java
+++ b/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java
@@ -8,7 +8,7 @@
import com.wordnik.swagger.core.util.JsonSerializer;
import com.wordnik.swagger.model.ApiDescription;
import com.wordnik.swagger.model.ApiListing;
-import com.wordnik.swagger.model.ApiListingReference;
+import com.wordnik.swagger.model.Operation;
import com.wordnik.swagger.model.ResourceListing;
import org.junit.Test;
@@ -47,13 +47,15 @@ public void testOperationApiExclude() {
ApiParser apiParser = createApiParser(Arrays.asList(BASE_CONTROLLER_PACKAGE + ".exclude"));
Map documents = apiParser.createApiListings();
- assertEquals(1, documents.size()); // ExcludeClassTestController excluded completely
+ assertEquals(2, documents.size()); // ExcludeClassTestController excluded completely
// validate that we don't expose any excluded operations in the documents
for (ApiListing documentation : documents.values()) {
- assertTrue(ScalaToJavaUtil.toJavaList(documentation.apis()).size() == 2);
for (ApiDescription api : ScalaToJavaUtil.toJavaList(documentation.apis())) {
- assertTrue(ScalaToJavaUtil.toJavaList(api.operations()).size() == 1);
+ for (Operation op : ScalaToJavaUtil.toJavaList(api.operations())) {
+ assertTrue("The operation " + op.nickname() + " should be excluded",
+ "exclude".equals(op.summary()));
+ }
}
}
}
@@ -73,16 +75,6 @@ public void testResourceListing() {
// }
}
- @Test
- public void testNoClassRequestMapping() {
- ApiParser apiParser = createApiParser();
- Map documentList = apiParser.createApiListings();
- ResourceListing resourceList = apiParser.getResourceListing(documentList);
- for (ApiListingReference api: ScalaToJavaUtil.toJavaList(resourceList.apis())) {
- assertNotNull("could not get api listing for path: " + api.path(), documentList.get(api.path().substring(4))); // each api should be accessible using the ApiListingReference minus /doc
- }
- }
-
private ApiParser createApiParser() {
return createApiParser(Arrays.asList(BASE_CONTROLLER_PACKAGE));
}
diff --git a/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java b/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java
index 589005d..92ddaaa 100644
--- a/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java
+++ b/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java
@@ -40,7 +40,7 @@ public void getResourceList() {
assertNotNull(documentation);
assertEquals("v1", documentation.apiVersion());
- assertEquals(3, documentation.apis().size());
+ assertEquals(END_POINT_PATHS.size(), documentation.apis().size());
for (ApiListingReference endPoint : ScalaToJavaUtil.toJavaList(documentation.apis())) {
assertTrue(END_POINT_PATHS.contains(endPoint.path()));
diff --git a/src/test/java/com/knappsack/swagger4springweb/testController/NoClassLevelMappingController.java b/src/test/java/com/knappsack/swagger4springweb/testController/NoClassLevelMappingController.java
deleted file mode 100644
index 72174ec..0000000
--- a/src/test/java/com/knappsack/swagger4springweb/testController/NoClassLevelMappingController.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.knappsack.swagger4springweb.testController;
-
-import com.knappsack.swagger4springweb.testModels.MockPojo;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.ResponseBody;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Controller
-public class NoClassLevelMappingController {
-
- @RequestMapping(value = "/api/v1/noClassLevelMapping", method = RequestMethod.GET, produces = "application/json")
- public
- @ResponseBody
- List noRoot() {
- return new ArrayList();
- }
-}