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

org.springdoc.core.GenericResponseService Maven / Gradle / Ivy

The newest version!
/*
 *
 *  *
 *  *  *
 *  *  *  *
 *  *  *  *  * Copyright 2019-2023 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;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.core.providers.JavadocProvider;

import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.ControllerAdviceBean;
import org.springframework.web.method.HandlerMethod;

import static java.util.Arrays.asList;
import static org.springdoc.core.Constants.DEFAULT_DESCRIPTION;
import static org.springdoc.core.SpringDocAnnotationsUtils.extractSchema;
import static org.springdoc.core.SpringDocAnnotationsUtils.getContent;
import static org.springdoc.core.SpringDocAnnotationsUtils.mergeSchema;
import static org.springdoc.core.converters.ConverterUtils.isResponseTypeWrapper;

/**
 * The type Generic response builder.
 *
 * @author bnasslahsen
 */
public class GenericResponseService {

	/**
	 * This extension name is used to temporary store
	 * the exception classes.
	 */
	private static final String EXTENSION_EXCEPTION_CLASSES = "x-exception-class";

	/**
	 * The Response entity exception handler class.
	 */
	private static Class responseEntityExceptionHandlerClass;

	/**
	 * The Operation builder.
	 */
	private final OperationService operationService;

	/**
	 * The Return type parsers.
	 */
	private final List returnTypeParsers;

	/**
	 * The Spring doc config properties.
	 */
	private final SpringDocConfigProperties springDocConfigProperties;

	/**
	 * The Property resolver utils.
	 */
	private final PropertyResolverUtils propertyResolverUtils;

	/**
	 * The Controller advice infos.
	 */
	private final List controllerAdviceInfos = new CopyOnWriteArrayList<>();

	/**
	 * The Controller infos.
	 */
	private final List localExceptionHandlers = new CopyOnWriteArrayList<>();

	/**
	 * The Reentrant lock.
	 */
	private final Lock reentrantLock = new ReentrantLock();

	/**
	 * The constant LOGGER.
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(GenericResponseService.class);

	/**
	 * Instantiates a new Generic response builder.
	 *
	 * @param operationService          the operation builder
	 * @param returnTypeParsers         the return type parsers
	 * @param springDocConfigProperties the spring doc config properties
	 * @param propertyResolverUtils     the property resolver utils
	 */
	public GenericResponseService(OperationService operationService, List returnTypeParsers,
			SpringDocConfigProperties springDocConfigProperties,
			PropertyResolverUtils propertyResolverUtils) {
		super();
		this.operationService = operationService;
		this.returnTypeParsers = returnTypeParsers;
		this.springDocConfigProperties = springDocConfigProperties;
		this.propertyResolverUtils = propertyResolverUtils;
	}

	/**
	 * Build content from doc.
	 *
	 * @param components             the components
	 * @param apiResponsesOp         the api responses op
	 * @param methodAttributes       the method attributes
	 * @param apiResponseAnnotations the api response annotations
	 * @param apiResponse            the api response
	 * @param openapi31              the openapi 31
	 */
	public static void buildContentFromDoc(Components components, ApiResponses apiResponsesOp,
			MethodAttributes methodAttributes,
			io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations,
			ApiResponse apiResponse, boolean openapi31) {

		io.swagger.v3.oas.annotations.media.Content[] contentdoc = apiResponseAnnotations.content();
		Optional optionalContent = getContent(contentdoc, new String[0],
				methodAttributes.getMethodProduces(), null, components, methodAttributes.getJsonViewAnnotation(), openapi31);
		if (apiResponsesOp.containsKey(apiResponseAnnotations.responseCode())) {
			// Merge with the existing content
			Content existingContent = apiResponsesOp.get(apiResponseAnnotations.responseCode()).getContent();
			if (optionalContent.isPresent()) {
				Content newContent = optionalContent.get();
				if (methodAttributes.isMethodOverloaded() && existingContent != null) {
					Arrays.stream(methodAttributes.getMethodProduces()).filter(mediaTypeStr -> (newContent.get(mediaTypeStr) != null)).forEach(mediaTypeStr -> {
						if (newContent.get(mediaTypeStr).getSchema() != null)
							mergeSchema(existingContent, newContent.get(mediaTypeStr).getSchema(), mediaTypeStr);
					});
					apiResponse.content(existingContent);
				}
				else
					apiResponse.content(newContent);
			}
			else {
				apiResponse.content(existingContent);
			}
		}
		else {
			optionalContent.ifPresent(apiResponse::content);
		}
	}

