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 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 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(); - } -}