com.github.kongchen.swagger.docgen.reader.AbstractReader 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.util.TypeExtracter;
import com.github.kongchen.swagger.docgen.util.TypeWithAnnotations;
import com.google.common.collect.Lists;
import com.sun.jersey.api.core.InjectParam;
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.Path;
import io.swagger.models.Tag;
import io.swagger.models.parameters.*;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.MapProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.RefProperty;
import io.swagger.util.ParameterProcessor;
import io.swagger.util.PathUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.apache.commons.lang3.text.StrBuilder;
import org.apache.maven.plugin.logging.Log;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.*;
import javax.ws.rs.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;
/**
* @author chekong on 15/4/28.
*/
public abstract class AbstractReader {
protected final Log LOG;
protected Swagger swagger;
private Set typesToSkip = new HashSet();
protected String operationIdFormat;
/**
* Supported parameters: {{packageName}}, {{className}}, {{methodName}}, {{httpMethod}}
* Suggested default value is: "{{className}}_{{methodName}}_{{httpMethod}}"
*/
public static final String OPERATION_ID_FORMAT_DEFAULT = "{{methodName}}";
public Set getTypesToSkip() {
return typesToSkip;
}
public void setTypesToSkip(List typesToSkip) {
this.typesToSkip = new HashSet(typesToSkip);
}
public void setTypesToSkip(Set typesToSkip) {
this.typesToSkip = typesToSkip;
}
public void addTypeToSkippedTypes(Type type) {
this.typesToSkip.add(type);
}
public AbstractReader(Swagger swagger, Log LOG) {
this.swagger = swagger;
this.LOG = LOG;
updateExtensionChain();
}
/**
* Method which allows sub-classes to modify the Swagger extension chain.
*/
protected void updateExtensionChain() {
// default implementation does nothing
}
protected List getSecurityRequirements(Api api) {
List securities = new ArrayList<>();
if(api == null) {
return securities;
}
for (Authorization auth : api.authorizations()) {
if (auth.value().isEmpty()) {
continue;
}
SecurityRequirement security = new SecurityRequirement();
security.setName(auth.value());
for (AuthorizationScope scope : auth.scopes()) {
if (!scope.scope().isEmpty()) {
security.addScope(scope.scope());
}
}
securities.add(security);
}
return securities;
}
protected String parseOperationPath(String operationPath, Map regexMap) {
return PathUtils.parsePath(operationPath, regexMap);
}
protected void updateOperationParameters(List parentParameters, Map regexMap, Operation operation) {
if (parentParameters != null) {
for (Parameter param : parentParameters) {
operation.parameter(param);
}
}
for (Parameter param : operation.getParameters()) {
String pattern = regexMap.get(param.getName());
if (pattern != null) {
param.setPattern(pattern);
}
}
}
protected Map parseResponseHeaders(ResponseHeader[] headers) {
if (headers == null) {
return null;
}
Map responseHeaders = null;
for (ResponseHeader header : headers) {
if (header.name().isEmpty()) {
continue;
}
if (responseHeaders == null) {
responseHeaders = new HashMap<>();
}
Class> cls = header.response();
if (!cls.equals(Void.class) && !cls.equals(void.class)) {
Property property = ModelConverters.getInstance().readAsProperty(cls);
if (property != null) {
Property responseProperty;
if (header.responseContainer().equalsIgnoreCase("list")) {
responseProperty = new ArrayProperty(property);
} else if (header.responseContainer().equalsIgnoreCase("map")) {
responseProperty = new MapProperty(property);
} else {
responseProperty = property;
}
responseProperty.setDescription(header.description());
responseHeaders.put(header.name(), responseProperty);
}
}
}
return responseHeaders;
}
protected void updatePath(String operationPath, String httpMethod, Operation operation) {
if (httpMethod == null) {
return;
}
Path path = swagger.getPath(operationPath);
if (path == null) {
path = new Path();
swagger.path(operationPath, path);
}
path.set(httpMethod, operation);
}
protected void updateTagsForOperation(Operation operation, ApiOperation apiOperation) {
if (apiOperation == null) {
return;
}
for (String tag : apiOperation.tags()) {
if (!tag.isEmpty()) {
operation.tag(tag);
swagger.tag(new Tag().name(tag));
}
}
}
protected boolean canReadApi(boolean readHidden, Api api) {
return (api == null) || (readHidden) || (!api.hidden());
}
protected Set extractTags(Api api) {
Set output = new LinkedHashSet<>();
if(api == null) {
return output;
}
boolean hasExplicitTags = false;
for (String tag : api.tags()) {
if (!tag.isEmpty()) {
hasExplicitTags = true;
output.add(new Tag().name(tag));
}
}
if (!hasExplicitTags) {
// derive tag from api path + description
String tagString = api.value().replace("/", "");
if (!tagString.isEmpty()) {
Tag tag = new Tag().name(tagString);
if (!api.description().isEmpty()) {
tag.description(api.description());
}
output.add(tag);
}
}
return output;
}
protected void updateOperationProtocols(ApiOperation apiOperation, Operation operation) {
if(apiOperation == null) {
return;
}
String[] protocols = apiOperation.protocols().split(",");
for (String protocol : protocols) {
String trimmed = protocol.trim();
if (!trimmed.isEmpty()) {
operation.scheme(Scheme.forValue(trimmed));
}
}
}
protected Map updateTagsForApi(Map parentTags, Api api) {
// the value will be used as a tag for 2.0 UNLESS a Tags annotation is present
Map tagsMap = new HashMap<>();
for (Tag tag : extractTags(api)) {
tagsMap.put(tag.getName(), tag);
}
if (parentTags != null) {
tagsMap.putAll(parentTags);
}
for (Tag tag : tagsMap.values()) {
swagger.tag(tag);
}
return tagsMap;
}
protected boolean isPrimitive(Type cls) {
return com.github.kongchen.swagger.docgen.util.TypeUtils.isPrimitive(cls);
}
protected void updateOperation(String[] apiConsumes, String[] apiProduces, Map tags, List securities, Operation operation) {
if (operation == null) {
return;
}
if (operation.getConsumes() == null) {
for (String mediaType : apiConsumes) {
operation.consumes(mediaType);
}
}
if (operation.getProduces() == null) {
for (String mediaType : apiProduces) {
operation.produces(mediaType);
}
}
if (operation.getTags() == null) {
for (String tagString : tags.keySet()) {
operation.tag(tagString);
}
}
for (SecurityRequirement security : securities) {
operation.security(security);
}
}
private boolean isApiParamHidden(List parameterAnnotations) {
for (Annotation parameterAnnotation : parameterAnnotations) {
if (parameterAnnotation instanceof ApiParam) {
return ((ApiParam) parameterAnnotation).hidden();
}
}
return false;
}
private boolean hasValidAnnotations(List parameterAnnotations) {
// Because method parameters can contain parameters that are valid, but
// not part of the API contract, first check to make sure the parameter
// has at lease one annotation before processing it. Also, check a
// whitelist to make sure that the annotation of the parameter is
// compatible with spring-maven-plugin
List validParameterAnnotations = new ArrayList<>();
validParameterAnnotations.add(ModelAttribute.class);
validParameterAnnotations.add(BeanParam.class);
validParameterAnnotations.add(InjectParam.class);
validParameterAnnotations.add(ApiParam.class);
validParameterAnnotations.add(PathParam.class);
validParameterAnnotations.add(QueryParam.class);
validParameterAnnotations.add(HeaderParam.class);
validParameterAnnotations.add(FormParam.class);
validParameterAnnotations.add(RequestParam.class);
validParameterAnnotations.add(RequestBody.class);
validParameterAnnotations.add(PathVariable.class);
validParameterAnnotations.add(RequestHeader.class);
validParameterAnnotations.add(RequestPart.class);
validParameterAnnotations.add(CookieValue.class);
boolean hasValidAnnotation = false;
for (Annotation potentialAnnotation : parameterAnnotations) {
if (validParameterAnnotations.contains(potentialAnnotation.annotationType())) {
hasValidAnnotation = true;
break;
}
}
return hasValidAnnotation;
}
// this is final to enforce that only the implementation method below can be overridden, to avoid confusion
protected final List getParameters(Type type, List annotations) {
return getParameters(type, annotations, typesToSkip);
}
// this method exists so that outside callers can choose their own custom types to skip
protected List getParameters(Type type, List annotations, Set typesToSkip) {
if (!hasValidAnnotations(annotations) || isApiParamHidden(annotations)) {
return Collections.emptyList();
}
Iterator chain = SwaggerExtensions.chain();
List parameters = new ArrayList<>();
Class> cls = TypeUtils.getRawType(type, type);
LOG.debug("Looking for path/query/header/form/cookie params in " + cls);
if (chain.hasNext()) {
SwaggerExtension extension = chain.next();
LOG.debug("trying extension " + extension);
parameters = extension.extractParameters(annotations, type, typesToSkip, chain);
}
if (!parameters.isEmpty()) {
for (Parameter parameter : parameters) {
ParameterProcessor.applyAnnotations(swagger, parameter, type, annotations);
}
} else {
LOG.debug("Looking for body params in " + cls);
// parameters is guaranteed to be empty at this point, replace it with a mutable collection
parameters = Lists.newArrayList();
if (!typesToSkip.contains(type)) {
Parameter param = ParameterProcessor.applyAnnotations(swagger, null, type, annotations);
if (param != null) {
parameters.add(param);
}
}
}
return parameters;
}
protected void updateApiResponse(Operation operation, ApiResponses responseAnnotation) {
for (ApiResponse apiResponse : responseAnnotation.value()) {
Map responseHeaders = parseResponseHeaders(apiResponse.responseHeaders());
Class> responseClass = apiResponse.response();
Response response = new Response()
.description(apiResponse.message())
.headers(responseHeaders);
if (responseClass.equals(Void.class)) {
if (operation.getResponses() != null) {
Response apiOperationResponse = operation.getResponses().get(String.valueOf(apiResponse.code()));
if (apiOperationResponse != null) {
response.setSchema(apiOperationResponse.getSchema());
}
}
} else {
Map models = ModelConverters.getInstance().read(responseClass);
for (String key : models.keySet()) {
final Property schema = new RefProperty().asDefault(key);
if (apiResponse.responseContainer().equals("List")) {
response.schema(new ArrayProperty(schema));
} else {
response.schema(schema);
}
swagger.model(key, models.get(key));
}
models = ModelConverters.getInstance().readAll(responseClass);
for (Map.Entry entry : models.entrySet()) {
swagger.model(entry.getKey(), entry.getValue());
}
if (response.getSchema() == null) {
Map responses = operation.getResponses();
if (responses != null) {
Response apiOperationResponse = responses.get(String.valueOf(apiResponse.code()));
if (apiOperationResponse != null) {
response.setSchema(apiOperationResponse.getSchema());
}
}
}
}
if (apiResponse.code() == 0) {
operation.defaultResponse(response);
} else {
operation.response(apiResponse.code(), response);
}
}
}
protected String[] updateOperationProduces(String[] parentProduces, String[] apiProduces, Operation operation) {
if (parentProduces != null) {
Set both = new LinkedHashSet<>(Arrays.asList(apiProduces));
both.addAll(Arrays.asList(parentProduces));
if (operation.getProduces() != null) {
both.addAll(operation.getProduces());
}
apiProduces = both.toArray(new String[both.size()]);
}
return apiProduces;
}
protected String[] updateOperationConsumes(String[] parentConsumes, String[] apiConsumes, Operation operation) {
if (parentConsumes != null) {
Set both = new LinkedHashSet<>(Arrays.asList(apiConsumes));
both.addAll(Arrays.asList(parentConsumes));
if (operation.getConsumes() != null) {
both.addAll(operation.getConsumes());
}
apiConsumes = both.toArray(new String[both.size()]);
}
return apiConsumes;
}
protected void readImplicitParameters(Method method, Operation operation) {
ApiImplicitParams implicitParams = AnnotationUtils.findAnnotation(method, ApiImplicitParams.class);
if (implicitParams == null) {
return;
}
for (ApiImplicitParam param : implicitParams.value()) {
Class> cls;
try {
cls = Class.forName(param.dataType());
} catch (ClassNotFoundException e) {
cls = method.getDeclaringClass();
}
Parameter p = readImplicitParam(param, cls);
if (p != null) {
operation.addParameter(p);
}
}
}
protected Parameter readImplicitParam(ApiImplicitParam param, Class> apiClass) {
Parameter parameter;
if (param.paramType().equalsIgnoreCase("path")) {
parameter = new PathParameter();
} else if (param.paramType().equalsIgnoreCase("query")) {
parameter = new QueryParameter();
} else if (param.paramType().equalsIgnoreCase("form") || param.paramType().equalsIgnoreCase("formData")) {
parameter = new FormParameter();
} else if (param.paramType().equalsIgnoreCase("body")) {
parameter = new BodyParameter();
} else if (param.paramType().equalsIgnoreCase("header")) {
parameter = new HeaderParameter();
} else {
return null;
}
return ParameterProcessor.applyAnnotations(swagger, parameter, apiClass, Arrays.asList(new Annotation[]{param}));
}
void processOperationDecorator(Operation operation, Method method) {
final Iterator chain = SwaggerExtensions.chain();
if (chain.hasNext()) {
SwaggerExtension extension = chain.next();
extension.decorateOperation(operation, method, chain);
}
}
protected String getOperationId(Method method, String httpMethod) {
if (this.operationIdFormat == null) {
this.operationIdFormat = OPERATION_ID_FORMAT_DEFAULT;
}
String packageName = method.getDeclaringClass().getPackage().getName();
String className = method.getDeclaringClass().getSimpleName();
String methodName = method.getName();
StrBuilder sb = new StrBuilder(this.operationIdFormat);
sb.replaceAll("{{packageName}}", packageName);
sb.replaceAll("{{className}}", className);
sb.replaceAll("{{methodName}}", methodName);
sb.replaceAll("{{httpMethod}}", httpMethod);
return sb.toString();
}
public List extractTypes(Class> cls, Set typesToSkip, List additionalAnnotations) {
TypeExtracter extractor = new TypeExtracter();
Collection typesWithAnnotations = extractor.extractTypes(cls);
List output = new ArrayList();
for (TypeWithAnnotations typeWithAnnotations : typesWithAnnotations) {
Type type = typeWithAnnotations.getType();
List annotations = new ArrayList(additionalAnnotations);
annotations.addAll(typeWithAnnotations.getAnnotations());
/*
* Skip the type of the bean itself when recursing into its members
* in order to avoid a cycle (stack overflow), as crazy as that user
* code would have to be.
*
* There are no tests to prove this works because the test bean
* classes are shared with SwaggerReaderTest and Swagger's own logic
* doesn't prevent this problem.
*/
Set recurseTypesToSkip = new HashSet(typesToSkip);
recurseTypesToSkip.add(cls);
output.addAll(this.getParameters(type, annotations, recurseTypesToSkip));
}
return output;
}
public String getOperationIdFormat() {
return operationIdFormat;
}
public void setOperationIdFormat(String operationIdFormat) {
this.operationIdFormat = operationIdFormat;
}
}