package com.github.kongchen.swagger.docgen.reader;
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.Model;
import io.swagger.models.Operation;
import io.swagger.models.Response;
import io.swagger.models.SecurityRequirement;
import io.swagger.models.Swagger;
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.ReflectionUtils;
import org.apache.maven.plugin.logging.Log;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class JaxrsReader extends AbstractReader implements ClassSwaggerReader {
private static final Logger LOGGER = LoggerFactory.getLogger(JaxrsReader.class);
private static final ResponseContainerConverter RESPONSE_CONTAINER_CONVERTER = new ResponseContainerConverter();
public JaxrsReader(Swagger swagger, Log LOG) {
super(swagger, LOG);
}
@Override
public Swagger read(Set<Class<?>> classes) {
for (Class cls : classes) {
read(cls);
}
return swagger;
}
public Swagger getSwagger() {
return swagger;
}
public Swagger read(Class cls) {
return read(cls, "", null, false, new String[0], new String[0], new HashMap<String, Tag>(), new ArrayList<Parameter>());
}
protected Swagger read(Class<?> cls, String parentPath, String parentMethod, boolean readHidden, String[] parentConsumes, String[] parentProduces, Map<String, Tag> parentTags, List<Parameter> parentParameters) {
if (swagger == null) {
swagger = new Swagger();
}
Api api = AnnotationUtils.findAnnotation(cls, Api.class);
Path apiPath = AnnotationUtils.findAnnotation(cls, Path.class);
// only read if allowing hidden apis OR api is not marked as hidden
if (!canReadApi(readHidden, api)) {
return swagger;
}
Map<String, Tag> tags = updateTagsForApi(parentTags, api);
List<SecurityRequirement> securities = getSecurityRequirements(api);
// merge consumes, pro duces
// look for method-level annotated properties
// handle subresources by looking at return type
// parse the method
for (Method method : cls.getMethods()) {
ApiOperation apiOperation = AnnotationUtils.findAnnotation(method, ApiOperation.class);
if (apiOperation == null || apiOperation.hidden()) {
continue;
}
Path methodPath = AnnotationUtils.findAnnotation(method, Path.class);
String operationPath = getPath(apiPath, methodPath, parentPath);
if (operationPath != null) {
Map<String, String> regexMap = new HashMap<String, String>();
operationPath = parseOperationPath(operationPath, regexMap);
String httpMethod = extractOperationMethod(apiOperation, method, SwaggerExtensions.chain());
Operation operation = parseMethod(method);
updateOperationParameters(parentParameters, regexMap, operation);
updateOperationProtocols(apiOperation, operation);
String[] apiConsumes = new String[0];
String[] apiProduces = new String[0];
Consumes consumes = AnnotationUtils.findAnnotation(cls, Consumes.class);
if (consumes != null) {
apiConsumes = consumes.value();
}
Produces produces = AnnotationUtils.findAnnotation(cls, Produces.class);
if (produces != null) {
apiProduces = produces.value();
}
apiConsumes = updateOperationConsumes(parentConsumes, apiConsumes, operation);
apiProduces = updateOperationProduces(parentProduces, apiProduces, operation);
handleSubResource(apiConsumes, httpMethod, apiProduces, tags, method, operationPath, operation);
// can't continue without a valid http method
httpMethod = (httpMethod == null) ? parentMethod : httpMethod;
updateTagsForOperation(operation, apiOperation);
updateOperation(apiConsumes, apiProduces, tags, securities, operation);
updatePath(operationPath, httpMethod, operation);
}
updateTagDescriptions();
}
return swagger;
}
private void updateTagDescriptions() {
HashMap<String, Tag> tags = new HashMap<String, Tag>();
for (Class<?> aClass: new Reflections("").getTypesAnnotatedWith(SwaggerDefinition.class)) {
SwaggerDefinition swaggerDefinition = AnnotationUtils.findAnnotation(aClass, SwaggerDefinition.class);
for (io.swagger.annotations.Tag tag : swaggerDefinition.tags()) {
String tagName = tag.name();
if (!tagName.isEmpty()) {
tags.put(tag.name(), new Tag().name(tag.name()).description(tag.description()));
}
}
}
if (swagger.getTags() != null) {
for (Tag tag : swagger.getTags()) {
Tag rightTag = tags.get(tag.getName());
if (rightTag != null && rightTag.getDescription() != null) {
tag.setDescription(rightTag.getDescription());
}
}
}
}
private void handleSubResource(String[] apiConsumes, String httpMethod, String[] apiProduces, Map<String, Tag> tags, Method method, String operationPath, Operation operation) {
if (isSubResource(method)) {
Class<?> responseClass = method.getReturnType();
Swagger subSwagger = read(responseClass, operationPath, httpMethod, true, apiConsumes, apiProduces, tags, operation.getParameters());
}
}
protected boolean isSubResource(Method method) {
Class<?> responseClass = method.getReturnType();
return (responseClass != null) && (AnnotationUtils.findAnnotation(responseClass, Api.class) != null);
}
private String getPath(Path classLevelPath, Path methodLevelPath, String parentPath) {
if (classLevelPath == null && methodLevelPath == null) {
return null;
}
StringBuilder stringBuilder = new StringBuilder();
if (parentPath != null && !parentPath.isEmpty() && !parentPath.equals("/")) {
if (!parentPath.startsWith("/")) {
parentPath = "/" + parentPath;
}
if (parentPath.endsWith("/")) {
parentPath = parentPath.substring(0, parentPath.length() - 1);
}
stringBuilder.append(parentPath);
}
if (classLevelPath != null) {
stringBuilder.append(classLevelPath.value());
}
if (methodLevelPath != null && !methodLevelPath.value().equals("/")) {
String methodPath = methodLevelPath.value();
if (!methodPath.startsWith("/") && !stringBuilder.toString().endsWith("/")) {
stringBuilder.append("/");
}
if (methodPath.endsWith("/")) {
methodPath = methodPath.substring(0, methodPath.length() - 1);
}
stringBuilder.append(methodPath);
}
String output = stringBuilder.toString();
if (!output.startsWith("/")) {
output = "/" + output;
}
if (output.endsWith("/") && output.length() > 1) {
return output.substring(0, output.length() - 1);
} else {
return output;
}
}
public Operation parseMethod(Method method) {
Operation operation = new Operation();
ApiOperation apiOperation = AnnotationUtils.findAnnotation(method, ApiOperation.class);
String operationId = method.getName();
String responseContainer = null;
Class<?> responseClass = null;
Map<String, Property> defaultResponseHeaders = null;
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());
Set<Map<String, Object>> customExtensions = parseCustomExtensions(apiOperation.extensions());
if (customExtensions != null) {
for (Map<String, Object> extension : customExtensions) {
if (extension == null) {
continue;
}
for (Map.Entry<String, Object> map : extension.entrySet()) {
operation.setVendorExtension(map.getKey().startsWith("x-") ? map.getKey() : "x-" + map.getKey(), map.getValue());
}
}
}
if (!apiOperation.response().equals(Void.class)) {
responseClass = apiOperation.response();
}
if (!apiOperation.responseContainer().isEmpty()) {
responseContainer = apiOperation.responseContainer();
}
List<SecurityRequirement> securities = new ArrayList<SecurityRequirement>();
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);
}
}
operation.operationId(operationId);
if (responseClass == null) {
// pick out response from method declaration
LOGGER.debug("picking up response class from method " + method);
Type t = method.getGenericReturnType();
responseClass = method.getReturnType();
if (!responseClass.equals(Void.class) && !responseClass.equals(void.class)
&& (AnnotationUtils.findAnnotation(responseClass, Api.class) == null)) {
LOGGER.debug("reading model " + responseClass);
Map<String, Model> models = ModelConverters.getInstance().readAll(t);
}
}
if ((responseClass != null)
&& !responseClass.equals(Void.class)
&& !responseClass.equals(javax.ws.rs.core.Response.class)
&& (AnnotationUtils.findAnnotation(responseClass, Api.class) == null)) {
if (isPrimitive(responseClass)) {
Property property = ModelConverters.getInstance().readAsProperty(responseClass);
if (property != null) {
Property responseProperty = RESPONSE_CONTAINER_CONVERTER.withResponseContainer(responseContainer, property);
operation.response(apiOperation.code(), new Response()
.description("successful operation")
.schema(responseProperty)
.headers(defaultResponseHeaders));
}
} else if (!responseClass.equals(Void.class) && !responseClass.equals(void.class)) {
Map<String, Model> models = ModelConverters.getInstance().read(responseClass);
if (models.isEmpty()) {
Property p = ModelConverters.getInstance().readAsProperty(responseClass);
operation.response(apiOperation.code(), new Response()
.description("successful operation")
.schema(p)
.headers(defaultResponseHeaders));
}
for (String key : models.keySet()) {
Property responseProperty = RESPONSE_CONTAINER_CONVERTER.withResponseContainer(responseContainer, new RefProperty().asDefault(key));
operation.response(apiOperation.code(), new Response()
.description("successful operation")
.schema(responseProperty)
.headers(defaultResponseHeaders));
swagger.model(key, models.get(key));
}
models = ModelConverters.getInstance().readAll(responseClass);
for (Map.Entry<String, Model> entry : models.entrySet()) {
swagger.model(entry.getKey(), entry.getValue());
}
}
}
Consumes consumes = AnnotationUtils.findAnnotation(method, Consumes.class);
if (consumes != null) {
for (String mediaType : consumes.value()) {
operation.consumes(mediaType);
}
}
Produces produces = AnnotationUtils.findAnnotation(method, Produces.class);
if (produces != null) {
for (String mediaType : produces.value()) {
operation.produces(mediaType);
}
}
ApiResponses responseAnnotation = AnnotationUtils.findAnnotation(method, ApiResponses.class);
if (responseAnnotation != null) {
updateApiResponse(operation, responseAnnotation);
}
if (AnnotationUtils.findAnnotation(method, Deprecated.class) != null) {
operation.deprecated(true);
}
// FIXME `hidden` is never used
boolean hidden = false;
if (apiOperation != null) {
hidden = apiOperation.hidden();
}
// process parameters
Class[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
Annotation[][] paramAnnotations = findParamAnnotations(method);
// paramTypes = method.getParameterTypes
// genericParamTypes = method.getGenericParameterTypes
for (int i = 0; i < parameterTypes.length; i++) {
Type type = genericParameterTypes[i];
List<Annotation> annotations = Arrays.asList(paramAnnotations[i]);
List<Parameter> parameters = getParameters(type, annotations);
for (Parameter parameter : parameters) {
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 Annotation[][] findParamAnnotations(Method method) {
Annotation[][] paramAnnotation = method.getParameterAnnotations();
method = ReflectionUtils.getOverriddenMethod(method);
while(method != null) {
paramAnnotation = merge(paramAnnotation, method.getParameterAnnotations());
method = ReflectionUtils.getOverriddenMethod(method);
}
return paramAnnotation;
}
private Annotation[][] merge(Annotation[][] paramAnnotation,
Annotation[][] superMethodParamAnnotations) {
Annotation[][] mergedAnnotations = new Annotation[paramAnnotation.length][];
for(int i=0; i<paramAnnotation.length; i++) {
mergedAnnotations[i] = merge(paramAnnotation[i], superMethodParamAnnotations[i]);
}
return mergedAnnotations;
}
private Annotation[] merge(Annotation[] annotations,
Annotation[] annotations2) {
Set<Annotation> mergedAnnotations = new HashSet<Annotation>();
mergedAnnotations.addAll(Arrays.asList(annotations));
mergedAnnotations.addAll(Arrays.asList(annotations2));
return mergedAnnotations.toArray(new Annotation[0]);
}
public String extractOperationMethod(ApiOperation apiOperation, Method method, Iterator<SwaggerExtension> chain) {
if (!apiOperation.httpMethod().isEmpty()) {
return apiOperation.httpMethod().toLowerCase();
} else if (AnnotationUtils.findAnnotation(method, GET.class) != null) {
return "get";
} else if (AnnotationUtils.findAnnotation(method, PUT.class) != null) {
return "put";
} else if (AnnotationUtils.findAnnotation(method, POST.class) != null) {
return "post";
} else if (AnnotationUtils.findAnnotation(method, DELETE.class) != null) {
return "delete";
} else if (AnnotationUtils.findAnnotation(method, OPTIONS.class) != null) {
return "options";
} else if (AnnotationUtils.findAnnotation(method, HEAD.class) != null) {
return "head";
} else if (AnnotationUtils.findAnnotation(method, io.swagger.jaxrs.PATCH.class) != null) {
return "patch";
} else {
// check for custom HTTP Method annotations
for (Annotation declaredAnnotation : method.getDeclaredAnnotations()) {
Annotation[] innerAnnotations = declaredAnnotation.annotationType().getAnnotations();
for (Annotation innerAnnotation : innerAnnotations) {
if (innerAnnotation instanceof HttpMethod) {
HttpMethod httpMethod = (HttpMethod) innerAnnotation;
return httpMethod.value().toLowerCase();
}
}
}
if (chain.hasNext()) {
return chain.next().extractOperationMethod(apiOperation, method, chain);
}
}
return null;
}
}