All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.carma.swagger.doclet.parser.ParameterReader Maven / Gradle / Ivy

package com.carma.swagger.doclet.parser;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.carma.swagger.doclet.DocletOptions;
import com.carma.swagger.doclet.model.ApiParameter;
import com.carma.swagger.doclet.model.Model;
import com.carma.swagger.doclet.model.Property;
import com.carma.swagger.doclet.parser.ParserHelper.NumericTypeFilter;
import com.carma.swagger.doclet.translator.Translator;
import com.carma.swagger.doclet.translator.Translator.OptionalName;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ParameterizedType;
import com.sun.javadoc.Type;

/**
 * The ParameterReader represents a utility class that supports reading api parameters
 * @version $Id$
 * @author conor.roche
 */
public class ParameterReader {

	private static final String JAX_RS_PATH_PARAM = "javax.ws.rs.PathParam";
	private static final String JAX_RS_PATH = "javax.ws.rs.Path";

	private static final Pattern PARAM_PATTERN = Pattern.compile("\\{[^}]+\\}");

	private final DocletOptions options;
	private final Collection allClasses;
	private final Translator translator;

	private List paramNames;
	private ClassDoc classDoc;

	/**
	 * This creates a PathParameterReader
	 * @param options the doclet options
	 * @param allClasses All classes for looking up types
	 */
	public ParameterReader(DocletOptions options, Collection allClasses) {
		this.options = options;
		this.translator = options == null ? null : options.getTranslator();
		this.allClasses = allClasses;
	}

	/**
	 * This reads a class storing any path parameters from the class level
	 * @param classDoc The class to read
	 */
	public void readClass(ClassDoc classDoc) {
		// reset paramNames
		if (this.paramNames == null) {
			this.paramNames = new ArrayList();
		} else {
			this.paramNames.clear();
		}

		this.classDoc = classDoc;
		String classLevelPath = ParserHelper.getInheritableClassLevelAnnotationValue(this.classDoc, this.options, JAX_RS_PATH, "value");
		if (classLevelPath != null) {
			// extract names/regex patterns of all params from the path
			addPathParams(classLevelPath, this.paramNames);
		}

	}

	void addPathParams(String path, Collection addToCollection) {
		Matcher m = PARAM_PATTERN.matcher(path);
		while (m.find()) {
			int start = m.start();
			int end = m.end();
			if (start < end - 1) {
				String withBraces = path.substring(start, end);
				String withoutBraces = withBraces.substring(1, withBraces.length() - 1).trim();
				int colonPos = withBraces.indexOf(':');
				String name = colonPos == -1 ? withoutBraces : withoutBraces.substring(0, colonPos - 1);
				addToCollection.add(name);
			}
		}
	}

	/**
	 * This gets the names of parameters in the class level @Path expression
	 * @return the list of parameter names reference in the class level expression
	 */
	public List getClassLevelParamNames() {
		return this.paramNames;
	}

	/**
	 * This reads a list of class parameters. For a class parameter to be returned it must
	 * a) be a variable in the path of the class or one of its methods and b) have a PathParam
	 * annotation either on a field or constructor
	 * @param models The model set to add any new encountered models to
	 * @return A list of class level api parameters
	 */
	public List readClassLevelParameters(Set models) {

		List params = new ArrayList();

		// add field path params
		FieldDoc[] fields = this.classDoc.fields(false);
		if (fields != null) {
			for (FieldDoc field : fields) {
				AnnotationParser p = new AnnotationParser(field, this.options);
				if (p.isAnnotatedBy(JAX_RS_PATH_PARAM)) {
					String paramName = p.getAnnotationValue(JAX_RS_PATH_PARAM, "value");
					if (paramName == null || paramName.isEmpty()) {
						paramName = field.name();
					}

					// if this path param is one of the param names of the class path then add it
					if (this.paramNames.contains(paramName)) {
						params.add(buildClassFieldApiParam(field));
					}
				}
			}
		}

		// add constructor path params
		ConstructorDoc[] constructors = this.classDoc.constructors(false);
		for (ConstructorDoc constructor : constructors) {
			Parameter[] parameters = constructor.parameters();
			for (Parameter param : parameters) {
				AnnotationParser p = new AnnotationParser(param, this.options);
				if (p.isAnnotatedBy(JAX_RS_PATH_PARAM)) {
					String paramName = p.getAnnotationValue(JAX_RS_PATH_PARAM, "value");
					if (paramName == null || paramName.isEmpty()) {
						paramName = param.name();
					}

					// if this path param is one of the param names of the class path then add it
					if (this.paramNames.contains(paramName)) {

						Set rawParamNames = ParserHelper.getParamNames(constructor);
						Set allParamNames = new HashSet(rawParamNames);

						params.addAll(buildApiParams(constructor, param, false, allParamNames, models));
					}
				}
			}
		}

		return params;
	}

