From 97c4526ccbf4fa3d3473c240de04cfafbd6a5efc Mon Sep 17 00:00:00 2001 From: Andrey Antonov Date: Mon, 17 Mar 2014 17:03:40 +0400 Subject: [PATCH 01/10] Bug fixed with @ModelAttribute parameters: they should have "body" type, not "form" --- .../parser/ApiOperationParser.java | 13 ++++++++++--- .../parser/ApiParameterParser.java | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java index 33f5155..4f3c17d 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java @@ -18,11 +18,12 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; 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 { @@ -78,8 +79,8 @@ public Operation parseDocumentationOperation(Method method) { httpMethod += requestMethod.name() + " "; } documentationOperation.setHttpMethod(httpMethod.trim()); - documentationOperation.addConsumes(Arrays.asList(methodRequestMapping.consumes())); - documentationOperation.addProduces(Arrays.asList(methodRequestMapping.produces())); + documentationOperation.addConsumes(asList(methodRequestMapping.consumes())); + documentationOperation.addProduces(asList(methodRequestMapping.produces())); // get ApiOperation information ApiOperation apiOperation = method.getAnnotation(ApiOperation.class); @@ -165,6 +166,8 @@ class DocumentationOperation { private List authorizations = new ArrayList(); Operation toScalaOperation() { + final List consumes = getConsumes(); + return new Operation(httpMethod, summary, notes, @@ -288,5 +291,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..43609e9 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java @@ -102,7 +102,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) { From 7d89f8be285879166fea29a97d69a61833cd68e0 Mon Sep 17 00:00:00 2001 From: Andrey Antonov Date: Mon, 24 Mar 2014 14:48:14 +0400 Subject: [PATCH 02/10] Don't cache basePath URL, since it caches URL from first request's referrer --- .../ApiDocumentationController.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java index 7055ed6..9e9e2b9 100644 --- a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java +++ b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java @@ -73,24 +73,26 @@ 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) { + 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("Unknown basePath"); } private Map getDocs(HttpServletRequest request) { From 8246a5a61f4313b64eaa2d81fe3741392907d3ff Mon Sep 17 00:00:00 2001 From: Andrey Antonov Date: Wed, 14 May 2014 10:40:49 +0400 Subject: [PATCH 03/10] Caching per filters list --- .../ApiDocumentationController.java | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java index 9e9e2b9..4bc1546 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.Lists; +import com.google.common.collect.Maps; import com.knappsack.swagger4springweb.filter.Filter; import com.knappsack.swagger4springweb.parser.ApiParser; import com.knappsack.swagger4springweb.parser.ApiParserImpl; @@ -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 Map, Map> documentationCache = Maps.newHashMap(); + private Map, ResourceListing> resourceListingCache = Maps.newHashMap(); + @RequestMapping(value = "/resourceList", method = RequestMethod.GET, produces = "application/json") public @ResponseBody @@ -84,7 +87,7 @@ public String getBasePath() { // requested from String referer = request.getHeader("Referer"); - if (basePathFromReferer && referer != null) { + if (basePathFromReferer && referer != null && !referer.isEmpty()) { return referer.substring(0, referer.lastIndexOf("/")); } else { String mapping = request.getServletPath(); @@ -92,34 +95,44 @@ public String getBasePath() { return requestURL.substring(0, requestURL.indexOf(mapping)); } } - throw new RuntimeException("Unknown 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(); + Map documentation = documentationCache.get(filters); + 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, filters); + documentation = apiParser.createApiListings(); + documentationCache.put(filters != null ? Lists.newArrayList(filters) : null, 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)); + ResourceListing resourceListing = resourceListingCache.get(filters); + 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, filters); + resourceListing = apiParser.getResourceListing(getDocs(request)); + resourceListingCache.put(filters != null ? Lists.newArrayList(filters) : null, resourceListing); + + return resourceListing; } private List getControllerPackages() { @@ -135,11 +148,6 @@ private List getControllerPackages() { return controllerPackages; } - @SuppressWarnings("unused") - public void setResourceList(ResourceListing resourceList) { - this.resourceList = resourceList; - } - @SuppressWarnings("unused") public String getBaseControllerPackage() { return baseControllerPackage; @@ -180,16 +188,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; From ddc98db3a0ef73810d5e982d33bf1ca4fcb80724 Mon Sep 17 00:00:00 2001 From: Andrey Antonov Date: Fri, 30 May 2014 18:09:18 +0400 Subject: [PATCH 04/10] @ModelAttribute contatining methods better parsing --- .../parser/ApiParameterParser.java | 33 ++++++ .../util/AnnotationUtils.java | 103 +++++++++++++++++- 2 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java index 43609e9..256765f 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()); @@ -173,6 +178,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/util/AnnotationUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java index 5e3df60..19a9f12 100644 --- a/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java +++ b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java @@ -4,13 +4,19 @@ import com.knappsack.swagger4springweb.model.AnnotatedParameter; import com.thoughtworks.paranamer.BytecodeReadingParanamer; import com.thoughtworks.paranamer.Paranamer; +import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiParam; +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 java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class AnnotationUtils { @@ -69,19 +75,104 @@ public static List getAnnotatedParameters(Method 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++; } return annotatedParameters; } + + 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 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 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; + } } From 60066105b7921ea60d3937950819a59c163c3267 Mon Sep 17 00:00:00 2001 From: Andrey Antonov Date: Tue, 28 Apr 2015 16:50:45 +0300 Subject: [PATCH 05/10] Fixing bug which causes inner classes be skipped during recursive lookup of models. So the inner classes marked with $ sign like List[com.something.Container$Inner] and regexp doesn't contain $ val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-]*)\\].*".r should be val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-\\$]*)\\].*".r --- .../parser/ApiOperationParser.java | 6 +- .../swagger4springweb/util/ModelUtils.java | 4 +- .../converter/CustomModelConverters.scala | 137 ++++++++++++++++++ .../converter/CustomModelPropertyParser.scala | 115 +++++++++++++++ .../CustomSwaggerSchemaConverter.scala | 65 +++++++++ 5 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala create mode 100644 src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala create mode 100644 src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java index 8c16519..57e5517 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java @@ -4,7 +4,7 @@ import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiResponse; import com.wordnik.swagger.annotations.ApiResponses; -import com.wordnik.swagger.converter.ModelConverters; +import com.wordnik.swagger.converter.CustomModelConverters; import com.wordnik.swagger.model.Model; import com.wordnik.swagger.model.Operation; import com.wordnik.swagger.model.Parameter; @@ -193,7 +193,7 @@ void setResponseClass(Class responseClass) { return; } - Option model = ModelConverters.read(responseClass); + Option model = CustomModelConverters.read(responseClass); if (model.nonEmpty()) { this.responseClass = model.get().name(); } else { @@ -280,7 +280,7 @@ public void setResponseContainer(final String container) { } public void setResponseContainer(final Class type) { - Option model = ModelConverters.read(type); + Option model = CustomModelConverters.read(type); if (model.nonEmpty()) { setResponseContainer(model.get().name()); } else { diff --git a/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java index 6f5e5b5..96e8727 100644 --- a/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java +++ b/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java @@ -1,6 +1,6 @@ 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; @@ -66,7 +66,7 @@ static boolean isIgnorableModel(String name) { } public static void addModels(final Class clazz, final Map models) { - scala.collection.immutable.List modelOption = ModelConverters.readAll(clazz); + scala.collection.immutable.List modelOption = CustomModelConverters.readAll(clazz); scala.collection.Iterator iterator = modelOption.iterator(); while (iterator.hasNext()) { Model model = iterator.next(); 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 From 4a685e9ad07fb5cb43110e3d85d5739439e22fa1 Mon Sep 17 00:00:00 2001 From: Andrey Antonov Date: Mon, 17 Aug 2015 13:20:32 +0300 Subject: [PATCH 06/10] Improved apiImplicitParam handling --- pom.xml | 2 +- .../parser/ApiOperationParser.java | 43 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 828f5d4..f1467fe 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.knappsack swagger4spring-web jar - 0.3.10-SNAPSHOT-ALERTME + 0.3.12-SNAPSHOT-ALERTME Swagger implementation for the Spring-Web framework https://github.com/wkennedy/swagger4spring-web diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java index 57e5517..8f696d6 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java @@ -1,10 +1,9 @@ package com.knappsack.swagger4springweb.parser; import com.knappsack.swagger4springweb.util.JavaToScalaUtil; -import com.wordnik.swagger.annotations.ApiOperation; -import com.wordnik.swagger.annotations.ApiResponse; -import com.wordnik.swagger.annotations.ApiResponses; +import com.wordnik.swagger.annotations.*; 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; @@ -14,6 +13,7 @@ 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; @@ -113,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(); } @@ -125,18 +125,49 @@ 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 Parameter addApiImplicitParams(Parameter documentationParameter, Method method) { + Annotation[] annotations = method.getDeclaredAnnotations(); + + 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 documentationParameter; + } + + 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 null; + } + private boolean isParameterPresented(DocumentationOperation documentationOperation, String parameter) { if (documentationOperation.getParameters().isEmpty()) { return false; From 961b2bc2cc5e08c85171fd8f823fecb939de224a Mon Sep 17 00:00:00 2001 From: Eugene Rubanov Date: Thu, 11 Aug 2016 09:56:19 +0100 Subject: [PATCH 07/10] add request scoped filters --- pom.xml | 2 +- .../ApiDocumentationController.java | 47 ++++++++++++++----- .../parser/ApiParserImpl.java | 2 +- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index f1467fe..6371228 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.knappsack swagger4spring-web jar - 0.3.12-SNAPSHOT-ALERTME + 0.3.13-SNAPSHOT-ALERTME Swagger implementation for the Spring-Web framework https://github.com/wkennedy/swagger4spring-web diff --git a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java index 4bc1546..2a11662 100644 --- a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java +++ b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java @@ -1,7 +1,7 @@ package com.knappsack.swagger4springweb.controller; -import com.google.common.collect.Lists; 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; @@ -17,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(); @@ -45,8 +45,8 @@ public class ApiDocumentationController { private ApiInfo apiInfo; private List filters; - private Map, Map> documentationCache = Maps.newHashMap(); - private Map, ResourceListing> resourceListingCache = Maps.newHashMap(); + private final Map, Map> documentationCache = Maps.newHashMap(); + private final Map, ResourceListing> resourceListingCache = Maps.newHashMap(); @RequestMapping(value = "/resourceList", method = RequestMethod.GET, produces = "application/json") public @@ -99,7 +99,9 @@ public String getBasePath() { } private Map getDocs(HttpServletRequest request) { - Map documentation = documentationCache.get(filters); + Set fullSetOfFilters = mergeFilters(getRequestFilters(request)); + + Map documentation = documentationCache.get(fullSetOfFilters); if (documentation != null) { return documentation; } @@ -109,15 +111,17 @@ private Map getDocs(HttpServletRequest request) { servletPath = request.getServletPath(); } ApiParser apiParser = new ApiParserImpl(apiInfo, getControllerPackages(), getBasePath(), - servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, filters); + servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, fullSetOfFilters); documentation = apiParser.createApiListings(); - documentationCache.put(filters != null ? Lists.newArrayList(filters) : null, documentation); + documentationCache.put(fullSetOfFilters, documentation); return documentation; } private ResourceListing getResourceList(HttpServletRequest request) { - ResourceListing resourceListing = resourceListingCache.get(filters); + Set fullSetOfFilters = mergeFilters(getRequestFilters(request)); + + ResourceListing resourceListing = resourceListingCache.get(fullSetOfFilters); if (resourceListing != null) { return resourceListing; } @@ -128,9 +132,9 @@ private ResourceListing getResourceList(HttpServletRequest request) { servletPath = servletPath.replace("/resourceList", ""); } ApiParser apiParser = new ApiParserImpl(apiInfo, getControllerPackages(), getBasePath(), - servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, filters); + servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables, fullSetOfFilters); resourceListing = apiParser.getResourceListing(getDocs(request)); - resourceListingCache.put(filters != null ? Lists.newArrayList(filters) : null, resourceListing); + resourceListingCache.put(fullSetOfFilters, resourceListing); return resourceListing; } @@ -148,6 +152,17 @@ private List getControllerPackages() { return controllerPackages; } + 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") public String getBaseControllerPackage() { return baseControllerPackage; @@ -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/ApiParserImpl.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java index 8ad72c9..9494931 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java @@ -36,7 +36,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; From dfc4a17690a3878e91b52bf94923b5b948a2b544 Mon Sep 17 00:00:00 2001 From: Eugene Rubanov Date: Thu, 11 Aug 2016 11:51:22 +0100 Subject: [PATCH 08/10] bump version to 0.3.14-.... --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6371228..eb7f0b0 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.knappsack swagger4spring-web jar - 0.3.13-SNAPSHOT-ALERTME + 0.3.14-SNAPSHOT-ALERTME Swagger implementation for the Spring-Web framework https://github.com/wkennedy/swagger4spring-web From 922ae2fa8723c848dd9ee44b776ba96085743649 Mon Sep 17 00:00:00 2001 From: Eugene Rubanov Date: Thu, 20 Jul 2017 15:15:23 +0100 Subject: [PATCH 09/10] apply Yuriy's changes for version 15 and using new reflection --- pom.xml | 4 ++-- .../knappsack/swagger4springweb/parser/ApiParserImpl.java | 5 +++-- .../controller/ApiDocumentationControllerTest.java | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index eb7f0b0..4dfd28c 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.knappsack swagger4spring-web jar - 0.3.14-SNAPSHOT-ALERTME + 0.3.15-SNAPSHOT-ALERTME Swagger implementation for the Spring-Web framework https://github.com/wkennedy/swagger4spring-web @@ -43,7 +43,7 @@ 4.9 1.6 2.3.0 - 0.9.8 + 0.9.10 diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java index 9494931..983ec18 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java @@ -9,6 +9,7 @@ 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.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -97,8 +98,8 @@ private Map processControllers(Set> controllerClass continue; } - Set requestMappingMethods = Reflections - .getAllMethods(controllerClass, withAnnotation(RequestMapping.class)); + Set requestMappingMethods = + ReflectionUtils.getAllMethods(controllerClass, withAnnotation(RequestMapping.class)); ApiListing apiListing = processControllerApi(controllerClass); String description = ""; Api controllerApi = controllerClass.getAnnotation(Api.class); diff --git a/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java b/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java index 027844d..92ddaaa 100644 --- a/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java +++ b/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java @@ -41,7 +41,6 @@ public void getResourceList() { assertNotNull(documentation); assertEquals("v1", documentation.apiVersion()); assertEquals(END_POINT_PATHS.size(), documentation.apis().size()); - assertEquals("/doc/api/v1/test", documentation.apis().apply(0).path()); for (ApiListingReference endPoint : ScalaToJavaUtil.toJavaList(documentation.apis())) { assertTrue(END_POINT_PATHS.contains(endPoint.path())); From e48e359428caa53cb29bef32ee0261f8b79872b4 Mon Sep 17 00:00:00 2001 From: Irina Maximenko Date: Fri, 21 Jul 2017 16:14:36 +0300 Subject: [PATCH 10/10] PT2-1043 Add support of @ApiImplicitParam to swagger4spring-web (#4) * PT2-1043 Add support of @ApiImplicitParam to swagger4spring-web * PT2-1043 Add support of @ApiImplicitParam to swagger4spring-web --- pom.xml | 2 +- .../parser/ApiParameterParser.java | 2 + .../util/AnnotationUtils.java | 48 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4dfd28c..944d7a9 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.knappsack swagger4spring-web jar - 0.3.15-SNAPSHOT-ALERTME + 0.3.16-SNAPSHOT-ALERTME Swagger implementation for the Spring-Web framework https://github.com/wkennedy/swagger4spring-web diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java index 256765f..d9557ae 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParameterParser.java @@ -81,6 +81,8 @@ public List parseApiParametersAndArgumentModels(Method method) { documentationParameters.add(parameter); } + documentationParameters.addAll(AnnotationUtils.getImplicitParameters(method)); + return documentationParameters; } diff --git a/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java index 19a9f12..d773ae2 100644 --- a/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java +++ b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java @@ -4,12 +4,18 @@ 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; @@ -95,6 +101,39 @@ public static List getAnnotatedParameters(Method method) { return annotatedParameters; } + /** + * @param method + * Method + * @return List - if the method is annotated with ApiImplicitParams that contain parameters definitions + */ + 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(); @@ -163,6 +202,15 @@ private static ApiOperation findApiOperation(final List annotations) 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,