package com.github.kongchen.swagger.docgen.spring; import com.fasterxml.jackson.databind.JavaType; import com.google.common.base.Strings; import com.google.common.collect.Lists; import io.swagger.annotations.ApiParam; import io.swagger.converter.ModelConverters; import io.swagger.jaxrs.ext.AbstractSwaggerExtension; import io.swagger.jaxrs.ext.SwaggerExtension; import io.swagger.models.Swagger; import io.swagger.models.parameters.*; import io.swagger.models.properties.ArrayProperty; import io.swagger.models.properties.FileProperty; import io.swagger.models.properties.Property; import io.swagger.models.properties.StringProperty; import io.swagger.util.ParameterProcessor; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.MethodUtils; import org.apache.commons.lang3.reflect.TypeUtils; import org.apache.maven.plugin.logging.Log; import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.*; /** * @author chekong on 15/4/27. */ public class SpringSwaggerExtension extends AbstractSwaggerExtension { private final static String DEFAULT_VALUE = "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n"; private static final RequestParam DEFAULT_REQUEST_PARAM = (RequestParam)MethodUtils.getMatchingMethod(AnnotationBearer.class, "get", String.class).getParameterAnnotations()[0][0]; private Log log; // Class specificly for holding default value annotations private static class AnnotationBearer { /** * Only used to get annotations.. * @param requestParam ignore this */ public void get(@RequestParam String requestParam) { } } public SpringSwaggerExtension(Log log) { this.log = log; } @Override public List<Parameter> extractParameters(List<Annotation> annotations, Type type, Set<Type> typesToSkip, Iterator<SwaggerExtension> chain) { if (this.shouldIgnoreType(type, typesToSkip)) { return new ArrayList<Parameter>(); } if (annotations.isEmpty()) { // Method arguments are not required to have any annotations annotations = Lists.newArrayList((Annotation) null); } Map<Class<?>, Annotation> annotationMap = toMap(annotations); List<Parameter> parameters = new ArrayList<Parameter>(); parameters.addAll(extractParametersFromModelAttributeAnnotation(type, annotationMap)); parameters.addAll(extractParametersFromAnnotation(type, annotationMap)); if (!parameters.isEmpty()) { return parameters; } return super.extractParameters(annotations, type, typesToSkip, chain); } private Map<Class<?>, Annotation> toMap(Collection<? extends Annotation> annotations) { Map<Class<?>, Annotation> annotationMap = new HashMap<>(); for (Annotation annotation : annotations) { if (annotation == null) { continue; } annotationMap.put(annotation.annotationType(), annotation); } return annotationMap; } private boolean hasClassStartingWith(Collection<Class<?>> list, String value) { for (Class<?> aClass : list) { if (aClass.getName().startsWith(value)) { return true; } } return false; } private List<Parameter> extractParametersFromAnnotation(Type type, Map<Class<?>, Annotation> annotations) { List<Parameter> parameters = new ArrayList<>(); if (isRequestParamType(type, annotations)) { parameters.add(extractRequestParam(type, (RequestParam)annotations.get(RequestParam.class))); } if (annotations.containsKey(PathVariable.class)) { PathVariable pathVariable = (PathVariable) annotations.get(PathVariable.class); PathParameter pathParameter = extractPathVariable(type, pathVariable); parameters.add(pathParameter); } if (annotations.containsKey(RequestHeader.class)) { RequestHeader requestHeader = (RequestHeader) annotations.get(RequestHeader.class); HeaderParameter headerParameter = extractRequestHeader(type, requestHeader); parameters.add(headerParameter); } if (annotations.containsKey(CookieValue.class)) { CookieValue cookieValue = (CookieValue) annotations.get(CookieValue.class); CookieParameter cookieParameter = extractCookieValue(type, cookieValue); parameters.add(cookieParameter); } if (annotations.containsKey(RequestPart.class)) { RequestPart requestPart = (RequestPart) annotations.get(RequestPart.class); FormParameter formParameter = extractRequestPart(type, requestPart); parameters.add(formParameter); } return parameters; } private Parameter extractRequestParam(Type type, RequestParam requestParam) { if (requestParam == null) { requestParam = DEFAULT_REQUEST_PARAM; } String paramName = StringUtils.defaultIfEmpty(requestParam.value(), requestParam.name()); QueryParameter queryParameter = new QueryParameter().name(paramName) .required(requestParam.required()); if (!DEFAULT_VALUE.equals(requestParam.defaultValue())) { queryParameter.setDefaultValue(requestParam.defaultValue()); // Supplying a default value implicitly sets required() to false. queryParameter.setRequired(false); } Property schema = readAsPropertyIfPrimitive(type); if (schema != null) { queryParameter.setProperty(schema); } return queryParameter; } private FormParameter extractRequestPart(Type type, RequestPart requestPart) { String paramName = StringUtils.defaultIfEmpty(requestPart.value(), requestPart.name()); FormParameter formParameter = new FormParameter().name(paramName) .required(requestPart.required()); JavaType ct = constructType(type); Property schema; if (MultipartFile.class.isAssignableFrom(ct.getRawClass())) { schema = new FileProperty(); } else if (ct.isContainerType() && MultipartFile.class.isAssignableFrom(ct.getContentType().getRawClass())) { schema = new ArrayProperty().items(new FileProperty()); } else { schema = ModelConverters.getInstance().readAsProperty(type); } if (schema != null) { formParameter.setProperty(schema); } return formParameter; } private CookieParameter extractCookieValue(Type type, CookieValue cookieValue) { String paramName = StringUtils.defaultIfEmpty(cookieValue.value(), cookieValue.name()); CookieParameter cookieParameter = new CookieParameter().name(paramName) .required(cookieValue.required()); Property schema = readAsPropertyIfPrimitive(type); if (!DEFAULT_VALUE.equals(cookieValue.defaultValue())) { cookieParameter.setDefaultValue(cookieValue.defaultValue()); cookieParameter.setRequired(false); } if (schema != null) { cookieParameter.setProperty(schema); } return cookieParameter; } private HeaderParameter extractRequestHeader(Type type, RequestHeader requestHeader) { String paramName = StringUtils.defaultIfEmpty(requestHeader.value(), requestHeader.name()); HeaderParameter headerParameter = new HeaderParameter().name(paramName) .required(requestHeader.required()); Property schema = readAsPropertyIfPrimitive(type); if (!DEFAULT_VALUE.equals(requestHeader.defaultValue())) { headerParameter.setDefaultValue(requestHeader.defaultValue()); headerParameter.setRequired(false); } if (schema != null) { headerParameter.setProperty(schema); } return headerParameter; } private PathParameter extractPathVariable(Type type, PathVariable pathVariable) { String paramName = StringUtils.defaultIfEmpty(pathVariable.value(), pathVariable.name()); PathParameter pathParameter = new PathParameter().name(paramName); Property schema = readAsPropertyIfPrimitive(type); if (schema != null) { pathParameter.setProperty(schema); } return pathParameter; } private Property readAsPropertyIfPrimitive(Type type) { if (com.github.kongchen.swagger.docgen.util.TypeUtils.isPrimitive(type)) { return ModelConverters.getInstance().readAsProperty(type); } else { String msg = String.format("Non-primitive type: %s used as request/path/cookie parameter", type); log.warn(msg); // fallback to string if type is simple wrapper for String to support legacy code JavaType ct = constructType(type); if (isSimpleWrapperForString(ct.getRawClass())) { log.warn(String.format("Non-primitive type: %s used as string for request/path/cookie parameter", type)); return new StringProperty(); } } return null; } private boolean isSimpleWrapperForString(Class<?> clazz) { try { Constructor<?>[] constructors = clazz.getConstructors(); if (constructors.length <= 2) { if (constructors.length == 1) { return clazz.getConstructor(String.class) != null; } else if (constructors.length == 2) { return clazz.getConstructor(String.class) != null && clazz.getConstructor() != null; } } return false; } catch (NoSuchMethodException e) { // ignore return false; } } private List<Parameter> extractParametersFromModelAttributeAnnotation(Type type, Map<Class<?>, Annotation> annotations) { ModelAttribute modelAttribute = (ModelAttribute)annotations.get(ModelAttribute.class); if ((modelAttribute == null || !hasClassStartingWith(annotations.keySet(), "org.springframework.web.bind.annotation"))&& BeanUtils.isSimpleProperty(TypeUtils.getRawType(type, null))) { return Collections.emptyList(); } List<Parameter> parameters = new ArrayList<Parameter>(); Class<?> clazz = TypeUtils.getRawType(type, type); for (PropertyDescriptor propertyDescriptor : BeanUtils.getPropertyDescriptors(clazz)) { // Get all the valid setter methods inside the bean Method propertyDescriptorSetter = propertyDescriptor.getWriteMethod(); if (propertyDescriptorSetter != null) { ApiParam propertySetterApiParam = AnnotationUtils.findAnnotation(propertyDescriptorSetter, ApiParam.class); if (propertySetterApiParam == null) { // If we find a setter that doesn't have @ApiParam annotation, then skip it continue; } // Here we have a bean setter method that is annotted with @ApiParam, but we still // need to know what type of parameter to create. In order to do this, we look for // any annotation attached to the first method parameter of the setter fucntion. Annotation[][] parameterAnnotations = propertyDescriptorSetter.getParameterAnnotations(); if (parameterAnnotations == null || parameterAnnotations.length == 0) { continue; } Class parameterClass = propertyDescriptor.getPropertyType(); List<Parameter> propertySetterExtractedParameters = this.extractParametersFromAnnotation( parameterClass, toMap(Arrays.asList(parameterAnnotations[0]))); for (Parameter parameter : propertySetterExtractedParameters) { if (Strings.isNullOrEmpty(parameter.getName())) { parameter.setName(propertyDescriptor.getDisplayName()); } ParameterProcessor.applyAnnotations(new Swagger(), parameter, type, Lists.newArrayList(propertySetterApiParam)); } parameters.addAll(propertySetterExtractedParameters); } } return parameters; } private boolean isRequestParamType(Type type, Map<Class<?>, Annotation> annotations) { RequestParam requestParam = (RequestParam) annotations.get(RequestParam.class); return requestParam != null || (BeanUtils.isSimpleProperty(TypeUtils.getRawType(type, type)) && !hasClassStartingWith(annotations.keySet(), "org.springframework.web.bind.annotation")); } @Override public boolean shouldIgnoreType(Type type, Set<Type> typesToSkip) { Class<?> clazz = TypeUtils.getRawType(type, type); if (clazz == null) { return false; } String clazzName = clazz.getName(); switch (clazzName) { case "javax.servlet.ServletRequest": case "javax.servlet.ServletResponse": case "javax.servlet.http.HttpSession": case "javax.servlet.http.PushBuilder": case "java.security.Principal": case "java.io.OutputStream": case "java.io.Writer": return true; default: } return clazzName.startsWith("org.springframework") && !"org.springframework.web.multipart.MultipartFile".equals(clazzName); } }