	/**
	 * This builds an Api parameter from a method or constructor parameter
	 * @param method The method or constructor
	 * @param parameter The parameter to build the api parameter for
	 * @param consumesMultipart whether the method consumes multipart
	 * @param allParamNames A list of all parameter names on the method
	 * @param models The model set to add any new encountered models to
	 * @return The list of parameters (can be multiple if bean params are encountered)
	 */
	public List buildApiParams(ExecutableMemberDoc method, Parameter parameter, boolean consumesMultipart, Set allParamNames,
			Set models) {

		// TODO cache these constructor/method level params so they are not reprocessed for each parameter

		// read required and optional params
		Set optionalParams = ParserHelper.getMatchingParams(method, allParamNames, this.options.getOptionalParamsTags(),
				this.options.getOptionalParamAnnotations(), this.options);

		Set requiredParams = ParserHelper.getMatchingParams(method, allParamNames, this.options.getRequiredParamsTags(),
				this.options.getRequiredParamAnnotations(), this.options);

		// read csv params
		List csvParams = ParserHelper.getCsvParams(method, allParamNames, this.options.getCsvParamsTags(), this.options);

		// read formats
		Map paramFormats = ParserHelper.getMethodParamNameValuePairs(method, allParamNames, this.options.getParamsFormatTags(), this.options);

		// read min and max values of params
		Map paramMinVals = ParserHelper.getParameterValues(method, allParamNames, this.options.getParamsMinValueTags(),
				this.options.getParamMinValueAnnotations(), new NumericTypeFilter(this.options), this.options, new String[] { "value", "min" });
		Map paramMaxVals = ParserHelper.getParameterValues(method, allParamNames, this.options.getParamsMaxValueTags(),
				this.options.getParamMaxValueAnnotations(), new NumericTypeFilter(this.options), this.options, new String[] { "value", "max" });

		// filter min/max params so they

		// read default values of params
		Map paramDefaultVals = ParserHelper.getMethodParamNameValuePairs(method, allParamNames, this.options.getParamsDefaultValueTags(),
				this.options);

		// read allowable values of params
		Map> paramAllowableVals = ParserHelper.getMethodParamNameValueLists(method, allParamNames,
				this.options.getParamsAllowableValuesTags(), this.options);

		// read override names of params
		Map paramNames = ParserHelper.getMethodParamNameValuePairs(method, allParamNames, this.options.getParamsNameTags(), this.options);

		Type paramType = getParamType(parameter.type());
		String paramCategory = ParserHelper.paramTypeOf(consumesMultipart, parameter, this.options);
		String paramName = parameter.name();

		List res = new ArrayList();

		// see if its a special composite type e.g. @BeanParam
		if ("composite".equals(paramCategory)) {

			ApiModelParser modelParser = new ApiModelParser(this.options, this.translator, paramType, consumesMultipart, true);
			Set compositeModels = modelParser.parse();
			String rootModelId = modelParser.getRootModelId();
			for (Model model : compositeModels) {
				if (model.getId().equals(rootModelId)) {
					List requiredFields = model.getRequiredFields();
					List optionalFields = model.getOptionalFields();
					Map modelProps = model.getProperties();
					for (Map.Entry entry : modelProps.entrySet()) {
						Property property = entry.getValue();
						String renderedParamName = entry.getKey();
						String rawFieldName = property.getRawFieldName();

						Boolean allowMultiple = getAllowMultiple(paramCategory, rawFieldName, csvParams);

						// see if there is a required javadoc tag directly on the bean param field, if so use that
						Boolean required = null;
						if (requiredFields != null && requiredFields.contains(renderedParamName)) {
							required = Boolean.TRUE;
						} else if (optionalFields != null && optionalFields.contains(renderedParamName)) {
							required = Boolean.FALSE;
						} else {
							required = getRequired(paramCategory, rawFieldName, property.getType(), optionalParams, requiredParams);
						}

						String itemsRef = property.getItems() == null ? null : property.getItems().getRef();
						String itemsType = property.getItems() == null ? null : property.getItems().getType();
						String itemsFormat = property.getItems() == null ? null : property.getItems().getFormat();
						List itemsAllowableValues = property.getItems() == null ? null : property.getItems().getAllowableValues();

						ApiParameter param = new ApiParameter(property.getParamCategory(), renderedParamName, required, allowMultiple, property.getType(),
								property.getFormat(), property.getDescription(), itemsRef, itemsType, itemsFormat, itemsAllowableValues, property.getUniqueItems(),
								property.getAllowableValues(), property.getMinimum(), property.getMaximum(), property.getDefaultValue());

						res.add(param);
					}
				}
			}

			return res;
		}

		// look for a custom input type for body params
		if ("body".equals(paramCategory)) {
			String customParamType = ParserHelper.getInheritableTagValue(method, this.options.getInputTypeTags(), this.options);
			paramType = readCustomParamType(customParamType, paramType, models);
		}

		OptionalName paramTypeFormat = this.translator.parameterTypeName(consumesMultipart, parameter, paramType);
		String typeName = paramTypeFormat.value();
		String format = paramTypeFormat.getFormat();

		// overide format if possible
		if (format == null) {
			format = paramFormats.get(paramName);
		}

		Boolean allowMultiple = null;
		List allowableValues = null;
		String itemsRef = null;
		String itemsType = null;
		String itemsFormat = null;
		List itemsAllowableValues = null;
		Boolean uniqueItems = null;
		String minimum = null;
		String maximum = null;
		String defaultVal = null;

		// set to form param type if data type is File
		if ("File".equals(typeName)) {
			paramCategory = "form";
		} else {

			Type containerOf = ParserHelper.getContainerType(paramType, null, this.allClasses);

			if (this.options.isParseModels()) {
				Type modelType = containerOf == null ? paramType : containerOf;
				models.addAll(new ApiModelParser(this.options, this.translator, modelType).parse());
			}

			// set enum values
			// a) if param type is enum build based on enum values
			ClassDoc typeClassDoc = parameter.type().asClassDoc();
			allowableValues = ParserHelper.getAllowableValues(typeClassDoc);
			if (allowableValues == null) {
				// b) if the method has a javadoc tag for allowable values use that
				allowableValues = paramAllowableVals.get(paramName);
			}

			if (allowableValues != null && !allowableValues.isEmpty()) {
				typeName = "string";
			}

			// set whether its a csv param
			allowMultiple = getAllowMultiple(paramCategory, paramName, csvParams);

			// get min and max param values
			minimum = paramMinVals.get(paramName);
			maximum = paramMaxVals.get(paramName);

			String validationContext = " for the method: " + method.name() + " parameter: " + paramName;

			// verify min max are numbers
			ParserHelper.verifyNumericValue(validationContext + " min value.", typeName, format, minimum);
			ParserHelper.verifyNumericValue(validationContext + " max value.", typeName, format, maximum);

			// get a default value, prioritize the jaxrs annotation
			// otherwise look for the javadoc tag
			defaultVal = ParserHelper.getDefaultValue(parameter, this.options);
			if (defaultVal == null) {
				defaultVal = paramDefaultVals.get(paramName);
			}

			// verify default vs min, max and by itself
			if (defaultVal != null) {
				if (minimum == null && maximum == null) {
					// just validate the default
					ParserHelper.verifyValue(validationContext + " default value.", typeName, format, defaultVal);
				}
				// if min/max then default is validated as part of comparison
				if (minimum != null) {
					int comparison = ParserHelper.compareNumericValues(validationContext + " min value.", typeName, format, defaultVal, minimum);
					if (comparison < 0) {
						throw new IllegalStateException("Invalid value for the default value of the method: " + method.name() + " parameter: " + paramName
								+ " it should be >= the minimum: " + minimum);
					}
				}
				if (maximum != null) {
					int comparison = ParserHelper.compareNumericValues(validationContext + " max value.", typeName, format, defaultVal, maximum);
					if (comparison > 0) {
						throw new IllegalStateException("Invalid value for the default value of the method: " + method.name() + " parameter: " + paramName
								+ " it should be <= the maximum: " + maximum);
					}
				}

				// if boolean then make lowercase
				if ("boolean".equalsIgnoreCase(typeName)) {
					defaultVal = defaultVal.toLowerCase();
				}
			}

			// if enum and default value check it matches the enum values
			if (allowableValues != null && defaultVal != null && !allowableValues.contains(defaultVal)) {
				throw new IllegalStateException("Invalid value: " + defaultVal + " for the default value of the method: " + method.name() + " parameter: "
						+ paramName + " it should be one of: " + allowableValues);
			}

			// set collection related fields
			// TODO: consider supporting parameterized collections as api parameters...
			if (containerOf != null) {
				itemsAllowableValues = ParserHelper.getAllowableValues(containerOf.asClassDoc());
				if (itemsAllowableValues != null) {
					itemsType = "string";
				} else {
					OptionalName oName = this.translator.typeName(containerOf);
					if (ParserHelper.isPrimitive(containerOf, this.options)) {
						itemsType = oName.value();
						itemsFormat = oName.getFormat();
					} else {
						itemsRef = oName.value();
					}
				}
			}

			if (typeName.equals("array")) {
				if (ParserHelper.isSet(paramType.qualifiedTypeName())) {
					uniqueItems = Boolean.TRUE;
				}
			}
		}

		// get whether required
		Boolean required = getRequired(paramCategory, paramName, typeName, optionalParams, requiredParams);

		// get the parameter name to use for the documentation
		String renderedParamName = ParserHelper.paramNameOf(parameter, paramNames, this.options.getParameterNameAnnotations(), this.options);

		// get description
		String description = this.options.replaceVars(commentForParameter(method, parameter));

		// build parameter
		ApiParameter param = new ApiParameter(paramCategory, renderedParamName, required, allowMultiple, typeName, format, description, itemsRef, itemsType,
				itemsFormat, itemsAllowableValues, uniqueItems, allowableValues, minimum, maximum, defaultVal);

		res.add(param);

		return res;
	}

