Skip to content

Commit

Permalink
Controller advice documents ApiResponse on every operation, even if t…
Browse files Browse the repository at this point in the history
…he operation does not annotate the exception to be thrown. Fixes #2483
  • Loading branch information
bnasslahsen committed Mar 11, 2024
1 parent 534080f commit cb3b772
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@
*/
package org.springdoc.core.models;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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

import org.springframework.util.CollectionUtils;

/**
* The type Controller advice info.
*
* @author bnasslahsen
*/
public class ControllerAdviceInfo {
Expand All @@ -40,9 +45,9 @@ public class ControllerAdviceInfo {
private final Object controllerAdvice;

/**
* The Api response map.
* The Method advice infos.
*/
private final Map<String, ApiResponse> apiResponseMap = new LinkedHashMap<>();
private List<MethodAdviceInfo> methodAdviceInfos = new ArrayList<>();

/**
* Instantiates a new Controller advice info.
Expand All @@ -68,6 +73,19 @@ public Object getControllerAdvice() {
* @return the api response map
*/
public Map<String, ApiResponse> getApiResponseMap() {
Map<String, ApiResponse> apiResponseMap = new LinkedHashMap<>();
for (MethodAdviceInfo methodAdviceInfo : methodAdviceInfos) {
if (!CollectionUtils.isEmpty(methodAdviceInfo.getApiResponses()))
apiResponseMap.putAll(methodAdviceInfo.getApiResponses());
}
return apiResponseMap;
}

public List<MethodAdviceInfo> getMethodAdviceInfos() {
return methodAdviceInfos;
}

public void addMethodAdviceInfos(MethodAdviceInfo methodAdviceInfo) {
this.methodAdviceInfos.add(methodAdviceInfo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
*
* *
* * *
* * * *
* * * * * Copyright 2019-2022 the original author or authors.
* * * * *
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
* * * * * you may not use this file except in compliance with the License.
* * * * * You may obtain a copy of the License at
* * * * *
* * * * * https://www.apache.org/licenses/LICENSE-2.0
* * * * *
* * * * * Unless required by applicable law or agreed to in writing, software
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * * * * See the License for the specific language governing permissions and
* * * * * limitations under the License.
* * * *
* * *
* *
*
*/
package org.springdoc.core.models;

import java.lang.reflect.Method;
import java.util.Set;

import io.swagger.v3.oas.models.responses.ApiResponses;

/**
* The type Method advice info.
*
* @author bnasslahsen
*/
public class MethodAdviceInfo {

/**
* The Method.
*/
private final Method method;

/**
* The Exceptions.
*/
private Set<Class<?>> exceptions;

/**
* The Api responses.
*/
private ApiResponses apiResponses;

/**
* Instantiates a new Method advice info.
*
* @param method the method
*/
public MethodAdviceInfo(Method method) {
this.method = method;
}

/**
* Gets method.
*
* @return the method
*/
public Method getMethod() {
return method;
}

/**
* Gets exceptions.
*
* @return the exceptions
*/
public Set<Class<?>> getExceptions() {
return exceptions;
}

/**
* Sets exceptions.
*
* @param exceptions the exceptions
*/
public void setExceptions(Set<Class<?>> exceptions) {
this.exceptions = exceptions;
}

/**
* Gets api responses.
*
* @return the api responses
*/
public ApiResponses getApiResponses() {
return apiResponses;
}

/**
* Sets api responses.
*
* @param apiResponses the api responses
*/
public void setApiResponses(ApiResponses apiResponses) {
this.apiResponses = apiResponses;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.core.models.ControllerAdviceInfo;
import org.springdoc.core.models.MethodAdviceInfo;
import org.springdoc.core.models.MethodAttributes;
import org.springdoc.core.parsers.ReturnTypeParser;
import org.springdoc.core.properties.SpringDocConfigProperties;
Expand Down Expand Up @@ -242,7 +243,7 @@ public static void setResponseEntityExceptionHandlerClass(Class<?> responseEntit
*/
public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
MethodAttributes methodAttributes) {
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod.getBeanType());
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod);
if (springDocConfigProperties.isOverrideWithGenericResponse()) {
genericMapResponse = filterAndEnrichGenericMapResponseByDeclarations(handlerMethod, genericMapResponse);
}
Expand Down Expand Up @@ -316,8 +317,13 @@ public void buildGenericResponse(Components components, Map<String, Object> find
String[] methodProduces = { springDocConfigProperties.getDefaultProducesMediaType() };
if (reqMappingMethod != null)
methodProduces = reqMappingMethod.produces();
Map<String, ApiResponse> controllerAdviceInfoApiResponseMap = controllerAdviceInfo.getApiResponseMap();
MethodParameter methodParameter = new MethodParameter(method, -1);
MethodAdviceInfo methodAdviceInfo = new MethodAdviceInfo(method);
controllerAdviceInfo.addMethodAdviceInfos(methodAdviceInfo);
// get exceptions lists
Set<Class<?>> exceptions = getExceptionsFromExceptionHandler(methodParameter);
methodAdviceInfo.setExceptions(exceptions);
Map<String, ApiResponse> controllerAdviceInfoApiResponseMap = controllerAdviceInfo.getApiResponseMap();
ApiResponses apiResponsesOp = new ApiResponses();
MethodAttributes methodAttributes = new MethodAttributes(methodProduces, springDocConfigProperties.getDefaultConsumesMediaType(),
springDocConfigProperties.getDefaultProducesMediaType(), controllerAdviceInfoApiResponseMap, locale);
Expand All @@ -328,9 +334,9 @@ public void buildGenericResponse(Components components, Map<String, Object> find
JavadocProvider javadocProvider = operationService.getJavadocProvider();
methodAttributes.setJavadocReturn(javadocProvider.getMethodJavadocReturn(methodParameter.getMethod()));
}
Map<String, ApiResponse> apiResponses = computeResponseFromDoc(components, methodParameter, apiResponsesOp, methodAttributes, springDocConfigProperties.isOpenapi31(), locale);
computeResponseFromDoc(components, methodParameter, apiResponsesOp, methodAttributes, springDocConfigProperties.isOpenapi31(), locale);
buildGenericApiResponses(components, methodParameter, apiResponsesOp, methodAttributes);
apiResponses.forEach(controllerAdviceInfoApiResponseMap::put);
methodAdviceInfo.setApiResponses(apiResponsesOp);
}
}
if (AnnotatedElementUtils.hasAnnotation(objClz, ControllerAdvice.class)) {
Expand Down Expand Up @@ -382,7 +388,7 @@ private Map<String, ApiResponse> computeResponseFromDoc(Components components, M
apiResponse.setDescription(propertyResolverUtils.resolve(apiResponseAnnotations.description(), methodAttributes.getLocale()));
buildContentFromDoc(components, apiResponsesOp, methodAttributes, apiResponseAnnotations, apiResponse, openapi31);
Map<String, Object> extensions = AnnotationsUtils.getExtensions(propertyResolverUtils.isOpenapi31(), apiResponseAnnotations.extensions());
if (!CollectionUtils.isEmpty(extensions)){
if (!CollectionUtils.isEmpty(extensions)) {
if (propertyResolverUtils.isResolveExtensionsProperties()) {
Map<String, Object> extensionsResolved = propertyResolverUtils.resolveExtensions(locale, extensions);
extensionsResolved.forEach(apiResponse::addExtension);
Expand Down Expand Up @@ -627,18 +633,7 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent()))
&& methodParameter.getExecutable().isAnnotationPresent(ExceptionHandler.class)) {
// ExceptionHandler's exception class resolution is non-trivial
// more info on its javadoc
ExceptionHandler exceptionHandler = methodParameter.getExecutable().getAnnotation(ExceptionHandler.class);
Set<Class<?>> exceptions = new HashSet<>();
if (exceptionHandler.value().length == 0) {
for (Parameter parameter : methodParameter.getExecutable().getParameters()) {
if (Throwable.class.isAssignableFrom(parameter.getType())) {
exceptions.add(parameter.getType());
}
}
}
else {
exceptions.addAll(asList(exceptionHandler.value()));
}
Set<Class<?>> exceptions = getExceptionsFromExceptionHandler(methodParameter);
apiResponse.addExtension(EXTENSION_EXCEPTION_CLASSES, exceptions);
}
apiResponsesOp.addApiResponse(httpCode, apiResponse);
Expand Down Expand Up @@ -685,20 +680,21 @@ else if (returnType instanceof ParameterizedType) {
/**
* Gets generic map response.
*
* @param beanType the bean type
* @param handlerMethod the handler method
* @return the generic map response
*/
private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
private Map<String, ApiResponse> getGenericMapResponse(HandlerMethod handlerMethod) {
reentrantLock.lock();
try {
Class<?> beanType = handlerMethod.getBeanType();
List<ControllerAdviceInfo> controllerAdviceInfosInThisBean = localExceptionHandlers.stream()
.filter(controllerInfo -> {
Class<?> objClz = controllerInfo.getControllerAdvice().getClass();
if (org.springframework.aop.support.AopUtils.isAopProxy(controllerInfo.getControllerAdvice()))
objClz = org.springframework.aop.support.AopUtils.getTargetClass(controllerInfo.getControllerAdvice());
return beanType.equals(objClz);
})
.collect(Collectors.toList());
.toList();

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

Class<?>[] methodExceptions = handlerMethod.getMethod().getExceptionTypes();

for (ControllerAdviceInfo controllerAdviceInfo : controllerAdviceInfosNotInThisBean) {
controllerAdviceInfo.getApiResponseMap().forEach((key, apiResponse) -> {
if (!genericApiResponseMap.containsKey(key))
genericApiResponseMap.put(key, apiResponse);
});
List<MethodAdviceInfo> methodAdviceInfos = controllerAdviceInfo.getMethodAdviceInfos();
for (MethodAdviceInfo methodAdviceInfo : methodAdviceInfos) {
Set<Class<?>> exceptions = methodAdviceInfo.getExceptions();
boolean addToGenericMap = false;

for (Class<?> exception : exceptions) {
if (isGlobalException(exception) ||
Arrays.stream(methodExceptions).anyMatch(methodException ->
methodException.isAssignableFrom(exception) ||
exception.isAssignableFrom(methodException))) {

addToGenericMap = true;
break;
}
}

if (addToGenericMap || exceptions.isEmpty()) {
methodAdviceInfo.getApiResponses().forEach((key, apiResponse) -> {
if (!genericApiResponseMap.containsKey(key))
genericApiResponseMap.put(key, apiResponse);
});
}
}
}

LinkedHashMap<String, ApiResponse> genericApiResponsesClone;
Expand All @@ -732,7 +749,7 @@ private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
reentrantLock.unlock();
}
}

/**
* Is valid http code boolean.
*
Expand Down Expand Up @@ -773,4 +790,40 @@ private boolean isHttpCodePresent(String httpCode, Set<io.swagger.v3.oas.annotat
return !responseSet.isEmpty() && responseSet.stream().anyMatch(apiResponseAnnotations -> httpCode.equals(apiResponseAnnotations.responseCode()));
}

/**
* Gets exceptions from exception handler.
*
* @param methodParameter the method parameter
* @return the exceptions from exception handler
*/
private Set<Class<?>> getExceptionsFromExceptionHandler(MethodParameter methodParameter) {
ExceptionHandler exceptionHandler = methodParameter.getExecutable().getAnnotation(ExceptionHandler.class);
Set<Class<?>> exceptions = new HashSet<>();
if (exceptionHandler != null) {
if (exceptionHandler.value().length == 0) {
for (Parameter parameter : methodParameter.getExecutable().getParameters()) {
if (Throwable.class.isAssignableFrom(parameter.getType())) {
exceptions.add(parameter.getType());
}
}
}
else {
exceptions.addAll(asList(exceptionHandler.value()));
}
}
return exceptions;
}


/**
* Is unchecked exception boolean.
*
* @param exceptionClass the exception class
* @return the boolean
*/
private boolean isGlobalException(Class<?> exceptionClass) {
return RuntimeException.class.isAssignableFrom(exceptionClass)
|| exceptionClass.isAssignableFrom(Exception.class)
|| Error.class.isAssignableFrom(exceptionClass);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import jakarta.validation.constraints.Size;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
Expand All @@ -46,7 +47,7 @@ public class PersonController {
private final Random ran = new Random();

@RequestMapping(path = "/person", method = RequestMethod.POST)
public Person person(@Valid @RequestBody Person person) {
public Person person(@Valid @RequestBody Person person) throws HttpMediaTypeNotSupportedException {

int nxt = ran.nextInt(10);
if (nxt >= 5) {
Expand All @@ -58,7 +59,7 @@ public Person person(@Valid @RequestBody Person person) {
@RequestMapping(path = "/personByLastName", method = RequestMethod.GET)
public List<Person> findByLastName(@RequestParam(name = "lastName", required = true) @NotNull
@NotBlank
@Size(max = 10) String lastName) {
@Size(max = 10) String lastName) throws HttpMediaTypeNotSupportedException {
List<Person> hardCoded = new ArrayList<>();
Person person = new Person();
person.setAge(20);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import test.org.springdoc.api.v30.app112.Person;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
Expand All @@ -47,7 +48,7 @@ public class PersonController2 {
private final Random ran = new Random();

@RequestMapping(path = "/person2", method = RequestMethod.POST)
public Person person(@Valid @RequestBody Person person) {
public Person person(@Valid @RequestBody Person person) throws HttpMediaTypeNotSupportedException {

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

0 comments on commit cb3b772

Please sign in to comment.