com.tenxerconsulting.swagger.doclet.parser.CrossClassApiParser Maven / Gradle / Ivy
The newest version!
package com.tenxerconsulting.swagger.doclet.parser;
import static com.google.common.base.Objects.equal;
import static com.google.common.collect.Maps.uniqueIndex;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Function;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.Tag;
import com.sun.javadoc.Type;
import com.tenxerconsulting.swagger.doclet.DocletOptions;
import com.tenxerconsulting.swagger.doclet.model.Api;
import com.tenxerconsulting.swagger.doclet.model.ApiDeclaration;
import com.tenxerconsulting.swagger.doclet.model.Method;
import com.tenxerconsulting.swagger.doclet.model.Model;
import com.tenxerconsulting.swagger.doclet.model.Operation;
/**
* The CrossClassApiParser represents an api class parser that supports ApiDeclaration being
* spread across multiple resource classes.
* @version $Id$
* @author conor.roche
*/
public class CrossClassApiParser {
private final DocletOptions options;
private final ClassDoc classDoc;
private final Collection classes;
private final String rootPath;
private final String swaggerVersion;
private final String apiVersion;
private final String basePath;
private final Method parentMethod;
private final Map subResourceClasses;
private final Collection typeClasses;
/**
* This creates a CrossClassApiParser for top level parsing
* @param options The options for parsing
* @param classDoc The class doc
* @param classes The doclet classes to document
* @param typeClasses Extra type classes that can be used as generic parameters
* @param subResourceClasses Sub resource doclet classes
* @param swaggerVersion Swagger version
* @param apiVersion Overall API version
* @param basePath Overall base path
*/
public CrossClassApiParser(DocletOptions options, ClassDoc classDoc, Collection classes, Map subResourceClasses,
Collection typeClasses, String swaggerVersion, String apiVersion, String basePath) {
super();
this.options = options;
this.classDoc = classDoc;
this.classes = classes;
this.typeClasses = typeClasses;
this.subResourceClasses = subResourceClasses;
this.rootPath = ParserHelper.resolveClassPath(classDoc, options);
this.swaggerVersion = swaggerVersion;
this.apiVersion = apiVersion;
this.basePath = basePath;
this.parentMethod = null;
}
/**
* This creates a CrossClassApiParser for parsing a subresource
* @param options The options for parsing
* @param classDoc The class doc
* @param classes The doclet classes to document
* @param typeClasses Extra type classes that can be used as generic parameters
* @param subResourceClasses Sub resource doclet classes
* @param swaggerVersion Swagger version
* @param apiVersion Overall API version
* @param basePath Overall base path
* @param parentMethod The parent method that "owns" this sub resource
* @param parentResourcePath The parent resource path
*/
public CrossClassApiParser(DocletOptions options, ClassDoc classDoc, Collection classes, Map subResourceClasses,
Collection typeClasses, String swaggerVersion, String apiVersion, String basePath, Method parentMethod, String parentResourcePath) {
super();
this.options = options;
this.classDoc = classDoc;
this.classes = classes;
this.typeClasses = typeClasses;
this.subResourceClasses = subResourceClasses;
this.rootPath = parentResourcePath + ParserHelper.resolveClassPath(classDoc, options);
this.swaggerVersion = swaggerVersion;
this.apiVersion = apiVersion;
this.basePath = basePath;
this.parentMethod = parentMethod;
}
/**
* This gets the root jaxrs path of the api resource class
* @return The root path
*/
public String getRootPath() {
return this.rootPath;
}
/**
* This parses the api declarations from the resource classes of the api
* @param declarations The map of resource name to declaration which will be added to
*/
public void parse(Map declarations) {
Collection allClasses = new ArrayList();
allClasses.addAll(this.classes);
allClasses.addAll(this.typeClasses);
ClassDocCache classCache = new ClassDocCache(allClasses);
// see if this is a resource class, it is if either it has class level @Path or has @GET etc on one of its methods
// (sub resource classes don't have @Path but will have method annotations)
if (this.rootPath.isEmpty()) {
boolean methodFound = false;
for (MethodDoc method : this.classDoc.methods()) {
if (ParserHelper.resolveMethodHttpMethod(method) != null) {
methodFound = true;
break;
}
}
if (!methodFound) {
if (this.options.isLogDebug()) {
System.out.println("ignoring non resource class: " + this.classDoc.name());
}
return;
}
}
ClassDoc currentClassDoc = this.classDoc;
while (currentClassDoc != null) {
if (this.options.isLogDebug()) {
System.out.println("processing resource class: " + currentClassDoc.name());
}
// read default error type for class
String defaultErrorTypeClass = ParserHelper.getInheritableTagValue(currentClassDoc, this.options.getDefaultErrorTypeTags(), this.options);
Type defaultErrorType = ParserHelper.findModel(this.classes, defaultErrorTypeClass);
Set classModels = new HashSet();
if (this.options.isParseModels() && defaultErrorType != null) {
classModels.addAll(new ApiModelParser(this.options, this.options.getTranslator(), defaultErrorType, null, this.classes).parse());
}
// read class level resource path, priority and description
String classResourcePath = ParserHelper.getInheritableTagValue(currentClassDoc, this.options.getResourceTags(), this.options);
String classResourcePriority = ParserHelper.getInheritableTagValue(currentClassDoc, this.options.getResourcePriorityTags(), this.options);
String classResourceDescription = ParserHelper.getInheritableTagValue(currentClassDoc, this.options.getResourceDescriptionTags(), this.options);
// check if its a sub resource
boolean isSubResourceClass = this.subResourceClasses != null && this.subResourceClasses.values().contains(currentClassDoc);
// dont process a subresource outside the context of its parent method
if (isSubResourceClass && this.parentMethod == null) {
// skip
if (this.options.isLogDebug()) {
System.out.println("skipping class as its a sub resource class and we are outside of the parent method context.");
}
} else {
for (MethodDoc method : currentClassDoc.methods()) {
if (this.options.isLogDebug()) {
System.out.println("processing method: " + method.name());
}
ApiMethodParser methodParser = this.parentMethod == null ? new ApiMethodParser(this.options, this.rootPath, method, allClasses,
defaultErrorTypeClass) : new ApiMethodParser(this.options, this.parentMethod, method, allClasses, defaultErrorTypeClass);
Method parsedMethod = methodParser.parse();
if (parsedMethod == null) {
if (this.options.isLogDebug()) {
System.out.println("skipping method: " + method.name() + " as it was not parsed to an api method");
}
continue;
}
// see which resource path to use for the method, if its got a resourceTag then use that
// otherwise use the root path
String resourcePath = buildResourcePath(classResourcePath, method);
if (parsedMethod.isSubResource()) {
if (this.options.isLogDebug()) {
System.out.println("parsing method: " + method.name() + " as a subresource");
}
ClassDoc subResourceClassDoc = classCache.findByType(method.returnType());
if (subResourceClassDoc != null) {
// delete class from the dictionary to handle recursive sub-resources
Collection shrunkClasses = new ArrayList(this.classes);
shrunkClasses.remove(currentClassDoc);
// recursively parse the sub-resource class
CrossClassApiParser subResourceParser = new CrossClassApiParser(this.options, subResourceClassDoc, shrunkClasses,
this.subResourceClasses, this.typeClasses, this.swaggerVersion, this.apiVersion, this.basePath, parsedMethod, resourcePath);
subResourceParser.parse(declarations);
}
continue;
}
ApiDeclaration declaration = declarations.get(resourcePath);
if (declaration == null) {
declaration = new ApiDeclaration(this.swaggerVersion, this.apiVersion, this.basePath, resourcePath, null, null, Integer.MAX_VALUE, null);
declaration.setApis(new ArrayList());
declaration.setModels(new HashMap());
declarations.put(resourcePath, declaration);
if (this.options.isLogDebug()) {
System.out.println("creating new api declaration for method: " + method.name());
}
} else {
if (this.options.isLogDebug()) {
System.out.println("reusing api declaration (" + declaration.getResourcePath() + ") for method: " + method.name());
}
}
// look for a priority tag for the resource listing and set on the resource if the resource hasn't had one set
setApiPriority(classResourcePriority, method, currentClassDoc, declaration);
// look for a method level description tag for the resource listing and set on the resource if the resource hasn't had one set
setApiDeclarationDescription(classResourceDescription, method, declaration);
// find api this method should be added to
addMethod(method, parsedMethod, declaration);
// add models
Set methodModels = methodParser.models();
Map idToModels = addApiModels(classModels, methodModels, method);
declaration.getModels().putAll(idToModels);
if (this.options.isLogDebug()) {
System.out.println("finished processing for method: " + method.name());
}
}
}
currentClassDoc = currentClassDoc.superclass();
// ignore parent object class
if (!ParserHelper.hasAncestor(currentClassDoc)) {
break;
}
}
}
private String buildResourcePath(String classResourcePath, MethodDoc method) {
String resourcePath = getRootPath();
if (classResourcePath != null) {
resourcePath = classResourcePath;
}
if (this.options.getResourceTags() != null) {
for (String resourceTag : this.options.getResourceTags()) {
Tag[] tags = method.tags(resourceTag);
if (tags != null && tags.length > 0) {
resourcePath = tags[0].text();
resourcePath = resourcePath.toLowerCase();
resourcePath = resourcePath.trim().replace(" ", "_");
break;
}
}
}
// sanitize the path and ensure it starts with /
if (resourcePath != null) {
resourcePath = ParserHelper.sanitizePath(resourcePath);
if (!resourcePath.startsWith("/")) {
resourcePath = "/" + resourcePath;
}
}
return resourcePath;
}
private Map addApiModels(Set classModels, Set methodModels, MethodDoc method) {
methodModels.addAll(classModels);
Map idToModels = Collections.emptyMap();
try {
idToModels = uniqueIndex(methodModels, new Function() {
public String apply(Model model) {
return model.getId();
}
});
} catch (Exception ex) {
throw new IllegalStateException(
"Detected duplicate models, if you use classes with the same name from different packages please set the doclet option -useFullModelIds and retry. The problematic method was : "
+ method + ", and models were: " + methodModels, ex);
}
return idToModels;
}
private void setApiPriority(String classResourcePriority, MethodDoc method, ClassDoc currentClassDoc, ApiDeclaration declaration) {
int priorityVal = Integer.MAX_VALUE;
String priority = ParserHelper.getInheritableTagValue(method, this.options.getResourcePriorityTags(), this.options);
if (priority != null) {
priorityVal = Integer.parseInt(priority);
} else if (classResourcePriority != null) {
// set from the class
priorityVal = Integer.parseInt(classResourcePriority);
}
if (priorityVal != Integer.MAX_VALUE && declaration.getPriority() == Integer.MAX_VALUE) {
declaration.setPriority(priorityVal);
}
}
private void setApiDeclarationDescription(String classResourceDescription, MethodDoc method, ApiDeclaration declaration) {
String description = ParserHelper.getInheritableTagValue(method, this.options.getResourceDescriptionTags(), this.options);
if (description == null) {
description = classResourceDescription;
}
if (description != null && declaration.getDescription() == null) {
declaration.setDescription(this.options.replaceVars(description));
}
}
private void addMethod(MethodDoc method, Method parsedMethod, ApiDeclaration declaration) {
Api methodApi = null;
for (Api api : declaration.getApis()) {
if (parsedMethod.getPath().equals(api.getPath())) {
methodApi = api;
break;
}
}
// read api level description
String apiDescription = ParserHelper.getInheritableTagValue(method, this.options.getApiDescriptionTags(), this.options);
if (methodApi == null) {
methodApi = new Api(parsedMethod.getPath(), this.options.replaceVars(apiDescription), new ArrayList());
declaration.getApis().add(methodApi);
} else if (methodApi.getDescription() == null && apiDescription != null) {
methodApi.setDescription(apiDescription);
}
boolean alreadyAdded = false;
// skip already added declarations
for (Operation operation : methodApi.getOperations()) {
boolean opParamsEmptyOrNull = operation.getParameters() == null || operation.getParameters().isEmpty();
boolean parsedParamsEmptyOrNull = parsedMethod.getParameters() == null || parsedMethod.getParameters().isEmpty();
if (operation.getMethod().equals(parsedMethod.getMethod())
&& ((parsedParamsEmptyOrNull && opParamsEmptyOrNull) || (!opParamsEmptyOrNull && !parsedParamsEmptyOrNull && operation.getParameters()
.size() == parsedMethod.getParameters().size())) && equal(operation.getNickname(), parsedMethod.getMethodName())) {
alreadyAdded = true;
}
}
if (!alreadyAdded) {
methodApi.getOperations().add(new Operation(parsedMethod));
}
}
}