	private ApiParameter buildClassFieldApiParam(FieldDoc field) {

		Type paramType = field.type();

		OptionalName paramTypeFormat = this.translator.typeName(paramType);
		String typeName = paramTypeFormat.value();
		String format = paramTypeFormat.getFormat();

		Boolean allowMultiple = null;
		List allowableValues = null;
		String itemsRef = null;
		String itemsType = null;
		String itemsFormat = null;
		List itemsAllowableValues = null;
		Boolean uniqueItems = null;
		String minimum = null;
		String maximum = null;
		String defaultVal = null;

		Type containerOf = ParserHelper.getContainerType(paramType, null, this.allClasses);

		// set enum values
		ClassDoc typeClassDoc = paramType.asClassDoc();
		allowableValues = ParserHelper.getAllowableValues(typeClassDoc);
		if (allowableValues != null) {
			typeName = "string";
		}

		// TODO set whether its a csv param
		// TODO get min and max param values
		// TODO set default

		// set collection related fields
		// TODO: consider supporting parameterized collections as api parameters...
		if (containerOf != null) {
			itemsAllowableValues = ParserHelper.getAllowableValues(containerOf.asClassDoc());
			if (itemsAllowableValues != null) {
				itemsType = "string";
			} else {
				OptionalName oName = this.translator.typeName(containerOf);
				if (ParserHelper.isPrimitive(containerOf, this.options)) {
					itemsType = oName.value();
					itemsFormat = oName.getFormat();
				} else {
					itemsRef = oName.value();
				}
			}
		}

		if (typeName.equals("array")) {
			if (ParserHelper.isSet(paramType.qualifiedTypeName())) {
				uniqueItems = Boolean.TRUE;
			}
		}

		// get whether required
		Boolean required = Boolean.TRUE;

		// get the parameter name to use for the documentation
		// TODO support name overriding
		Map overrideParamNames = null;
		String renderedParamName = ParserHelper.fieldParamNameOf(field, overrideParamNames, this.options.getParameterNameAnnotations(), this.options);

		// TODO get description from field
		String description = null;

		// build parameter
		ApiParameter param = new ApiParameter("path", renderedParamName, required, allowMultiple, typeName, format, description, itemsRef, itemsType,
				itemsFormat, itemsAllowableValues, uniqueItems, allowableValues, minimum, maximum, defaultVal);

		return param;

	}