	/**
	 * Sets description.
	 *
	 * @param httpCode    the http code
	 * @param apiResponse the api response
	 */
	public static void setDescription(String httpCode, ApiResponse apiResponse) {
		try {
			HttpStatus httpStatus = HttpStatus.valueOf(Integer.parseInt(httpCode));
			apiResponse.setDescription(httpStatus.getReasonPhrase());
		}
		catch (IllegalArgumentException e) {
			apiResponse.setDescription(DEFAULT_DESCRIPTION);
		}
	}

	/**
	 * Sets response entity exception handler class.
	 *
	 * @param responseEntityExceptionHandlerClass the response entity exception handler class
	 */
	public static void setResponseEntityExceptionHandlerClass(Class responseEntityExceptionHandlerClass) {
		GenericResponseService.responseEntityExceptionHandlerClass = responseEntityExceptionHandlerClass;
	}

	/**
	 * Build api responses.
	 *
	 * @param components       the components
	 * @param handlerMethod    the handler method
	 * @param operation        the operation
	 * @param methodAttributes the method attributes
	 * @return the api responses
	 */
	public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
			MethodAttributes methodAttributes) {
		Map genericMapResponse = getGenericMapResponse(handlerMethod);
		if (springDocConfigProperties.isOverrideWithGenericResponse()) {
			genericMapResponse = filterAndEnrichGenericMapResponseByDeclarations(handlerMethod, genericMapResponse);
		}
		ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(genericMapResponse);
		//Then use the apiResponses from documentation
		ApiResponses apiResponsesFromDoc = operation.getResponses();
		if (!CollectionUtils.isEmpty(apiResponsesFromDoc))
			apiResponsesFromDoc.forEach(apiResponses::addApiResponse);
		// for each one build ApiResponse and add it to existing responses
		// Fill api Responses
		computeResponseFromDoc(components, handlerMethod.getReturnType(), apiResponses, methodAttributes, springDocConfigProperties.isOpenapi31());
		buildApiResponses(components, handlerMethod.getReturnType(), apiResponses, methodAttributes);
		return apiResponses;
	}

	/**
	 * Filters the generic API responses by the declared exceptions.
	 * If Javadoc comment found for the declaration than it overrides the default description.
	 *
	 * @param handlerMethod      the method which can have exception declarations
	 * @param genericMapResponse the default generic API responses
	 * @return the filtered and enriched responses
	 */
	private Map filterAndEnrichGenericMapResponseByDeclarations(HandlerMethod handlerMethod, Map genericMapResponse) {
		if (operationService.getJavadocProvider() != null) {
			JavadocProvider javadocProvider = operationService.getJavadocProvider();
			for (Map.Entry genericResponse : genericMapResponse.entrySet()) {
				Map extensions = genericResponse.getValue().getExtensions();
				Collection genericExceptions = (Collection) extensions.get(EXTENSION_EXCEPTION_CLASSES);
				for (Class declaredException : handlerMethod.getMethod().getExceptionTypes()) {
					if (genericExceptions.contains(declaredException.getName())) {
						Map javadocThrows = javadocProvider.getMethodJavadocThrows(handlerMethod.getMethod());
						String description = javadocThrows.get(declaredException.getName());
						if (description == null)
							description = javadocThrows.get(declaredException.getSimpleName());
						if (description != null && !description.trim().isEmpty()) {
							genericResponse.getValue().setDescription(description);
						}
					}
				}
			}
		}
		return genericMapResponse;
	}

	/**
	 * Build generic response.
	 *
	 * @param components           the components
	 * @param findControllerAdvice the find controller advice
	 * @param locale               the locale
	 */
	public void buildGenericResponse(Components components, Map findControllerAdvice, Locale locale) {
		// ControllerAdvice
		for (Map.Entry entry : findControllerAdvice.entrySet()) {
			List methods = new ArrayList<>();
			Object controllerAdvice = entry.getValue();
			// get all methods with annotation @ExceptionHandler
			Class objClz = controllerAdvice.getClass();
			if (org.springframework.aop.support.AopUtils.isAopProxy(controllerAdvice))
				objClz = org.springframework.aop.support.AopUtils.getTargetClass(controllerAdvice);
			ControllerAdviceInfo controllerAdviceInfo = new ControllerAdviceInfo(controllerAdvice);
			Arrays.stream(ReflectionUtils.getAllDeclaredMethods(objClz))
					.filter(m -> m.isAnnotationPresent(ExceptionHandler.class)
							|| isResponseEntityExceptionHandlerMethod(m)
					).forEach(methods::add);
			// for each one build ApiResponse and add it to existing responses
			for (Method method : methods) {
				if (!operationService.isHidden(method)) {
					RequestMapping reqMappingMethod = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
					String[] methodProduces = { springDocConfigProperties.getDefaultProducesMediaType() };
					if (reqMappingMethod != null)
						methodProduces = reqMappingMethod.produces();
					MethodParameter methodParameter = new MethodParameter(method, -1);
					MethodAdviceInfo methodAdviceInfo = new MethodAdviceInfo(method);
					controllerAdviceInfo.addMethodAdviceInfos(methodAdviceInfo);
					// get exceptions lists
					Set> exceptions = getExceptionsFromExceptionHandler(methodParameter);
					methodAdviceInfo.setExceptions(exceptions);
					Map controllerAdviceInfoApiResponseMap = controllerAdviceInfo.getApiResponseMap();
					ApiResponses apiResponsesOp = new ApiResponses();
					MethodAttributes methodAttributes = new MethodAttributes(methodProduces, springDocConfigProperties.getDefaultConsumesMediaType(),
							springDocConfigProperties.getDefaultProducesMediaType(), controllerAdviceInfoApiResponseMap, locale);
					//calculate JsonView Annotation
					methodAttributes.setJsonViewAnnotation(AnnotatedElementUtils.findMergedAnnotation(method, JsonView.class));
					//use the javadoc return if present
					if (operationService.getJavadocProvider() != null) {
						JavadocProvider javadocProvider = operationService.getJavadocProvider();
						methodAttributes.setJavadocReturn(javadocProvider.getMethodJavadocReturn(methodParameter.getMethod()));
					}
					computeResponseFromDoc(components, methodParameter, apiResponsesOp, methodAttributes, springDocConfigProperties.isOpenapi31());
					buildGenericApiResponses(components, methodParameter, apiResponsesOp, methodAttributes);
					methodAdviceInfo.setApiResponses(apiResponsesOp);
				}
			}
			if (AnnotatedElementUtils.hasAnnotation(objClz, ControllerAdvice.class)) {
				controllerAdviceInfos.add(controllerAdviceInfo);
			}
			else {
				localExceptionHandlers.add(controllerAdviceInfo);
			}
		}
	}

	/**
	 * Is response entity exception handler method boolean.
	 *
	 * @param m the m
	 * @return the boolean
	 */
	private boolean isResponseEntityExceptionHandlerMethod(Method m) {
		if (AnnotatedElementUtils.hasAnnotation(m.getDeclaringClass(), ControllerAdvice.class))
			return responseEntityExceptionHandlerClass != null && (responseEntityExceptionHandlerClass.isAssignableFrom(m.getDeclaringClass()) && ReflectionUtils.findMethod(responseEntityExceptionHandlerClass, m.getName(), m.getParameterTypes()) != null);
		return false;
	}

	/**
	 * Compute response from doc map.
	 *
	 * @param components       the components
	 * @param methodParameter  the method parameter
	 * @param apiResponsesOp   the api responses op
	 * @param methodAttributes the method attributes
	 * @param openapi31        the openapi 31
	 * @return the map
	 */
	private Map computeResponseFromDoc(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
			MethodAttributes methodAttributes, boolean openapi31) {
		// Parsing documentation, if present
		Set responsesArray = getApiResponses(Objects.requireNonNull(methodParameter.getMethod()));
		if (!responsesArray.isEmpty()) {
			methodAttributes.setWithApiResponseDoc(true);
			for (io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations : responsesArray) {
				String httpCode = apiResponseAnnotations.responseCode();
				ApiResponse apiResponse = new ApiResponse();
				if (StringUtils.isNotBlank(apiResponseAnnotations.ref())) {
					apiResponse.$ref(apiResponseAnnotations.ref());
					apiResponsesOp.addApiResponse(apiResponseAnnotations.responseCode(), apiResponse);
					continue;
				}
				apiResponse.setDescription(propertyResolverUtils.resolve(apiResponseAnnotations.description(), methodAttributes.getLocale()));
				buildContentFromDoc(components, apiResponsesOp, methodAttributes, apiResponseAnnotations, apiResponse, openapi31);
				Map extensions = AnnotationsUtils.getExtensions(propertyResolverUtils.isOpenapi31(), apiResponseAnnotations.extensions());
				if (!CollectionUtils.isEmpty(extensions)){
					if (propertyResolverUtils.isResolveExtensionsProperties()) {
						Map extensionsResolved = propertyResolverUtils.resolveExtensions(methodAttributes.getLocale(), extensions);
						extensionsResolved.forEach(apiResponse::addExtension);
					}
					else {
						apiResponse.extensions(extensions);
					}
				}
				AnnotationsUtils.getHeaders(apiResponseAnnotations.headers(), methodAttributes.getJsonViewAnnotation(), openapi31)
						.ifPresent(apiResponse::headers);
				apiResponsesOp.addApiResponse(httpCode, apiResponse);
			}
		}
		return apiResponsesOp;
	}

	/**
	 * Build generic api responses.
	 *
	 * @param components       the components
	 * @param methodParameter  the method parameter
	 * @param apiResponsesOp   the api responses op
	 * @param methodAttributes the method attributes
	 */
	private void buildGenericApiResponses(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
			MethodAttributes methodAttributes) {
		if (!CollectionUtils.isEmpty(apiResponsesOp)) {
			// API Responses at operation and @ApiResponse annotation
			for (Map.Entry entry : apiResponsesOp.entrySet()) {
				String httpCode = entry.getKey();
				ApiResponse apiResponse = entry.getValue();
				buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, apiResponse, true);
			}
		}
		else {
			// Use response parameters with no description filled - No documentation
			// available
			String httpCode = evaluateResponseStatus(methodParameter.getMethod(), Objects.requireNonNull(methodParameter.getMethod()).getClass(), true);
			if (Objects.nonNull(httpCode)) {
				ApiResponse apiResponse = methodAttributes.getGenericMapResponse().containsKey(httpCode) ? methodAttributes.getGenericMapResponse().get(httpCode)
						: new ApiResponse();
				buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, apiResponse, true);
			}
		}
	}

	/**
	 * Build api responses.
	 *
	 * @param components       the components
	 * @param methodParameter  the method parameter
	 * @param apiResponsesOp   the api responses op
	 * @param methodAttributes the method attributes
	 */
	private void buildApiResponses(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
			MethodAttributes methodAttributes) {
		Map genericMapResponse = methodAttributes.getGenericMapResponse();
		if (!CollectionUtils.isEmpty(apiResponsesOp) && apiResponsesOp.size() > genericMapResponse.size()) {
			// API Responses at operation and @ApiResponse annotation
			for (Map.Entry entry : apiResponsesOp.entrySet()) {
				String httpCode = entry.getKey();
				boolean methodAttributesCondition = !methodAttributes.isMethodOverloaded() || (methodAttributes.isMethodOverloaded() && isValidHttpCode(httpCode, methodParameter));
				if (!genericMapResponse.containsKey(httpCode) && methodAttributesCondition) {
					ApiResponse apiResponse = entry.getValue();
					buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, apiResponse, false);
				}
			}
			if (AnnotatedElementUtils.hasAnnotation(methodParameter.getMethod(), ResponseStatus.class)) {
				// Handles the case with @ResponseStatus, if the specified response is not already handled explicitly
				String httpCode = evaluateResponseStatus(methodParameter.getMethod(), Objects.requireNonNull(methodParameter.getMethod()).getClass(), false);
				if (Objects.nonNull(httpCode) && !apiResponsesOp.containsKey(httpCode) && !apiResponsesOp.containsKey(ApiResponses.DEFAULT)) {
					buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, new ApiResponse(), false);
				}
			}
		}
		else {
			String httpCode = evaluateResponseStatus(methodParameter.getMethod(), Objects.requireNonNull(methodParameter.getMethod()).getClass(), false);
			if (Objects.nonNull(httpCode))
				buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, new ApiResponse(), false);
		}
	}

	/**
	 * Gets api responses.
	 *
	 * @param method the method
	 * @return the api responses
	 */
	public Set getApiResponses(Method method) {
		Class declaringClass = method.getDeclaringClass();

		Set apiResponsesDoc = AnnotatedElementUtils
				.findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.responses.ApiResponses.class);
		Set responses = apiResponsesDoc.stream()
				.flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet());

		Set apiResponsesDocDeclaringClass = AnnotatedElementUtils
				.findAllMergedAnnotations(declaringClass, io.swagger.v3.oas.annotations.responses.ApiResponses.class);
		responses.addAll(
				apiResponsesDocDeclaringClass.stream().flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet()));

		Set apiResponseDoc = AnnotatedElementUtils
				.findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.responses.ApiResponse.class);
		responses.addAll(apiResponseDoc);

		Set apiResponseDocDeclaringClass = AnnotatedElementUtils
				.findMergedRepeatableAnnotations(declaringClass,
						io.swagger.v3.oas.annotations.responses.ApiResponse.class);
		responses.addAll(apiResponseDocDeclaringClass);

		return responses;
	}

	/**
	 * Build content content.
	 *
	 * @param components      the components
	 * @param methodParameter the method parameter
	 * @param methodProduces  the method produces
	 * @param jsonView        the json view
	 * @return the content
	 */
	private Content buildContent(Components components, MethodParameter methodParameter, String[] methodProduces, JsonView jsonView) {
		Type returnType = getReturnType(methodParameter);
		return buildContent(components, methodParameter.getParameterAnnotations(), methodProduces, jsonView, returnType);
	}

	/**
	 * Build content content.
	 *
	 * @param components     the components
	 * @param annotations    the annotations
	 * @param methodProduces the method produces
	 * @param jsonView       the json view
	 * @param returnType     the return type
	 * @return the content
	 */
	public Content buildContent(Components components, Annotation[] annotations, String[] methodProduces, JsonView jsonView, Type returnType) {
		Content content = new Content();
		// if void, no content
		if (isVoid(returnType))
			return null;
		if (ArrayUtils.isNotEmpty(methodProduces)) {
			Schema schemaN = calculateSchema(components, returnType, jsonView, annotations);
			if (schemaN != null) {
				io.swagger.v3.oas.models.media.MediaType mediaType = new io.swagger.v3.oas.models.media.MediaType();
				mediaType.setSchema(schemaN);
				// Fill the content
				setContent(methodProduces, content, mediaType);
			}
		}
		return content;
	}

	/**
	 * Gets return type.
	 *
	 * @param methodParameter the method parameter
	 * @return the return type
	 */
	private Type getReturnType(MethodParameter methodParameter) {
		Type returnType = Object.class;
		for (ReturnTypeParser returnTypeParser : returnTypeParsers) {
			if (returnType.getTypeName().equals(Object.class.getTypeName())) {
				returnType = returnTypeParser.getReturnType(methodParameter);
			}
			else
				break;
		}

		return returnType;
	}

	/**
	 * Calculate schema schema.
	 *
	 * @param components  the components
	 * @param returnType  the return type
	 * @param jsonView    the json view
	 * @param annotations the annotations
	 * @return the schema
	 */
	private Schema calculateSchema(Components components, Type returnType, JsonView jsonView, Annotation[] annotations) {
		if (!isVoid(returnType) && !SpringDocAnnotationsUtils.isAnnotationToIgnore(returnType))
			return extractSchema(components, returnType, jsonView, annotations, propertyResolverUtils.getSpecVersion());
		return null;
	}

	/**
	 * Sets content.
	 *
	 * @param methodProduces the method produces
	 * @param content        the content
	 * @param mediaType      the media type
	 */
	private void setContent(String[] methodProduces, Content content,
			io.swagger.v3.oas.models.media.MediaType mediaType) {
		Arrays.stream(methodProduces).forEach(mediaTypeStr -> content.addMediaType(mediaTypeStr, mediaType));
	}

	/**
	 * Build api responses.
	 *
	 * @param components       the components
	 * @param methodParameter  the method parameter
	 * @param apiResponsesOp   the api responses op
	 * @param methodAttributes the method attributes
	 * @param httpCode         the http code
	 * @param apiResponse      the api response
	 * @param isGeneric        the is generic
	 */
	private void buildApiResponses(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
			MethodAttributes methodAttributes, String httpCode, ApiResponse apiResponse, boolean isGeneric) {
		// No documentation
		if (StringUtils.isBlank(apiResponse.get$ref())) {
			if (apiResponse.getContent() == null) {
				Content content = buildContent(components, methodParameter, methodAttributes.getMethodProduces(),
						methodAttributes.getJsonViewAnnotation());
				apiResponse.setContent(content);
			}
			else if (CollectionUtils.isEmpty(apiResponse.getContent()))
				apiResponse.setContent(null);
			if (StringUtils.isBlank(apiResponse.getDescription())) {
				// use javadoc
				if (!StringUtils.isBlank(methodAttributes.getJavadocReturn()))
					apiResponse.setDescription(methodAttributes.getJavadocReturn());
				else
					setDescription(httpCode, apiResponse);
			}
		}
		if (apiResponse.getContent() != null
				&& ((isGeneric || methodAttributes.isMethodOverloaded()) && methodAttributes.isNoApiResponseDoc())) {
			// Merge with existing schema
			Content existingContent = apiResponse.getContent();
			Type type = ReturnTypeParser.getType(methodParameter);
			Schema schemaN = calculateSchema(components, type,
					methodAttributes.getJsonViewAnnotation(), methodParameter.getParameterAnnotations());
			if (schemaN != null && ArrayUtils.isNotEmpty(methodAttributes.getMethodProduces()))
				Arrays.stream(methodAttributes.getMethodProduces()).forEach(mediaTypeStr -> mergeSchema(existingContent, schemaN, mediaTypeStr));
		}
		if (springDocConfigProperties.isOverrideWithGenericResponse()
				&& methodParameter.getExecutable().isAnnotationPresent(ExceptionHandler.class)) {
			// ExceptionHandler's exception class resolution is non-trivial
			// more info on its javadoc
			Set> exceptions = getExceptionsFromExceptionHandler(methodParameter);
			apiResponse.addExtension(EXTENSION_EXCEPTION_CLASSES, exceptions);
		}
		apiResponsesOp.addApiResponse(httpCode, apiResponse);
	}

	/**
	 * Evaluate response status string.
	 *
	 * @param method    the method
	 * @param beanType  the bean type
	 * @param isGeneric the is generic
	 * @return the string
	 */
	public String evaluateResponseStatus(Method method, Class beanType, boolean isGeneric) {
		String responseStatus = null;
		ResponseStatus annotation = AnnotatedElementUtils.findMergedAnnotation(method, ResponseStatus.class);
		if (annotation == null && beanType != null)
			annotation = AnnotatedElementUtils.findMergedAnnotation(beanType, ResponseStatus.class);
		if (annotation != null)
			responseStatus = String.valueOf(annotation.code().value());
		if (annotation == null && !isGeneric)
			responseStatus = String.valueOf(HttpStatus.OK.value());
		return responseStatus;
	}

	/**
	 * Is void boolean.
	 *
	 * @param returnType the return type
	 * @return the boolean
	 */
	private boolean isVoid(Type returnType) {
		boolean result = false;
		if (Void.TYPE.equals(returnType) || Void.class.equals(returnType))
			result = true;
		else if (returnType instanceof ParameterizedType) {
			Type[] types = ((ParameterizedType) returnType).getActualTypeArguments();
			if (types != null && isResponseTypeWrapper(ResolvableType.forType(returnType).getRawClass()))
				result = isVoid(types[0]);
		}
		return result;
	}


	/**
	 * Gets generic map response.
	 *
	 * @param handlerMethod the handler method
	 * @return the generic map response
	 */
	private Map getGenericMapResponse(HandlerMethod handlerMethod) {
		reentrantLock.lock();
		try {
			Class beanType = handlerMethod.getBeanType();
			List 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());

			Map genericApiResponseMap = controllerAdviceInfosInThisBean.stream()
					.map(ControllerAdviceInfo::getApiResponseMap)
					.collect(LinkedHashMap::new, Map::putAll, Map::putAll);

			List controllerAdviceInfosNotInThisBean = controllerAdviceInfos.stream()
					.filter(controllerAdviceInfo ->
							new ControllerAdviceBean(controllerAdviceInfo.getControllerAdvice()).isApplicableToBeanType(beanType))
					.collect(Collectors.toList());

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

			for (ControllerAdviceInfo controllerAdviceInfo : controllerAdviceInfosNotInThisBean) {
				List methodAdviceInfos = controllerAdviceInfo.getMethodAdviceInfos();
				for (MethodAdviceInfo methodAdviceInfo : methodAdviceInfos) {
					Set> 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 genericApiResponsesClone;
			try {
				ObjectMapper objectMapper = new ObjectMapper();
				genericApiResponsesClone = objectMapper.readValue(objectMapper.writeValueAsString(genericApiResponseMap), ApiResponses.class);
				return genericApiResponsesClone;
			}
			catch (JsonProcessingException e) {
				LOGGER.warn("Json Processing Exception occurred: {}", e.getMessage());
				return genericApiResponseMap;
			}
		}
		finally {
			reentrantLock.unlock();
		}
	}

	/**
	 * Is valid http code boolean.
	 *
	 * @param httpCode        the http code
	 * @param methodParameter the method parameter
	 * @return the boolean
	 */
	private boolean isValidHttpCode(String httpCode, MethodParameter methodParameter) {
		boolean result = false;
		final Method method = methodParameter.getMethod();
		if (method != null) {
			Set responseSet = getApiResponses(method);
			if (isHttpCodePresent(httpCode, responseSet))
				result = true;
			else {
				final io.swagger.v3.oas.annotations.Operation apiOperation = AnnotatedElementUtils.findMergedAnnotation(method,
						io.swagger.v3.oas.annotations.Operation.class);
				if (apiOperation != null) {
					responseSet = new HashSet<>(Arrays.asList(apiOperation.responses()));
					if (isHttpCodePresent(httpCode, responseSet))
						result = true;
				}
				if (httpCode.equals(evaluateResponseStatus(method, method.getClass(), false))) 
					result = true;
			}
		}
		return result;
	}

	/**
	 * Is http code present boolean.
	 *
	 * @param httpCode    the http code
	 * @param responseSet the response set
	 * @return the boolean
	 */
	private boolean isHttpCodePresent(String httpCode, Set responseSet) {
		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> getExceptionsFromExceptionHandler(MethodParameter methodParameter) {
		ExceptionHandler exceptionHandler = methodParameter.getMethodAnnotation(ExceptionHandler.class);
		Set> exceptions = new HashSet<>();
		if (exceptionHandler != null) {
			if (exceptionHandler.value().length == 0) {
				for (Parameter parameter : methodParameter.getMethod().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);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy