Skip to content

Commit a924c00

Browse files
author
Marcin Zarebski
committed
Extract parameters from parameter object
fixes #120
1 parent ef918f0 commit a924c00

File tree

8 files changed

+292
-47
lines changed

8 files changed

+292
-47
lines changed

.editorconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
tab_width = 4
7+
indent_style = tab
8+
9+
[*.json]
10+
indent_style = space
11+
indent_size = 2
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.springdoc.api.annotations;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target(ElementType.PARAMETER)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface ParameterObject {}

springdoc-openapi-common/src/main/java/org/springdoc/core/AbstractRequestBuilder.java

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,6 @@
1818

1919
package org.springdoc.core;
2020

21-
import java.lang.annotation.Annotation;
22-
import java.lang.reflect.Method;
23-
import java.math.BigDecimal;
24-
import java.util.ArrayList;
25-
import java.util.Arrays;
26-
import java.util.HashMap;
27-
import java.util.LinkedHashMap;
28-
import java.util.List;
29-
import java.util.Map;
30-
import java.util.Objects;
31-
import java.util.Optional;
32-
import java.util.Set;
33-
import java.util.stream.Collectors;
34-
import java.util.stream.Stream;
35-
36-
import javax.validation.constraints.DecimalMax;
37-
import javax.validation.constraints.DecimalMin;
38-
import javax.validation.constraints.Max;
39-
import javax.validation.constraints.Min;
40-
import javax.validation.constraints.NotNull;
41-
import javax.validation.constraints.Pattern;
42-
import javax.validation.constraints.Size;
43-
4421
import com.fasterxml.jackson.annotation.JsonView;
4522
import io.swagger.v3.oas.annotations.enums.ParameterIn;
4623
import io.swagger.v3.oas.models.Components;
@@ -53,29 +30,31 @@
5330
import io.swagger.v3.oas.models.parameters.Parameter;
5431
import io.swagger.v3.oas.models.parameters.RequestBody;
5532
import org.apache.commons.lang3.StringUtils;
33+
import org.springdoc.api.annotations.ParameterObject;
5634
import org.springdoc.core.customizers.OperationCustomizer;
5735
import org.springdoc.core.customizers.ParameterCustomizer;
58-
5936
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
6037
import org.springframework.core.MethodParameter;
6138
import org.springframework.core.annotation.AnnotatedElementUtils;
6239
import org.springframework.http.HttpMethod;
6340
import org.springframework.util.CollectionUtils;
6441
import org.springframework.validation.BindingResult;
6542
import org.springframework.validation.Errors;
66-
import org.springframework.web.bind.annotation.CookieValue;
67-
import org.springframework.web.bind.annotation.PathVariable;
68-
import org.springframework.web.bind.annotation.RequestAttribute;
69-
import org.springframework.web.bind.annotation.RequestHeader;
70-
import org.springframework.web.bind.annotation.RequestMethod;
71-
import org.springframework.web.bind.annotation.RequestParam;
72-
import org.springframework.web.bind.annotation.ValueConstants;
43+
import org.springframework.web.bind.annotation.*;
7344
import org.springframework.web.bind.support.SessionStatus;
7445
import org.springframework.web.context.request.NativeWebRequest;
7546
import org.springframework.web.context.request.WebRequest;
7647
import org.springframework.web.method.HandlerMethod;
7748
import org.springframework.web.util.UriComponentsBuilder;
7849