	private String commentForParameter(ExecutableMemberDoc method, Parameter parameter) {
		for (ParamTag tag : method.paramTags()) {
			if (tag.parameterName().equals(parameter.name())) {
				return tag.parameterComment();
			}
		}
		return "";
	}

	private Boolean getAllowMultiple(String paramCategory, String paramName, List csvParams) {
		Boolean allowMultiple = null;
		if ("query".equals(paramCategory) || "path".equals(paramCategory) || "header".equals(paramCategory)) {
			if (csvParams != null && csvParams.contains(paramName)) {
				allowMultiple = Boolean.TRUE;
			}
		}
		return allowMultiple;
	}

	private Boolean getRequired(String paramCategory, String paramName, String typeName, Collection optionalParams, Collection requiredParams) {
		// set whether the parameter is required or not
		Boolean required = null;
		// if its a path param then its required as per swagger spec
		if ("path".equals(paramCategory)) {
			required = Boolean.TRUE;
		}
		// if its in the required list then its required
		else if (requiredParams.contains(paramName)) {
			required = Boolean.TRUE;
		}
		// else if its in the optional list its optional
		else if (optionalParams.contains(paramName)) {
			// leave as null as this is equivalent to false but doesn't add to the json
		}
		// else if its a body or File param its required
		else if ("body".equals(paramCategory) || ("File".equals(typeName) && "form".equals(paramCategory))) {
			required = Boolean.TRUE;
		}
		// otherwise its optional
		else {
			// leave as null as this is equivalent to false but doesn't add to the json
		}
		return required;
	}

	private Type getParamType(Type type) {
		if (type != null) {
			ParameterizedType pt = type.asParameterizedType();
			if (pt != null) {
				Type[] typeArgs = pt.typeArguments();
				if (typeArgs != null && typeArgs.length > 0) {
					// if its a generic wrapper type then return the wrapped type
					if (this.options.getGenericWrapperTypes().contains(type.qualifiedTypeName())) {
						return typeArgs[0];
					}
				}
			}
		}
		return type;
	}

	private Type readCustomParamType(String customTypeName, Type defaultType, Set models) {
		if (customTypeName != null) {
			// lookup the type from the doclet classes
			Type customType = ParserHelper.findModel(this.allClasses, customTypeName);
			if (customType != null) {
				// also add this custom return type to the models
				if (this.options.isParseModels()) {
					models.addAll(new ApiModelParser(this.options, this.translator, customType).parse());
				}
				return customType;
			}
		}
		return defaultType;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy