Skip to content

Commit cb3b772

Browse files
committed
Controller advice documents ApiResponse on every operation, even if the operation does not annotate the exception to be thrown. Fixes #2483
1 parent 534080f commit cb3b772

File tree

12 files changed

+227
-41
lines changed

12 files changed

+227
-41
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/ControllerAdviceInfo.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@
2323
*/
2424
package org.springdoc.core.models;
2525

26+
import java.util.ArrayList;
2627
import java.util.LinkedHashMap;
28+
import java.util.List;
2729
import java.util.Map;
2830

2931
import io.swagger.v3.oas.models.responses.ApiResponse;
3032

33+
import org.springframework.util.CollectionUtils;
34+
3135
/**
3236
* The type Controller advice info.
37+
*
3338
* @author bnasslahsen
3439
*/
3540
public class ControllerAdviceInfo {
@@ -40,9 +45,9 @@ public class ControllerAdviceInfo {
4045
private final Object controllerAdvice;
4146

4247
/**
43-
* The Api response map.
48+
* The Method advice infos.
4449
*/
45-
private final Map<String, ApiResponse> apiResponseMap = new LinkedHashMap<>();
50+
private List<MethodAdviceInfo> methodAdviceInfos = new ArrayList<>();
4651

4752
/**
4853
* Instantiates a new Controller advice info.
@@ -68,6 +73,19 @@ public Object getControllerAdvice() {
6873
* @return the api response map
6974
*/
7075
public Map<String, ApiResponse> getApiResponseMap() {
76+
Map<String, ApiResponse> apiResponseMap = new LinkedHashMap<>();
77+
for (MethodAdviceInfo methodAdviceInfo : methodAdviceInfos) {
78+
if (!CollectionUtils.isEmpty(methodAdviceInfo.getApiResponses()))
79+
apiResponseMap.putAll(methodAdviceInfo.getApiResponses());
80+
}
7181
return apiResponseMap;
7282
}
83+
84+
public List<MethodAdviceInfo> getMethodAdviceInfos() {
85+
return methodAdviceInfos;
86+
}
87+
88+
public void addMethodAdviceInfos(MethodAdviceInfo methodAdviceInfo) {
89+
this.methodAdviceInfos.add(methodAdviceInfo);
90+
}
7391
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2022 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
package org.springdoc.core.models;
25+
26+
import java.lang.reflect.Method;
27+
import java.util.Set;
28+
29+
import io.swagger.v3.oas.models.responses.ApiResponses;
30+
31+
/**
32+
* The type Method advice info.
33+
*
34+
* @author bnasslahsen
35+
*/
36+
public class MethodAdviceInfo {
37+
38+
/**
39+
* The Method.
40+
*/
41+
private final Method method;
42+
43+
/**
44+
* The Exceptions.
45+
*/
46+
private Set<Class<?>> exceptions;
47+
48+
/**
49+
* The Api responses.
50+
*/
51+
private ApiResponses apiResponses;
52+
53+
/**
54+
* Instantiates a new Method advice info.
55+
*
56+
* @param method the method
57+
*/
58+
public MethodAdviceInfo(Method method) {
59+
this.method = method;
60+
}
61+
62+
/**
63+
* Gets method.
64+
*
65+
* @return the method
66+
*/
67+
public Method getMethod() {
68+
return method;
69+
}
70+
71+
/**
72+
* Gets exceptions.
73+
*
74+
* @return the exceptions
75+
*/
76+
public Set<Class<?>> getExceptions() {
77+
return exceptions;
78+
}
79+
80+
/**
81+
* Sets exceptions.
82+
*
83+
* @param exceptions the exceptions
84+
*/
85+
public void setExceptions(Set<Class<?>> exceptions) {
86+
this.exceptions = exceptions;
87+
}
88+
89+
/**
90+
* Gets api responses.
91+
*
92+
* @return the api responses
93+
*/
94+
public ApiResponses getApiResponses() {
95+
return apiResponses;
96+
}
97+
98+
/**
99+
* Sets api responses.
100+
*
101+
* @param apiResponses the api responses
102+
*/
103+
public void setApiResponses(ApiResponses apiResponses) {
104+
this.apiResponses = apiResponses;
105+
}
106+
}

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.slf4j.Logger;
6262
import org.slf4j.LoggerFactory;
6363
import org.springdoc.core.models.ControllerAdviceInfo;
64+
import org.springdoc.core.models.MethodAdviceInfo;
6465
import org.springdoc.core.models.MethodAttributes;
6566
import org.springdoc.core.parsers.ReturnTypeParser;
6667
import org.springdoc.core.properties.SpringDocConfigProperties;
@@ -242,7 +243,7 @@ public static void setResponseEntityExceptionHandlerClass(Class<?> responseEntit
242243
*/
243244
public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
244245
MethodAttributes methodAttributes) {
245-
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod.getBeanType());
246+
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod);
246247
if (springDocConfigProperties.isOverrideWithGenericResponse()) {
247248
genericMapResponse = filterAndEnrichGenericMapResponseByDeclarations(handlerMethod, genericMapResponse);
248249
}
@@ -316,8 +317,13 @@ public void buildGenericResponse(Components components, Map<String, Object> find
316317
String[] methodProduces = { springDocConfigProperties.getDefaultProducesMediaType() };
317318
if (reqMappingMethod != null)
318319
methodProduces = reqMappingMethod.produces();
319-
Map<String, ApiResponse> controllerAdviceInfoApiResponseMap = controllerAdviceInfo.getApiResponseMap();
320320
MethodParameter methodParameter = new MethodParameter(method, -1);
321+
MethodAdviceInfo methodAdviceInfo = new MethodAdviceInfo(method);
322+
controllerAdviceInfo.addMethodAdviceInfos(methodAdviceInfo);
323+
// get exceptions lists
324+
Set<Class<?>> exceptions = getExceptionsFromExceptionHandler(methodParameter);
325+
methodAdviceInfo.setExceptions(exceptions);
326+
Map<String, ApiResponse> controllerAdviceInfoApiResponseMap = controllerAdviceInfo.getApiResponseMap();
321327
ApiResponses apiResponsesOp = new ApiResponses();
322328
MethodAttributes methodAttributes = new MethodAttributes(methodProduces, springDocConfigProperties.getDefaultConsumesMediaType(),
323329
springDocConfigProperties.getDefaultProducesMediaType(), controllerAdviceInfoApiResponseMap, locale);
@@ -328,9 +334,9 @@ public void buildGenericResponse(Components components, Map<String, Object> find
328334
JavadocProvider javadocProvider = operationService.getJavadocProvider();
329335
methodAttributes.setJavadocReturn(javadocProvider.getMethodJavadocReturn(methodParameter.getMethod()));
330336
}
331-
Map<String, ApiResponse> apiResponses = computeResponseFromDoc(components, methodParameter, apiResponsesOp, methodAttributes, springDocConfigProperties.isOpenapi31(), locale);
337+
computeResponseFromDoc(components, methodParameter, apiResponsesOp, methodAttributes, springDocConfigProperties.isOpenapi31(), locale);
332338
buildGenericApiResponses(components, methodParameter, apiResponsesOp, methodAttributes);
333-
apiResponses.forEach(controllerAdviceInfoApiResponseMap::put);
339+
methodAdviceInfo.setApiResponses(apiResponsesOp);
334340
}
335341
}
336342
if (AnnotatedElementUtils.hasAnnotation(objClz, ControllerAdvice.class)) {
@@ -382,7 +388,7 @@ private Map<String, ApiResponse> computeResponseFromDoc(Components components, M
382388
apiResponse.setDescription(propertyResolverUtils.resolve(apiResponseAnnotations.description(), methodAttributes.getLocale()));
383389
buildContentFromDoc(components, apiResponsesOp, methodAttributes, apiResponseAnnotations, apiResponse, openapi31);
384390
Map<String, Object> extensions = AnnotationsUtils.getExtensions(propertyResolverUtils.isOpenapi31(), apiResponseAnnotations.extensions());
385-
if (!CollectionUtils.isEmpty(extensions)){
391+
if (!CollectionUtils.isEmpty(extensions)) {
386392
if (propertyResolverUtils.isResolveExtensionsProperties()) {
387393
Map<String, Object> extensionsResolved = propertyResolverUtils.resolveExtensions(locale, extensions);
388394
extensionsResolved.forEach(apiResponse::addExtension);
@@ -627,18 +633,7 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent()))
627633
&& methodParameter.getExecutable().isAnnotationPresent(ExceptionHandler.class)) {
628634
// ExceptionHandler's exception class resolution is non-trivial
629635
// more info on its javadoc
630-
ExceptionHandler exceptionHandler = methodParameter.getExecutable().getAnnotation(ExceptionHandler.class);
631-
Set<Class<?>> exceptions = new HashSet<>();
632-
if (exceptionHandler.value().length == 0) {
633-
for (Parameter parameter : methodParameter.getExecutable().getParameters()) {
634-
if (Throwable.class.isAssignableFrom(parameter.getType())) {
635-
exceptions.add(parameter.getType());
636-
}
637-
}
638-
}
639-
else {
640-
exceptions.addAll(asList(exceptionHandler.value()));
641-
}
636+
Set<Class<?>> exceptions = getExceptionsFromExceptionHandler(methodParameter);
642637
apiResponse.addExtension(EXTENSION_EXCEPTION_CLASSES, exceptions);
643638
}
644639
apiResponsesOp.addApiResponse(httpCode, apiResponse);
@@ -685,20 +680,21 @@ else if (returnType instanceof ParameterizedType) {
685680
/**
686681
* Gets generic map response.
687682
*
688-
* @param beanType the bean type
683+
* @param handlerMethod the handler method
689684
* @return the generic map response
690685
*/
691-
private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
686+
private Map<String, ApiResponse> getGenericMapResponse(HandlerMethod handlerMethod) {
692687
reentrantLock.lock();
693688
try {
689+
Class<?> beanType = handlerMethod.getBeanType();
694690
List<ControllerAdviceInfo> controllerAdviceInfosInThisBean = localExceptionHandlers.stream()
695691
.filter(controllerInfo -> {
696692
Class<?> objClz = controllerInfo.getControllerAdvice().getClass();
697693
if (org.springframework.aop.support.AopUtils.isAopProxy(controllerInfo.getControllerAdvice()))
698694
objClz = org.springframework.aop.support.AopUtils.getTargetClass(controllerInfo.getControllerAdvice());
699695
return beanType.equals(objClz);
700696
})
701-
.collect(Collectors.toList());
697+
.toList();
702698

703699
Map<String, ApiResponse> genericApiResponseMap = controllerAdviceInfosInThisBean.stream()
704700
.map(ControllerAdviceInfo::getApiResponseMap)
@@ -710,11 +706,32 @@ private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
710706
.filter(controllerAdviceInfo -> !beanType.equals(controllerAdviceInfo.getControllerAdvice().getClass()))
711707
.toList();
712708

709+
Class<?>[] methodExceptions = handlerMethod.getMethod().getExceptionTypes();
710+
713711
for (ControllerAdviceInfo controllerAdviceInfo : controllerAdviceInfosNotInThisBean) {
714-
controllerAdviceInfo.getApiResponseMap().forEach((key, apiResponse) -> {
715-
if (!genericApiResponseMap.containsKey(key))
716-
genericApiResponseMap.put(key, apiResponse);
717-
});
712+
List<MethodAdviceInfo> methodAdviceInfos = controllerAdviceInfo.getMethodAdviceInfos();
713+
for (MethodAdviceInfo methodAdviceInfo : methodAdviceInfos) {
714+
Set<Class<?>> exceptions = methodAdviceInfo.getExceptions();
715+
boolean addToGenericMap = false;
716+
717+
for (Class<?> exception : exceptions) {
718+
if (isGlobalException(exception) ||
719+
Arrays.stream(methodExceptions).anyMatch(methodException ->
720+
methodException.isAssignableFrom(exception) ||
721+
exception.isAssignableFrom(methodException))) {
722+
723+
addToGenericMap = true;
724+
break;
725+
}
726+
}
727+
728+
if (addToGenericMap || exceptions.isEmpty()) {
729+
methodAdviceInfo.getApiResponses().forEach((key, apiResponse) -> {
730+
if (!genericApiResponseMap.containsKey(key))
731+
genericApiResponseMap.put(key, apiResponse);
732+
});
733+
}
734+
}
718735
}
719736

720737
LinkedHashMap<String, ApiResponse> genericApiResponsesClone;
@@ -732,7 +749,7 @@ private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
732749
reentrantLock.unlock();
733750
}
734751
}
735-
752+
736753
/**
737754
* Is valid http code boolean.
738755
*
@@ -773,4 +790,40 @@ private boolean isHttpCodePresent(String httpCode, Set<io.swagger.v3.oas.annotat
773790
return !responseSet.isEmpty() && responseSet.stream().anyMatch(apiResponseAnnotations -> httpCode.equals(apiResponseAnnotations.responseCode()));
774791
}
775792

793+
/**
794+
* Gets exceptions from exception handler.
795+
*
796+
* @param methodParameter the method parameter
797+
* @return the exceptions from exception handler
798+
*/
799+
private Set<Class<?>> getExceptionsFromExceptionHandler(MethodParameter methodParameter) {
800+
ExceptionHandler exceptionHandler = methodParameter.getExecutable().getAnnotation(ExceptionHandler.class);
801+
Set<Class<?>> exceptions = new HashSet<>();
802+
if (exceptionHandler != null) {
803+
if (exceptionHandler.value().length == 0) {
804+
for (Parameter parameter : methodParameter.getExecutable().getParameters()) {
805+
if (Throwable.class.isAssignableFrom(parameter.getType())) {
806+
exceptions.add(parameter.getType());
807+
}
808+
}
809+
}
810+
else {
811+
exceptions.addAll(asList(exceptionHandler.value()));
812+
}
813+
}
814+
return exceptions;
815+
}
816+
817+
818+
/**
819+
* Is unchecked exception boolean.
820+
*
821+
* @param exceptionClass the exception class
822+
* @return the boolean
823+
*/
824+
private boolean isGlobalException(Class<?> exceptionClass) {
825+
return RuntimeException.class.isAssignableFrom(exceptionClass)
826+
|| exceptionClass.isAssignableFrom(Exception.class)
827+
|| Error.class.isAssignableFrom(exceptionClass);
828+
}
776829
}

springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app110/PersonController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import jakarta.validation.constraints.Size;
3535

3636
import org.springframework.validation.annotation.Validated;
37+
import org.springframework.web.HttpMediaTypeNotSupportedException;
3738
import org.springframework.web.bind.annotation.RequestBody;
3839
import org.springframework.web.bind.annotation.RequestMapping;
3940
import org.springframework.web.bind.annotation.RequestMethod;
@@ -46,7 +47,7 @@ public class PersonController {
4647
private final Random ran = new Random();
4748

4849
@RequestMapping(path = "/person", method = RequestMethod.POST)
49-
public Person person(@Valid @RequestBody Person person) {
50+
public Person person(@Valid @RequestBody Person person) throws HttpMediaTypeNotSupportedException {
5051

5152
int nxt = ran.nextInt(10);
5253
if (nxt >= 5) {
@@ -58,7 +59,7 @@ public Person person(@Valid @RequestBody Person person) {
5859
@RequestMapping(path = "/personByLastName", method = RequestMethod.GET)
5960
public List<Person> findByLastName(@RequestParam(name = "lastName", required = true) @NotNull
6061
@NotBlank
61-
@Size(max = 10) String lastName) {
62+
@Size(max = 10) String lastName) throws HttpMediaTypeNotSupportedException {
6263
List<Person> hardCoded = new ArrayList<>();
6364
Person person = new Person();
6465
person.setAge(20);

springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app112/sample/PersonController2.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import test.org.springdoc.api.v30.app112.Person;
3636

3737
import org.springframework.validation.annotation.Validated;
38+
import org.springframework.web.HttpMediaTypeNotSupportedException;
3839
import org.springframework.web.bind.annotation.RequestBody;
3940
import org.springframework.web.bind.annotation.RequestMapping;
4041
import org.springframework.web.bind.annotation.RequestMethod;
@@ -47,7 +48,7 @@ public class PersonController2 {
4748
private final Random ran = new Random();
4849

4950
@RequestMapping(path = "/person2", method = RequestMethod.POST)
50-
public Person person(@Valid @RequestBody Person person) {
51+
public Person person(@Valid @RequestBody Person person) throws HttpMediaTypeNotSupportedException {
5152

5253
int nxt = ran.nextInt(10);
5354
if (nxt >= 5) {
@@ -59,7 +60,7 @@ public Person person(@Valid @RequestBody Person person) {
5960
@RequestMapping(path = "/personByLastName2", method = RequestMethod.GET)
6061
public List<Person> findByLastName(@RequestParam(name = "lastName", required = true) @NotNull
6162
@NotBlank
62-
@Size(max = 10) String lastName) {
63+
@Size(max = 10) String lastName) throws HttpMediaTypeNotSupportedException {
6364
List<Person> hardCoded = new ArrayList<>();
6465
Person person = new Person();
6566
person.setAge(20);

0 commit comments

Comments
 (0)