50+
import javax.validation.constraints.*;
51+
import java.lang.annotation.Annotation;
52+
import java.lang.reflect.Method;
53+
import java.math.BigDecimal;
54+
import java.util.*;
55+
import java.util.stream.Collectors;
56+
import java.util.stream.Stream;
57+
7958
import static org.springdoc.core.Constants.OPENAPI_ARRAY_TYPE;
8059
import static org.springdoc.core.Constants.OPENAPI_STRING_TYPE;
8160
import static org.springdoc.core.Constants.QUERY_PARAM;
@@ -160,42 +139,57 @@ public Operation build(HandlerMethod handlerMethod, RequestMethod requestMethod,
160139
// requests
161140
String[] pNames = this.localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
162141
MethodParameter[] parameters = handlerMethod.getMethodParameters();
163-
String[] reflectionParametersNames = Arrays.stream(parameters).map(MethodParameter::getParameterName).toArray(String[]::new);
164-
if (pNames == null)
165-
pNames = reflectionParametersNames;
142+
143+
List<MethodParameter> explodedParameters = new ArrayList<>();
144+
for (int i = 0; i < parameters.length; ++i) {
145+
MethodParameter p = parameters[i];
146+
if (p.hasParameterAnnotation(ParameterObject.class)) {
147+
Class<?> paramClass = p.getParameterType();
148+
Stream.of(paramClass.getDeclaredFields())
149+
.map(f -> DelegatingMethodParameter.fromGetterOfField(paramClass, f))
150+
.filter(Objects::nonNull)
151+
.forEach(explodedParameters::add);
152+
} else {
153+
String name = pNames != null ? pNames[i] : p.getParameterName();
154+
explodedParameters.add(new DelegatingMethodParameter(p, name, null));
155+
}
156+
}
157+
parameters = explodedParameters.toArray(new MethodParameter[0]);
158+
166159
RequestBodyInfo requestBodyInfo = new RequestBodyInfo();
167160
List<Parameter> operationParameters = (operation.getParameters() != null) ? operation.getParameters() : new ArrayList<>();
168161
Map<String, io.swagger.v3.oas.annotations.Parameter> parametersDocMap = getApiParameters(handlerMethod.getMethod());
169162
Components components = openAPI.getComponents();
170163

171-
for (int i = 0; i < pNames.length; i++) {
164+
for (MethodParameter methodParameter : parameters) {
172165
// check if query param
173166
Parameter parameter = null;
174-
final String pName = pNames[i] == null ? reflectionParametersNames[i] : pNames[i];
175-
MethodParameter methodParameter = parameters[i];
176167
io.swagger.v3.oas.annotations.Parameter parameterDoc = methodParameter.getParameterAnnotation(io.swagger.v3.oas.annotations.Parameter.class);
177-
if (parameterDoc == null)
178-
parameterDoc = parametersDocMap.get(pName);
168+
if (parameterDoc == null) {
169+
parameterDoc = parametersDocMap.get(methodParameter.getParameterName());
170+
}
179171
// use documentation as reference
180172
if (parameterDoc != null) {
181-
if (parameterDoc.hidden())
173+
if (parameterDoc.hidden()) {
182174
continue;
175+
}
183176
parameter = parameterBuilder.buildParameterFromDoc(parameterDoc, null,
184177
methodAttributes.getJsonViewAnnotation());
185178
}
186179

187180
if (!isParamToIgnore(methodParameter)) {
188-
ParameterInfo parameterInfo = new ParameterInfo(pName, methodParameter, parameter);
181+
ParameterInfo parameterInfo = new ParameterInfo(methodParameter.getParameterName(), methodParameter, parameter);
189182
parameter = buildParams(parameterInfo, components, requestMethod,
190183
methodAttributes.getJsonViewAnnotation());
191184
// Merge with the operation parameters
192185
parameter = parameterBuilder.mergeParameter(operationParameters, parameter);
193186
List<Annotation> parameterAnnotations = Arrays.asList(methodParameter.getParameterAnnotations());
194-
if (isValidParameter(parameter))
187+
if (isValidParameter(parameter)) {
195188
applyBeanValidatorAnnotations(parameter, parameterAnnotations);
196-
else if (!RequestMethod.GET.equals(requestMethod)) {
197-
if (operation.getRequestBody() != null)
189+
} else if (!RequestMethod.GET.equals(requestMethod)) {
190+
if (operation.getRequestBody() != null) {
198191
requestBodyInfo.setRequestBody(operation.getRequestBody());
192+
}
199193
requestBodyBuilder.calculateRequestBodyInfo(components, methodAttributes,
200194
parameterInfo, requestBodyInfo);
201195
applyBeanValidatorAnnotations(requestBodyInfo.getRequestBody(), parameterAnnotations);
@@ -205,7 +199,7 @@ else if (!RequestMethod.GET.equals(requestMethod)) {
205199
}
206200

207201
LinkedHashMap<String, Parameter> map = getParameterLinkedHashMap(components, methodAttributes, operationParameters, parametersDocMap);
208-
setParams(operation, new ArrayList<Parameter>(map.values()), requestBodyInfo);
202+
setParams(operation, new ArrayList<>(map.values()), requestBodyInfo);
209203
// allow for customisation
210204
return customiseOperation(operation, handlerMethod);
211205
}
@@ -297,7 +291,7 @@ else if (pathVar != null) {
297291
String name = StringUtils.isBlank(pathVar.value()) ? pName : pathVar.value();
298292
parameterInfo.setpName(name);
299293
// check if PATH PARAM
300-
requestInfo = new RequestInfo(ParameterIn.PATH.toString(), pathVar.value(), Boolean.TRUE, null);
294+
requestInfo = new RequestInfo(ParameterIn.PATH.toString(), pathVar.value(), !methodParameter.isOptional(), null);
301295
parameter = buildParam(parameterInfo, components, requestInfo, jsonView);
302296
}
303297
else if (cookieValue != null) {
@@ -307,7 +301,7 @@ else if (cookieValue != null) {
307301
}
308302
// By default
309303
if (RequestMethod.GET.equals(requestMethod) || (parameterInfo.getParameterModel() != null && ParameterIn.PATH.toString().equals(parameterInfo.getParameterModel().getIn())))
310-
parameter = this.buildParam(QUERY_PARAM, components, parameterInfo, Boolean.TRUE, null, jsonView);
304+
parameter = this.buildParam(QUERY_PARAM, components, parameterInfo, !methodParameter.isOptional(), null, jsonView);
311305

312306
return parameter;
313307
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.springdoc.core;
2+
3+
import org.apache.commons.lang3.ArrayUtils;
4+
import org.springframework.core.MethodParameter;
5+
import org.springframework.core.ParameterNameDiscoverer;
6+
import org.springframework.lang.NonNull;
7+
import org.springframework.lang.Nullable;
8+
9+
import java.beans.IntrospectionException;
10+
import java.beans.Introspector;
11+
import java.beans.PropertyDescriptor;
12+
import java.lang.annotation.Annotation;
13+
import java.lang.reflect.*;
14+
import java.util.Objects;
15+
import java.util.stream.Stream;
16+
17+
class DelegatingMethodParameter extends MethodParameter {
18+
private volatile MethodParameter delegate;
19+
private Annotation[] additionalParameterAnnotations;
20+
private String parameterName;
21+
22+
DelegatingMethodParameter(MethodParameter delegate, String parameterName, Annotation[] additionalParameterAnnotations) {
23+
super(delegate);
24+
this.delegate = delegate;
25+
this.additionalParameterAnnotations = additionalParameterAnnotations;
26+
this.parameterName = parameterName;
27+
}
28+
29+
@Nullable
30+
static MethodParameter fromGetterOfField(Class<?> paramClass, Field field) {
31+
try {
32+
return Stream.of(Introspector.getBeanInfo(paramClass).getPropertyDescriptors())
33+
.filter(d -> d.getName().equals(field.getName()))
34+
.map(PropertyDescriptor::getReadMethod)
35+
.filter(Objects::nonNull)
36+
.findFirst()
37+
.map(method -> new MethodParameter(method, -1))
38+
.map(param -> new DelegatingMethodParameter(param, field.getName(), field.getDeclaredAnnotations()))
39+
.orElse(null);
40+
} catch (IntrospectionException e) {
41+
return null;
42+
}
43+
}
44+
45+
@Override
46+
@NonNull
47+
public Annotation[] getParameterAnnotations() {
48+
return ArrayUtils.addAll(delegate.getParameterAnnotations(), additionalParameterAnnotations);
49+
}
50+
51+
@Override
52+
public String getParameterName() {
53+
return parameterName;
54+
}
55+
56+
@Override
57+
public Method getMethod() {
58+
return delegate.getMethod();
59+
}
60+
61+
@Override
62+
public Constructor<?> getConstructor() {
63+
return delegate.getConstructor();
64+
}
65+
66+
@Override
67+
public Class<?> getDeclaringClass() {
68+
return delegate.getDeclaringClass();
69+
}
70+
71+
@Override
72+
public Member getMember() {
73+
return delegate.getMember();
74+
}
75+
76+
@Override
77+
public AnnotatedElement getAnnotatedElement() {
78+
return delegate.getAnnotatedElement();
79+
}
80+
81+
@Override
82+
public Executable getExecutable() {
83+
return delegate.getExecutable();
84+
}
85+
86+
@Override
87+
public MethodParameter withContainingClass(Class<?> containingClass) {
88+
return delegate.withContainingClass(containingClass);
89+
}
90+
91+
@Override
92+
public Class<?> getContainingClass() {
93+
return delegate.getContainingClass();
94+
}
95+
96+
@Override
97+
public Class<?> getParameterType() {
98+
return delegate.getParameterType();
99+
}
100+
101+
@Override
102+
public Type getGenericParameterType() {
103+
return delegate.getGenericParameterType();
104+
}
105+
106+
@Override
107+
public Class<?> getNestedParameterType() {
108+
return delegate.getNestedParameterType();
109+
}
110+
111+
@Override
112+
public Type getNestedGenericParameterType() {
113+
return delegate.getNestedGenericParameterType();
114+
}
115+
116+
@Override
117+
public void initParameterNameDiscovery(ParameterNameDiscoverer parameterNameDiscoverer) {
118+
delegate.initParameterNameDiscovery(parameterNameDiscoverer);
119+
}
120+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package test.org.springdoc.api.app102;
2+
3+
import io.swagger.v3.oas.annotations.Parameter;
4+
import org.springframework.lang.Nullable;
5+
6+
public class RequestParams {
7+
@Nullable
8+
@Parameter(description = "string parameter")
9+
private String stringParam;
10+
11+
@Parameter(description = "int parameter")
12+
private int intParam;
13+
14+
public String getStringParam() {
15+
return stringParam;
16+
}
17+
18+
public void setStringParam(String stringParam) {
19+
this.stringParam = stringParam;
20+
}
21+
22+
public int getIntParam() {
23+
return intParam;
24+
}
25+
26+
public void setIntParam(int intParam) {
27+
this.intParam = intParam;
28+
}
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package test.org.springdoc.api.app102;
2+
3+
import org.springframework.boot.autoconfigure.SpringBootApplication;
4+
import test.org.springdoc.api.AbstractSpringDocTest;
5+
6+
public class SpringDocApp102Test extends AbstractSpringDocTest {
7+
@SpringBootApplication
8+
static class SpringDocTestApp {}
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package test.org.springdoc.api.app102;
2+
3+
import org.springdoc.api.annotations.ParameterObject;
4+
import org.springframework.lang.Nullable;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestParam;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
@RestController
10+
public class TestController {
11+
@GetMapping("test")
12+
public void getTest(@RequestParam @Nullable String param, @ParameterObject RequestParams requestParams) {
13+
}
14+
}

0 commit comments

Comments
 (0)