Skip to content

Extract parameters from parameter object #505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
charset = utf-8
end_of_line = lf
tab_width = 4
indent_style = tab

[*.json]
indent_style = space
indent_size = 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.springdoc.api.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParameterObject {}
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,6 @@

package org.springdoc.core;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.models.Components;
Expand All @@ -53,29 +30,32 @@
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.api.annotations.ParameterObject;
import org.springdoc.core.converters.AdditionalModelsConverter;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.ParameterCustomizer;

import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpMethod;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.util.UriComponentsBuilder;

import javax.validation.constraints.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.springdoc.core.Constants.OPENAPI_ARRAY_TYPE;
import static org.springdoc.core.Constants.OPENAPI_STRING_TYPE;
import static org.springdoc.core.Constants.QUERY_PARAM;
Expand Down Expand Up @@ -160,42 +140,57 @@ public Operation build(HandlerMethod handlerMethod, RequestMethod requestMethod,
// requests
String[] pNames = this.localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
MethodParameter[] parameters = handlerMethod.getMethodParameters();
String[] reflectionParametersNames = Arrays.stream(parameters).map(MethodParameter::getParameterName).toArray(String[]::new);
if (pNames == null)
pNames = reflectionParametersNames;

List<MethodParameter> explodedParameters = new ArrayList<>();
for (int i = 0; i < parameters.length; ++i) {
MethodParameter p = parameters[i];
if (p.hasParameterAnnotation(ParameterObject.class)) {
Class<?> paramClass = AdditionalModelsConverter.getReplacement(p.getParameterType());
Stream.of(paramClass.getDeclaredFields())
.map(f -> DelegatingMethodParameter.fromGetterOfField(paramClass, f))
.filter(Objects::nonNull)
.forEach(explodedParameters::add);
} else {
String name = pNames != null ? pNames[i] : p.getParameterName();
explodedParameters.add(new DelegatingMethodParameter(p, name, null));
}
}
parameters = explodedParameters.toArray(new MethodParameter[0]);

RequestBodyInfo requestBodyInfo = new RequestBodyInfo();
List<Parameter> operationParameters = (operation.getParameters() != null) ? operation.getParameters() : new ArrayList<>();
Map<String, io.swagger.v3.oas.annotations.Parameter> parametersDocMap = getApiParameters(handlerMethod.getMethod());
Components components = openAPI.getComponents();

for (int i = 0; i < pNames.length; i++) {
for (MethodParameter methodParameter : parameters) {
// check if query param
Parameter parameter = null;
final String pName = pNames[i] == null ? reflectionParametersNames[i] : pNames[i];
MethodParameter methodParameter = parameters[i];
io.swagger.v3.oas.annotations.Parameter parameterDoc = methodParameter.getParameterAnnotation(io.swagger.v3.oas.annotations.Parameter.class);
if (parameterDoc == null)
parameterDoc = parametersDocMap.get(pName);
if (parameterDoc == null) {
parameterDoc = parametersDocMap.get(methodParameter.getParameterName());
}
// use documentation as reference
if (parameterDoc != null) {
if (parameterDoc.hidden())
if (parameterDoc.hidden()) {
continue;
}
parameter = parameterBuilder.buildParameterFromDoc(parameterDoc, null,
methodAttributes.getJsonViewAnnotation());
}

if (!isParamToIgnore(methodParameter)) {
ParameterInfo parameterInfo = new ParameterInfo(pName, methodParameter, parameter);
ParameterInfo parameterInfo = new ParameterInfo(methodParameter.getParameterName(), methodParameter, parameter);
parameter = buildParams(parameterInfo, components, requestMethod,
methodAttributes.getJsonViewAnnotation());
// Merge with the operation parameters
parameter = parameterBuilder.mergeParameter(operationParameters, parameter);
List<Annotation> parameterAnnotations = Arrays.asList(methodParameter.getParameterAnnotations());
if (isValidParameter(parameter))
if (isValidParameter(parameter)) {
applyBeanValidatorAnnotations(parameter, parameterAnnotations);
else if (!RequestMethod.GET.equals(requestMethod)) {
if (operation.getRequestBody() != null)
} else if (!RequestMethod.GET.equals(requestMethod)) {
if (operation.getRequestBody() != null) {
requestBodyInfo.setRequestBody(operation.getRequestBody());
}
requestBodyBuilder.calculateRequestBodyInfo(components, methodAttributes,
parameterInfo, requestBodyInfo);
applyBeanValidatorAnnotations(requestBodyInfo.getRequestBody(), parameterAnnotations);
Expand All @@ -205,7 +200,7 @@ else if (!RequestMethod.GET.equals(requestMethod)) {
}

LinkedHashMap<String, Parameter> map = getParameterLinkedHashMap(components, methodAttributes, operationParameters, parametersDocMap);
setParams(operation, new ArrayList<Parameter>(map.values()), requestBodyInfo);
setParams(operation, new ArrayList<>(map.values()), requestBodyInfo);
// allow for customisation
return customiseOperation(operation, handlerMethod);
}
Expand Down Expand Up @@ -297,7 +292,7 @@ else if (pathVar != null) {
String name = StringUtils.isBlank(pathVar.value()) ? pName : pathVar.value();
parameterInfo.setpName(name);
// check if PATH PARAM
requestInfo = new RequestInfo(ParameterIn.PATH.toString(), pathVar.value(), Boolean.TRUE, null);
requestInfo = new RequestInfo(ParameterIn.PATH.toString(), pathVar.value(), !methodParameter.isOptional(), null);
parameter = buildParam(parameterInfo, components, requestInfo, jsonView);
}
else if (cookieValue != null) {
Expand All @@ -307,7 +302,7 @@ else if (cookieValue != null) {
}
// By default
if (RequestMethod.GET.equals(requestMethod) || (parameterInfo.getParameterModel() != null && ParameterIn.PATH.toString().equals(parameterInfo.getParameterModel().getIn())))
parameter = this.buildParam(QUERY_PARAM, components, parameterInfo, Boolean.TRUE, null, jsonView);
parameter = this.buildParam(QUERY_PARAM, components, parameterInfo, !methodParameter.isOptional(), null, jsonView);

return parameter;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.springdoc.core;

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.Objects;
import java.util.stream.Stream;

class DelegatingMethodParameter extends MethodParameter {
private volatile MethodParameter delegate;
private Annotation[] additionalParameterAnnotations;
private String parameterName;

DelegatingMethodParameter(MethodParameter delegate, String parameterName, Annotation[] additionalParameterAnnotations) {
super(delegate);
this.delegate = delegate;
this.additionalParameterAnnotations = additionalParameterAnnotations;
this.parameterName = parameterName;
}

@Nullable
static MethodParameter fromGetterOfField(Class<?> paramClass, Field field) {
try {
return Stream.of(Introspector.getBeanInfo(paramClass).getPropertyDescriptors())
.filter(d -> d.getName().equals(field.getName()))
.map(PropertyDescriptor::getReadMethod)
.filter(Objects::nonNull)
.findFirst()
.map(method -> new MethodParameter(method, -1))
.map(param -> new DelegatingMethodParameter(param, field.getName(), field.getDeclaredAnnotations()))
.orElse(null);
} catch (IntrospectionException e) {
return null;
}
}

@Override
@NonNull
public Annotation[] getParameterAnnotations() {
return ArrayUtils.addAll(delegate.getParameterAnnotations(), additionalParameterAnnotations);
}

@Override
public String getParameterName() {
return parameterName;
}

@Override
public Method getMethod() {
return delegate.getMethod();
}

@Override
public Constructor<?> getConstructor() {
return delegate.getConstructor();
}

@Override
public Class<?> getDeclaringClass() {
return delegate.getDeclaringClass();
}

@Override
public Member getMember() {
return delegate.getMember();
}

@Override
public AnnotatedElement getAnnotatedElement() {
return delegate.getAnnotatedElement();
}

@Override
public Executable getExecutable() {
return delegate.getExecutable();
}

@Override
public MethodParameter withContainingClass(Class<?> containingClass) {
return delegate.withContainingClass(containingClass);
}

@Override
public Class<?> getContainingClass() {
return delegate.getContainingClass();
}

@Override
public Class<?> getParameterType() {
return delegate.getParameterType();
}

@Override
public Type getGenericParameterType() {
return delegate.getGenericParameterType();
}

@Override
public Class<?> getNestedParameterType() {
return delegate.getNestedParameterType();
}

@Override
public Type getNestedGenericParameterType() {
return delegate.getNestedGenericParameterType();
}

@Override
public void initParameterNameDiscovery(ParameterNameDiscoverer parameterNameDiscoverer) {
delegate.initParameterNameDiscovery(parameterNameDiscoverer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@

package org.springdoc.core.converters;


import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import com.fasterxml.jackson.databind.JavaType;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.media.Schema;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class AdditionalModelsConverter implements ModelConverter {

private static final Map<Class, Class> modelToClassMap = new HashMap();
Expand All @@ -44,15 +43,21 @@ public static void replaceWithSchema(Class source, Schema target) {
modelToSchemaMap.put(source, target);
}

public static Class getReplacement(Class clazz) {
return modelToClassMap.getOrDefault(clazz, clazz);
}

@Override
public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
JavaType javaType = Json.mapper().constructType(type.getType());
if (javaType != null) {
Class<?> cls = javaType.getRawClass();
if (modelToSchemaMap.containsKey(cls))
if (modelToSchemaMap.containsKey(cls)) {
return modelToSchemaMap.get(cls);
if (modelToClassMap.containsKey(cls))
}
if (modelToClassMap.containsKey(cls)) {
type = new AnnotatedType(modelToClassMap.get(cls)).resolveAsRef(true);
}
}
return (chain.hasNext()) ? chain.next().resolve(type, context, chain) : null;
}
Expand Down
Loading