com.github.kongchen.swagger.docgen.reader.SpringMvcApiReader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of swagger-maven-plugin Show documentation
Show all versions of swagger-maven-plugin Show documentation
A maven build plugin which helps you generate API document during build phase
The newest version!
package com.github.kongchen.swagger.docgen.reader;
import com.github.kongchen.swagger.docgen.GenerateException;
import com.github.kongchen.swagger.docgen.spring.SpringResource;
import com.github.kongchen.swagger.docgen.spring.SpringSwaggerExtension;
import com.github.kongchen.swagger.docgen.util.SpringUtils;
import io.swagger.annotations.*;
import io.swagger.converter.ModelConverters;
import io.swagger.jaxrs.ext.SwaggerExtension;
import io.swagger.jaxrs.ext.SwaggerExtensions;
import io.swagger.models.*;
import io.swagger.models.Tag;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.RefProperty;
import io.swagger.util.BaseReaderUtils;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.util.StringUtils;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
public class SpringMvcApiReader extends AbstractReader implements ClassSwaggerReader {
private static final ResponseContainerConverter RESPONSE_CONTAINER_CONVERTER = new ResponseContainerConverter();
private final SpringExceptionHandlerReader exceptionHandlerReader;
private String resourcePath;
public SpringMvcApiReader(Swagger swagger, Log log) {
super(swagger, log);
exceptionHandlerReader = new SpringExceptionHandlerReader(log);
}
@Override
protected void updateExtensionChain() {
List extensions = new ArrayList();
extensions.add(new SpringSwaggerExtension(LOG));
SwaggerExtensions.setExtensions(extensions);
}
@Override
public Swagger read(Set> classes) throws GenerateException {
//relate all methods to one base request mapping if multiple controllers exist for that mapping
//get all methods from each controller & find their request mapping
//create map - resource string (after first slash) as key, new SpringResource as value
Map resourceMap = generateResourceMap(classes);
exceptionHandlerReader.processExceptionHandlers(classes);
for (SpringResource resource: resourceMap.values()) {
read(resource);
}
return swagger;
}
public Swagger read(SpringResource resource) {
if (swagger == null) {
swagger = new Swagger();
}
List methods = resource.getMethods();
Map tags = new HashMap();
List resourceSecurities = new ArrayList();
// Add the description from the controller api
Class> controller = resource.getControllerClass();
RequestMapping controllerRM = findMergedAnnotation(controller, RequestMapping.class);
String[] controllerProduces = new String[0];
String[] controllerConsumes = new String[0];
if (controllerRM != null) {
controllerConsumes = controllerRM.consumes();
controllerProduces = controllerRM.produces();
}
if (controller.isAnnotationPresent(Api.class)) {
Api api = findMergedAnnotation(controller, Api.class);
if (!canReadApi(false, api)) {
return swagger;
}
tags = updateTagsForApi(null, api);
resourceSecurities = getSecurityRequirements(api);
}
resourcePath = resource.getControllerMapping();
//collect api from method with @RequestMapping
Map> apiMethodMap = collectApisByRequestMapping(methods);
for (String path : apiMethodMap.keySet()) {
for (Method method : apiMethodMap.get(path)) {
RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class);
if (requestMapping == null) {
continue;
}
ApiOperation apiOperation = findMergedAnnotation(method, ApiOperation.class);
if (apiOperation != null && apiOperation.hidden()) {
continue;
}
Map regexMap = new HashMap();
String operationPath = parseOperationPath(path, regexMap);
//http method
for (RequestMethod requestMethod : requestMapping.method()) {
String httpMethod = requestMethod.toString().toLowerCase();
Operation operation = parseMethod(method, requestMethod);
updateOperationParameters(new ArrayList(), regexMap, operation);
updateOperationProtocols(apiOperation, operation);
String[] apiProduces = requestMapping.produces();
String[] apiConsumes = requestMapping.consumes();
apiProduces = (apiProduces.length == 0) ? controllerProduces : apiProduces;
apiConsumes = (apiConsumes.length == 0) ? controllerConsumes : apiConsumes;
apiConsumes = updateOperationConsumes(new String[0], apiConsumes, operation);
apiProduces = updateOperationProduces(new String[0], apiProduces, operation);
updateTagsForOperation(operation, apiOperation);
updateOperation(apiConsumes, apiProduces, tags, resourceSecurities, operation);
updatePath(operationPath, httpMethod, operation);
}
}
}
return swagger;
}
private Operation parseMethod(Method method, RequestMethod requestMethod) {
int responseCode = 200;
Operation operation = new Operation();
RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class);
Type responseClass = null;
List produces = new ArrayList();
List consumes = new ArrayList();
String responseContainer = null;
String operationId = getOperationId(method, requestMethod.name());
Map defaultResponseHeaders = null;
ApiOperation apiOperation = findMergedAnnotation(method, ApiOperation.class);
if(apiOperation != null) {
if (apiOperation.hidden()) {
return null;
}
if (!apiOperation.nickname().isEmpty()) {
operationId = apiOperation.nickname();
}
defaultResponseHeaders = parseResponseHeaders(apiOperation.responseHeaders());
operation.summary(apiOperation.value()).description(apiOperation.notes());
Map customExtensions = BaseReaderUtils.parseExtensions(apiOperation.extensions());
operation.setVendorExtensions(customExtensions);
if (!apiOperation.response().equals(Void.class)) {
responseClass = apiOperation.response();
}
if (!apiOperation.responseContainer().isEmpty()) {
responseContainer = apiOperation.responseContainer();
}
///security
List securities = new ArrayList();
for (Authorization auth : apiOperation.authorizations()) {
if (!auth.value().isEmpty()) {
SecurityRequirement security = new SecurityRequirement();
security.setName(auth.value());
for (AuthorizationScope scope : auth.scopes()) {
if (!scope.scope().isEmpty()) {
security.addScope(scope.scope());
}
}
securities.add(security);
}
}
for (SecurityRequirement sec : securities) {
operation.security(sec);
}
responseCode = apiOperation.code();
}
if (responseClass == null) {
// pick out response from method declaration
LOG.info("picking up response class from method " + method);
responseClass = method.getGenericReturnType();
}
if (responseClass instanceof ParameterizedType && ResponseEntity.class.equals(((ParameterizedType) responseClass).getRawType())) {
responseClass = ((ParameterizedType) responseClass).getActualTypeArguments()[0];
}
boolean hasApiAnnotation = false;
if (responseClass instanceof Class) {
hasApiAnnotation = findAnnotation((Class) responseClass, Api.class) != null;
}
if (responseClass != null
&& !responseClass.equals(Void.class)
&& !responseClass.equals(ResponseEntity.class)
&& !hasApiAnnotation) {
if (isPrimitive(responseClass)) {
Property property = ModelConverters.getInstance().readAsProperty(responseClass);
if (property != null) {
Property responseProperty = RESPONSE_CONTAINER_CONVERTER.withResponseContainer(responseContainer, property);
operation.response(responseCode, new Response()
.description("successful operation")
.schema(responseProperty)
.headers(defaultResponseHeaders));
}
} else if (!responseClass.equals(Void.class) && !responseClass.equals(void.class)) {
Map models = ModelConverters.getInstance().read(responseClass);
if (models.isEmpty()) {
Property pp = ModelConverters.getInstance().readAsProperty(responseClass);
operation.response(responseCode, new Response()
.description("successful operation")
.schema(pp)
.headers(defaultResponseHeaders));
}
for (String key : models.keySet()) {
Property responseProperty = RESPONSE_CONTAINER_CONVERTER.withResponseContainer(responseContainer, new RefProperty().asDefault(key));
operation.response(responseCode, new Response()
.description("successful operation")
.schema(responseProperty)
.headers(defaultResponseHeaders));
swagger.model(key, models.get(key));
}
}
Map models = ModelConverters.getInstance().readAll(responseClass);
for (Map.Entry entry : models.entrySet()) {
swagger.model(entry.getKey(), entry.getValue());
}
}
operation.operationId(operationId);
for (String str : requestMapping.produces()) {
if (!produces.contains(str)) {
produces.add(str);
}
}
for (String str : requestMapping.consumes()) {
if (!consumes.contains(str)) {
consumes.add(str);
}
}
ApiResponses responseAnnotation = findMergedAnnotation(method, ApiResponses.class);
if (responseAnnotation != null) {
updateApiResponse(operation, responseAnnotation);
} else {
ResponseStatus responseStatus = findMergedAnnotation(method, ResponseStatus.class);
if (responseStatus != null) {
operation.response(responseStatus.value().value(), new Response().description(responseStatus.reason()));
}
}
List errorResponses = exceptionHandlerReader.getResponseStatusesFromExceptions(method);
for (ResponseStatus responseStatus: errorResponses) {
int code = responseStatus.code().value();
String description = defaultIfEmpty(responseStatus.reason(), responseStatus.code().getReasonPhrase());
operation.response(code, new Response().description(description));
}
Deprecated annotation = findAnnotation(method, Deprecated.class);
if (annotation != null) {
operation.deprecated(true);
}
// process parameters
Class[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
Annotation[][] paramAnnotations = method.getParameterAnnotations();
DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
// paramTypes = method.getParameterTypes
// genericParamTypes = method.getGenericParameterTypes
for (int i = 0; i < parameterTypes.length; i++) {
Type type = genericParameterTypes[i];
List annotations = Arrays.asList(paramAnnotations[i]);
List parameters = getParameters(type, annotations);
for (Parameter parameter : parameters) {
if(parameter.getName().isEmpty()) {
parameter.setName(parameterNames[i]);
}
operation.parameter(parameter);
}
}
if (operation.getResponses() == null) {
operation.defaultResponse(new Response().description("successful operation"));
}
// Process @ApiImplicitParams
this.readImplicitParameters(method, operation);
processOperationDecorator(operation, method);
return operation;
}
private Map> collectApisByRequestMapping(List methods) {
Map> apiMethodMap = new HashMap>();
for (Method method : methods) {
RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class);
if (requestMapping != null) {
String path;
if (requestMapping.value().length != 0) {
path = generateFullPath(requestMapping.value()[0]);
} else {
path = resourcePath;
}
if (apiMethodMap.containsKey(path)) {
apiMethodMap.get(path).add(method);
} else {
List ms = new ArrayList();
ms.add(method);
apiMethodMap.put(path, ms);
}
}
}
return apiMethodMap;
}
private String generateFullPath(String path) {
if (StringUtils.isNotEmpty(path)) {
return this.resourcePath + (path.startsWith("/") ? path : '/' + path);
} else {
return this.resourcePath;
}
}
//Helper method for loadDocuments()
private Map analyzeController(Class> controllerClazz, Map resourceMap, String description) {
String[] controllerRequestMappingValues = SpringUtils.getControllerResquestMapping(controllerClazz);
// Iterate over all value attributes of the class-level RequestMapping annotation
for (String controllerRequestMappingValue : controllerRequestMappingValues) {
for (Method method : controllerClazz.getMethods()) {
// Skip methods introduced by compiler
if (method.isSynthetic()) {
continue;
}
RequestMapping methodRequestMapping = findMergedAnnotation(method, RequestMapping.class);
// Look for method-level @RequestMapping annotation
if (methodRequestMapping != null) {
RequestMethod[] requestMappingRequestMethods = methodRequestMapping.method();
// For each method-level @RequestMapping annotation, iterate over HTTP Verb
for (RequestMethod requestMappingRequestMethod : requestMappingRequestMethods) {
String[] methodRequestMappingValues = methodRequestMapping.value();
// Check for cases where method-level @RequestMapping#value is not set, and use the controllers @RequestMapping
if (methodRequestMappingValues.length == 0) {
// The map key is a concat of the following:
// 1. The controller package
// 2. The controller class name
// 3. The controller-level @RequestMapping#value
String resourceKey = controllerClazz.getCanonicalName() + controllerRequestMappingValue + requestMappingRequestMethod;
if (!resourceMap.containsKey(resourceKey)) {
resourceMap.put(
resourceKey,
new SpringResource(controllerClazz, controllerRequestMappingValue, resourceKey, description));
}
resourceMap.get(resourceKey).addMethod(method);
} else {
// Here we know that method-level @RequestMapping#value is populated, so
// iterate over all the @RequestMapping#value attributes, and add them to the resource map.
for (String methodRequestMappingValue : methodRequestMappingValues) {
String resourceKey = controllerClazz.getCanonicalName() + controllerRequestMappingValue
+ methodRequestMappingValue + requestMappingRequestMethod;
if (!methodRequestMappingValue.isEmpty()) {
if (!resourceMap.containsKey(resourceKey)) {
resourceMap.put(resourceKey, new SpringResource(controllerClazz, methodRequestMappingValue, resourceKey, description));
}
resourceMap.get(resourceKey).addMethod(method);
}
}
}
}
}
}
}
controllerClazz.getFields();
controllerClazz.getDeclaredFields(); //<--In case developer declares a field without an associated getter/setter.
//this will allow NoClassDefFoundError to be caught before it triggers bamboo failure.
return resourceMap;
}
protected Map generateResourceMap(Set> validClasses) throws GenerateException {
Map resourceMap = new HashMap();
for (Class> aClass : validClasses) {
RequestMapping requestMapping = findAnnotation(aClass, RequestMapping.class);
//This try/catch block is to stop a bamboo build from failing due to NoClassDefFoundError
//This occurs when a class or method loaded by reflections contains a type that has no dependency
try {
resourceMap = analyzeController(aClass, resourceMap, "");
List mList = new ArrayList(Arrays.asList(aClass.getMethods()));
if (aClass.getSuperclass() != null) {
mList.addAll(Arrays.asList(aClass.getSuperclass().getMethods()));
}
} catch (NoClassDefFoundError e) {
LOG.error(e.getMessage());
LOG.info(aClass.getName());
//exception occurs when a method type or annotation is not recognized by the plugin
}
}
return resourceMap;
}
}