1
18
19 package org.springdoc.core;
20
21 import java.lang.reflect.Method;
22 import java.lang.reflect.ParameterizedType;
23 import java.lang.reflect.Type;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Optional;
30 import java.util.Set;
31 import java.util.stream.Collectors;
32 import java.util.stream.Stream;
33
34 import com.fasterxml.jackson.annotation.JsonView;
35 import io.swagger.v3.core.util.AnnotationsUtils;
36 import io.swagger.v3.oas.models.Components;
37 import io.swagger.v3.oas.models.Operation;
38 import io.swagger.v3.oas.models.media.ComposedSchema;
39 import io.swagger.v3.oas.models.media.Content;
40 import io.swagger.v3.oas.models.media.Schema;
41 import io.swagger.v3.oas.models.responses.ApiResponse;
42 import io.swagger.v3.oas.models.responses.ApiResponses;
43 import org.apache.commons.lang3.ArrayUtils;
44 import org.apache.commons.lang3.StringUtils;
45
46 import org.springframework.core.MethodParameter;
47 import org.springframework.core.ResolvableType;
48 import org.springframework.core.annotation.AnnotatedElementUtils;
49 import org.springframework.http.HttpStatus;
50 import org.springframework.util.CollectionUtils;
51 import org.springframework.web.bind.annotation.ExceptionHandler;
52 import org.springframework.web.bind.annotation.RequestMapping;
53 import org.springframework.web.bind.annotation.ResponseStatus;
54 import org.springframework.web.method.HandlerMethod;
55
56 import static org.springdoc.core.Constants.DEFAULT_DESCRIPTION;
57 import static org.springdoc.core.converters.ConverterUtils.isResponseTypeWrapper;
58
59 @SuppressWarnings("rawtypes")
60 public class GenericResponseBuilder {
61
62 private final Map<String, ApiResponse> genericMapResponse = new LinkedHashMap<>();
63
64 private final OperationBuilder operationBuilder;
65
66 private final List<ReturnTypeParser> returnTypeParsers;
67
68 private final SpringDocConfigProperties springDocConfigProperties;
69
70 private final PropertyResolverUtils propertyResolverUtils;
71
72 public GenericResponseBuilder(OperationBuilder operationBuilder, List<ReturnTypeParser> returnTypeParsers,
73 SpringDocConfigProperties springDocConfigProperties,
74 PropertyResolverUtils propertyResolverUtils) {
75 super();
76 this.operationBuilder = operationBuilder;
77 this.returnTypeParsers = returnTypeParsers;
78 this.springDocConfigProperties = springDocConfigProperties;
79 this.propertyResolverUtils = propertyResolverUtils;
80 }
81
82 public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
83 MethodAttributes methodAttributes) {
84 ApiResponses apiResponses = new ApiResponses();
85 genericMapResponse.forEach(apiResponses::addApiResponse);
86
87 ApiResponses apiResponsesFromDoc = operation.getResponses();
88 if (!CollectionUtils.isEmpty(apiResponsesFromDoc))
89 apiResponsesFromDoc.forEach(apiResponses::addApiResponse);
90
91
92 computeResponse(components, handlerMethod.getReturnType(), apiResponses, methodAttributes, false);
93 return apiResponses;
94 }
95
96 public void buildGenericResponse(Components components, Map<String, Object> findControllerAdvice) {
97
98 List<Method> methods = getMethods(findControllerAdvice);
99
100 for (Method method : methods) {
101 if (!operationBuilder.isHidden(method)) {
102 RequestMapping reqMappringMethod = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
103 String[] methodProduces = { springDocConfigProperties.getDefaultProducesMediaType() };
104 if (reqMappringMethod != null)
105 methodProduces = reqMappringMethod.produces();
106 Map<String, ApiResponse> apiResponses = computeResponse(components, new MethodParameter(method, -1), new ApiResponses(),
107 new MethodAttributes(methodProduces, springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType()), true);
108 apiResponses.forEach(genericMapResponse::put);
109 }
110 }
111 }
112
113 private List<Method> getMethods(Map<String, Object> findControllerAdvice) {
114 List<Method> methods = new ArrayList<>();
115 for (Map.Entry<String, Object> entry : findControllerAdvice.entrySet()) {
116 Object controllerAdvice = entry.getValue();
117
118 Class<?> objClz = controllerAdvice.getClass();
119 if (org.springframework.aop.support.AopUtils.isAopProxy(controllerAdvice))
120 objClz = org.springframework.aop.support.AopUtils.getTargetClass(controllerAdvice);
121 Arrays.stream(objClz.getDeclaredMethods()).filter(m -> m.isAnnotationPresent(ExceptionHandler.class)).forEach(methods::add);
122 }
123 return methods;
124 }
125
126 private Map<String, ApiResponse> computeResponse(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
127 MethodAttributes methodAttributes, boolean isGeneric) {
128
129 Set<io.swagger.v3.oas.annotations.responses.ApiResponse> responsesArray = getApiResponses(methodParameter.getMethod());
130 if (!responsesArray.isEmpty()) {
131 methodAttributes.setWithApiResponseDoc(true);
132 if (!springDocConfigProperties.isOverrideWithGenericResponse())
133 for (String key : genericMapResponse.keySet())
134 apiResponsesOp.remove(key);
135 for (io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations : responsesArray) {
136 ApiResponse apiResponse = new ApiResponse();
137 if (StringUtils.isNotBlank(apiResponseAnnotations.ref())) {
138 apiResponse.$ref(apiResponseAnnotations.ref());
139 apiResponsesOp.addApiResponse(apiResponseAnnotations.responseCode(), apiResponse);
140 continue;
141 }
142 apiResponse.setDescription(propertyResolverUtils.resolve(apiResponseAnnotations.description()));
143 io.swagger.v3.oas.annotations.media.Content[] contentdoc = apiResponseAnnotations.content();
144 buildContentFromDoc(components, apiResponsesOp, methodAttributes,
145 apiResponseAnnotations, apiResponse, contentdoc);
146 Map<String, Object> extensions = AnnotationsUtils.getExtensions(apiResponseAnnotations.extensions());
147 if (!CollectionUtils.isEmpty(extensions))
148 apiResponse.extensions(extensions);
149 AnnotationsUtils.getHeaders(apiResponseAnnotations.headers(), methodAttributes.getJsonViewAnnotation())
150 .ifPresent(apiResponse::headers);
151 apiResponsesOp.addApiResponse(apiResponseAnnotations.responseCode(), apiResponse);
152 }
153 }
154 buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, isGeneric);
155 return apiResponsesOp;
156 }
157
158 private void buildContentFromDoc(Components components, ApiResponses apiResponsesOp,
159 MethodAttributes methodAttributes,
160 io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations, ApiResponse apiResponse,
161 io.swagger.v3.oas.annotations.media.Content[] contentdoc) {
162 Optional<Content> optionalContent = SpringDocAnnotationsUtils.getContent(contentdoc, new String[0],
163 methodAttributes.getMethodProduces(), null, components, methodAttributes.getJsonViewAnnotation());
164 if (apiResponsesOp.containsKey(apiResponseAnnotations.responseCode())) {
165
166 Content existingContent = apiResponsesOp.get(apiResponseAnnotations.responseCode()).getContent();
167 if (optionalContent.isPresent() && existingContent != null) {
168 Content newContent = optionalContent.get();
169 if (methodAttributes.isMethodOverloaded()) {
170 Arrays.stream(methodAttributes.getMethodProduces()).filter(mediaTypeStr -> (newContent.get(mediaTypeStr) != null)).forEach(mediaTypeStr -> mergeSchema(existingContent, newContent.get(mediaTypeStr).getSchema(), mediaTypeStr));
171 apiResponse.content(existingContent);
172 }
173 else
174 apiResponse.content(newContent);
175 }
176 }
177 else {
178 optionalContent.ifPresent(apiResponse::content);
179 }
180 }
181
182 private void buildApiResponses(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
183 MethodAttributes methodAttributes, boolean isGeneric) {
184 if (!CollectionUtils.isEmpty(apiResponsesOp) && (apiResponsesOp.size() != genericMapResponse.size() || isGeneric)) {
185
186 for (Map.Entry<String, ApiResponse> entry : apiResponsesOp.entrySet()) {
187 String httpCode = entry.getKey();
188 ApiResponse apiResponse = entry.getValue();
189 buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, apiResponse,
190 isGeneric);
191 }
192 }
193 else {
194
195
196 String httpCode = evaluateResponseStatus(methodParameter.getMethod(), methodParameter.getMethod().getClass(), isGeneric);
197 ApiResponse apiResponse = genericMapResponse.containsKey(httpCode) ? genericMapResponse.get(httpCode)
198 : new ApiResponse();
199 if (httpCode != null)
200 buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, apiResponse,
201 isGeneric);
202 }
203 }
204
205 private Set<io.swagger.v3.oas.annotations.responses.ApiResponse> getApiResponses(Method method) {
206 Class<?> declaringClass = method.getDeclaringClass();
207
208 Set<io.swagger.v3.oas.annotations.responses.ApiResponses> apiResponsesDoc = AnnotatedElementUtils
209 .findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.responses.ApiResponses.class);
210 Set<io.swagger.v3.oas.annotations.responses.ApiResponse> responses = apiResponsesDoc.stream()
211 .flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet());
212
213 Set<io.swagger.v3.oas.annotations.responses.ApiResponses> apiResponsesDocDeclaringClass = AnnotatedElementUtils
214 .findAllMergedAnnotations(declaringClass, io.swagger.v3.oas.annotations.responses.ApiResponses.class);
215 responses.addAll(
216 apiResponsesDocDeclaringClass.stream().flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet()));
217
218 Set<io.swagger.v3.oas.annotations.responses.ApiResponse> apiResponseDoc = AnnotatedElementUtils
219 .findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.responses.ApiResponse.class);
220 responses.addAll(apiResponseDoc);
221
222 Set<io.swagger.v3.oas.annotations.responses.ApiResponse> apiResponseDocDeclaringClass = AnnotatedElementUtils
223 .findMergedRepeatableAnnotations(declaringClass,
224 io.swagger.v3.oas.annotations.responses.ApiResponse.class);
225 responses.addAll(apiResponseDocDeclaringClass);
226
227 return responses;
228 }
229
230 private Content buildContent(Components components, MethodParameter methodParameter, String[] methodProduces, JsonView jsonView) {
231 Content content = new Content();
232 Type returnType = getReturnType(methodParameter);
233
234 if (isVoid(returnType))
235 return null;
236 if (ArrayUtils.isNotEmpty(methodProduces)) {
237 Schema<?> schemaN = calculateSchema(components, returnType, jsonView);
238 if (schemaN != null) {
239 io.swagger.v3.oas.models.media.MediaType mediaType = new io.swagger.v3.oas.models.media.MediaType();
240 mediaType.setSchema(schemaN);
241
242 setContent(methodProduces, content, mediaType);
243 }
244 }
245 return content;
246 }
247
248 private Type getReturnType(MethodParameter methodParameter) {
249 Type returnType = Object.class;
250 for (ReturnTypeParser returnTypeParser : returnTypeParsers) {
251 if (returnType.getTypeName().equals(Object.class.getTypeName())) {
252 returnType = returnTypeParser.getReturnType(methodParameter);
253 }
254 else
255 break;
256 }
257
258 return returnType;
259 }
260
261 public Schema calculateSchema(Components components, Type returnType, JsonView jsonView) {
262 return !isVoid(returnType) ? SpringDocAnnotationsUtils.extractSchema(components, returnType, jsonView) : null;
263 }
264
265 private void setContent(String[] methodProduces, Content content,
266 io.swagger.v3.oas.models.media.MediaType mediaType) {
267 Arrays.stream(methodProduces).forEach(mediaTypeStr -> content.addMediaType(mediaTypeStr, mediaType));
268 }
269
270 private void buildApiResponses(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
271 MethodAttributes methodAttributes, String httpCode, ApiResponse apiResponse, boolean isGeneric) {
272
273 if (StringUtils.isBlank(apiResponse.get$ref())) {
274 if (apiResponse.getContent() == null) {
275 Content content = buildContent(components, methodParameter, methodAttributes.getMethodProduces(),
276 methodAttributes.getJsonViewAnnotation());
277 apiResponse.setContent(content);
278 }
279 else if (CollectionUtils.isEmpty(apiResponse.getContent()))
280 apiResponse.setContent(null);
281 if (StringUtils.isBlank(apiResponse.getDescription()))
282 apiResponse.setDescription(DEFAULT_DESCRIPTION);
283 }
284 if (apiResponse.getContent() != null
285 && ((isGeneric || methodAttributes.isMethodOverloaded()) && methodAttributes.isNoApiResponseDoc())) {
286
287 Content existingContent = apiResponse.getContent();
288 Schema<?> schemaN = calculateSchema(components, methodParameter.getGenericParameterType(),
289 methodAttributes.getJsonViewAnnotation());
290 if (schemaN != null && ArrayUtils.isNotEmpty(methodAttributes.getMethodProduces()))
291 Arrays.stream(methodAttributes.getMethodProduces()).forEach(mediaTypeStr -> mergeSchema(existingContent, schemaN, mediaTypeStr));
292 }
293 apiResponsesOp.addApiResponse(httpCode, apiResponse);
294 }
295
296 private void mergeSchema(Content existingContent, Schema<?> schemaN, String mediaTypeStr) {
297 if (existingContent.containsKey(mediaTypeStr)) {
298 io.swagger.v3.oas.models.media.MediaType mediaType = existingContent.get(mediaTypeStr);
299 if (!schemaN.equals(mediaType.getSchema())) {
300
301 Schema firstSchema = mediaType.getSchema();
302 ComposedSchema schemaObject;
303 if (firstSchema instanceof ComposedSchema) {
304 schemaObject = (ComposedSchema) firstSchema;
305 List<Schema> listOneOf = schemaObject.getOneOf();
306 if (!CollectionUtils.isEmpty(listOneOf) && !listOneOf.contains(schemaN))
307 schemaObject.addOneOfItem(schemaN);
308 }
309 else {
310 schemaObject = new ComposedSchema();
311 schemaObject.addOneOfItem(schemaN);
312 schemaObject.addOneOfItem(firstSchema);
313 }
314 mediaType.setSchema(schemaObject);
315 existingContent.addMediaType(mediaTypeStr, mediaType);
316 }
317 }
318 else
319
320 existingContent.addMediaType(mediaTypeStr, new io.swagger.v3.oas.models.media.MediaType().schema(schemaN));
321 }
322
323 private String evaluateResponseStatus(Method method, Class<?> beanType, boolean isGeneric) {
324 String responseStatus = null;
325 ResponseStatus annotation = AnnotatedElementUtils.findMergedAnnotation(method, ResponseStatus.class);
326 if (annotation == null && beanType != null)
327 annotation = AnnotatedElementUtils.findMergedAnnotation(beanType, ResponseStatus.class);
328 if (annotation != null)
329 responseStatus = String.valueOf(annotation.code().value());
330 if (annotation == null && !isGeneric)
331 responseStatus = String.valueOf(HttpStatus.OK.value());
332 return responseStatus;
333 }
334
335 private boolean isVoid(Type returnType) {
336 boolean result = false;
337 if (Void.TYPE.equals(returnType))
338 result = true;
339 else if (returnType instanceof ParameterizedType) {
340 Type[] types = ((ParameterizedType) returnType).getActualTypeArguments();
341 if (types != null && isResponseTypeWrapper(ResolvableType.forType(returnType).getRawClass()))
342 return isVoid(types[0]);
343 }
344 if (Void.class.equals(returnType))
345 result = true;
346 return result;
347 }
348 }
349