1
18
19 package org.springdoc.core;
20
21 import java.lang.reflect.Method;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.LinkedHashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.Set;
30 import java.util.stream.Collectors;
31
32 import io.swagger.v3.core.util.AnnotationsUtils;
33 import io.swagger.v3.oas.annotations.Hidden;
34 import io.swagger.v3.oas.models.Components;
35 import io.swagger.v3.oas.models.OpenAPI;
36 import io.swagger.v3.oas.models.Operation;
37 import io.swagger.v3.oas.models.PathItem;
38 import io.swagger.v3.oas.models.callbacks.Callback;
39 import io.swagger.v3.oas.models.headers.Header;
40 import io.swagger.v3.oas.models.links.Link;
41 import io.swagger.v3.oas.models.media.Schema;
42 import io.swagger.v3.oas.models.parameters.Parameter;
43 import io.swagger.v3.oas.models.responses.ApiResponse;
44 import io.swagger.v3.oas.models.responses.ApiResponses;
45 import org.apache.commons.lang3.StringUtils;
46
47 import org.springframework.core.annotation.AnnotationUtils;
48 import org.springframework.util.CollectionUtils;
49
50 import static org.springdoc.core.Constants.DEFAULT_DESCRIPTION;
51 import static org.springdoc.core.Constants.DELETE_METHOD;
52 import static org.springdoc.core.Constants.GET_METHOD;
53 import static org.springdoc.core.Constants.HEAD_METHOD;
54 import static org.springdoc.core.Constants.OPTIONS_METHOD;
55 import static org.springdoc.core.Constants.PATCH_METHOD;
56 import static org.springdoc.core.Constants.POST_METHOD;
57 import static org.springdoc.core.Constants.PUT_METHOD;
58 import static org.springdoc.core.Constants.TRACE_METHOD;
59
60 public class OperationBuilder {
61
62 private final GenericParameterBuilder parameterBuilder;
63
64 private final RequestBodyBuilder requestBodyBuilder;
65
66 private final SecurityParser securityParser;
67
68 private final PropertyResolverUtils propertyResolverUtils;
69
70 public OperationBuilder(GenericParameterBuilder parameterBuilder, RequestBodyBuilder requestBodyBuilder,
71 SecurityParser securityParser, PropertyResolverUtils propertyResolverUtils) {
72 super();
73 this.parameterBuilder = parameterBuilder;
74 this.requestBodyBuilder = requestBodyBuilder;
75 this.securityParser = securityParser;
76 this.propertyResolverUtils = propertyResolverUtils;
77 }
78
79 public OpenAPI parse(io.swagger.v3.oas.annotations.Operation apiOperation,
80 Operation operation, OpenAPI openAPI, MethodAttributes methodAttributes) {
81 Components components = openAPI.getComponents();
82 if (StringUtils.isNotBlank(apiOperation.summary()))
83 operation.setSummary(propertyResolverUtils.resolve(apiOperation.summary()));
84
85 if (StringUtils.isNotBlank(apiOperation.description()))
86 operation.setDescription(propertyResolverUtils.resolve(apiOperation.description()));
87
88 if (StringUtils.isNotBlank(apiOperation.operationId()))
89 operation.setOperationId(getOperationId(apiOperation.operationId(), openAPI));
90
91 if (apiOperation.deprecated())
92 operation.setDeprecated(apiOperation.deprecated());
93
94 buildTags(apiOperation, operation);
95
96 if (operation.getExternalDocs() == null)
97 AnnotationsUtils.getExternalDocumentation(apiOperation.externalDocs())
98 .ifPresent(operation::setExternalDocs);
99
100
101 AnnotationsUtils.getServers(apiOperation.servers())
102 .ifPresent(servers -> servers.forEach(operation::addServersItem));
103
104
105 for (io.swagger.v3.oas.annotations.Parameter parameterDoc : apiOperation.parameters()) {
106 Parameter parameter = parameterBuilder.buildParameterFromDoc(parameterDoc, components,
107 methodAttributes.getJsonViewAnnotation());
108 operation.addParametersItem(parameter);
109 }
110
111
112 requestBodyBuilder.buildRequestBodyFromDoc(apiOperation.requestBody(), methodAttributes.getClassConsumes(),
113 methodAttributes.getMethodConsumes(), components, null).ifPresent(operation::setRequestBody);
114
115
116 buildResponse(components, apiOperation, operation, methodAttributes);
117
118
119 securityParser.buildSecurityRequirement(apiOperation.security(), operation);
120
121
122 buildExtensions(apiOperation, operation);
123 return openAPI;
124 }
125
126 public boolean isHidden(Method method) {
127 io.swagger.v3.oas.annotations.Operation apiOperation = AnnotationUtils.findAnnotation(method,
128 io.swagger.v3.oas.annotations.Operation.class);
129 return (apiOperation != null && (apiOperation.hidden()))
130 || (AnnotationUtils.findAnnotation(method, Hidden.class) != null);
131 }
132
133 public Optional<Map<String, Callback>> buildCallbacks(
134 Set<io.swagger.v3.oas.annotations.callbacks.Callback> apiCallbacks, OpenAPI openAPI,
135 MethodAttributes methodAttributes) {
136 Map<String, Callback> callbacks = new LinkedHashMap<>();
137
138 boolean doBreak = false;
139 for (io.swagger.v3.oas.annotations.callbacks.Callback methodCallback : apiCallbacks) {
140 Map<String, Callback> callbackMap = new HashMap<>();
141 if (methodCallback == null) {
142 callbacks.putAll(callbackMap);
143 doBreak = true;
144 }
145 Callback callbackObject = new Callback();
146 if (!doBreak && StringUtils.isNotBlank(methodCallback.ref())) {
147 callbackObject.set$ref(methodCallback.ref());
148 callbackMap.put(methodCallback.name(), callbackObject);
149 callbacks.putAll(callbackMap);
150 doBreak = true;
151 }
152
153 if (doBreak)
154 break;
155
156 PathItem pathItemObject = new PathItem();
157 for (io.swagger.v3.oas.annotations.Operation callbackOperation : methodCallback.operation()) {
158 Operation callbackNewOperation = new Operation();
159 parse(callbackOperation, callbackNewOperation, openAPI, methodAttributes);
160 setPathItemOperation(pathItemObject, callbackOperation.method(), callbackNewOperation);
161 }
162 callbackObject.addPathItem(methodCallback.callbackUrlExpression(), pathItemObject);
163 callbackMap.put(methodCallback.name(), callbackObject);
164 callbacks.putAll(callbackMap);
165 }
166
167 if (CollectionUtils.isEmpty(callbacks))
168 return Optional.empty();
169 else
170 return Optional.of(callbacks);
171 }
172
173 private void setPathItemOperation(PathItem pathItemObject, String method, Operation operation) {
174 switch (method) {
175 case POST_METHOD:
176 pathItemObject.post(operation);
177 break;
178 case GET_METHOD:
179 pathItemObject.get(operation);
180 break;
181 case DELETE_METHOD:
182 pathItemObject.delete(operation);
183 break;
184 case PUT_METHOD:
185 pathItemObject.put(operation);
186 break;
187 case PATCH_METHOD:
188 pathItemObject.patch(operation);
189 break;
190 case TRACE_METHOD:
191 pathItemObject.trace(operation);
192 break;
193 case HEAD_METHOD:
194 pathItemObject.head(operation);
195 break;
196 case OPTIONS_METHOD:
197 pathItemObject.options(operation);
198 break;
199 default:
200
201 break;
202 }
203 }
204
205 private void buildExtensions(io.swagger.v3.oas.annotations.Operation apiOperation, Operation operation) {
206 if (apiOperation.extensions().length > 0) {
207 Map<String, Object> extensions = AnnotationsUtils.getExtensions(apiOperation.extensions());
208 extensions.forEach(operation::addExtension);
209 }
210 }
211
212 private void buildTags(io.swagger.v3.oas.annotations.Operation apiOperation, Operation operation) {
213 Optional<List<String>> mlist = getStringListFromStringArray(apiOperation.tags());
214 if (mlist.isPresent()) {
215 List<String> tags = mlist.get().stream()
216 .filter(t -> operation.getTags() == null
217 || (operation.getTags() != null && !operation.getTags().contains(t)))
218 .collect(Collectors.toList());
219 tags.forEach(operation::addTagsItem);
220 }
221 }
222
223 private String getOperationId(String operationId, OpenAPI openAPI) {
224 boolean operationIdUsed = existOperationId(operationId, openAPI);
225 String operationIdToFind = null;
226 int counter = 0;
227 while (operationIdUsed) {
228 operationIdToFind = String.format("%s_%d", operationId, ++counter);
229 operationIdUsed = existOperationId(operationIdToFind, openAPI);
230 }
231 if (operationIdToFind != null) {
232 operationId = operationIdToFind;
233 }
234 return operationId;
235 }
236
237 private boolean existOperationId(String operationId, OpenAPI openAPI) {
238 if (openAPI == null) {
239 return false;
240 }
241 if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) {
242 return false;
243 }
244 for (PathItem path : openAPI.getPaths().values()) {
245 Set<String> pathOperationIds = extractOperationIdFromPathItem(path);
246 if (pathOperationIds.contains(operationId)) {
247 return true;
248 }
249 }
250 return false;
251 }
252
253 private Set<String> extractOperationIdFromPathItem(PathItem path) {
254 Set<String> ids = new HashSet<>();
255 if (path.getGet() != null && StringUtils.isNotBlank(path.getGet().getOperationId())) {
256 ids.add(path.getGet().getOperationId());
257 }
258 if (path.getPost() != null && StringUtils.isNotBlank(path.getPost().getOperationId())) {
259 ids.add(path.getPost().getOperationId());
260 }
261 if (path.getPut() != null && StringUtils.isNotBlank(path.getPut().getOperationId())) {
262 ids.add(path.getPut().getOperationId());
263 }
264 if (path.getDelete() != null && StringUtils.isNotBlank(path.getDelete().getOperationId())) {
265 ids.add(path.getDelete().getOperationId());
266 }
267 if (path.getOptions() != null && StringUtils.isNotBlank(path.getOptions().getOperationId())) {
268 ids.add(path.getOptions().getOperationId());
269 }
270 if (path.getHead() != null && StringUtils.isNotBlank(path.getHead().getOperationId())) {
271 ids.add(path.getHead().getOperationId());
272 }
273 if (path.getPatch() != null && StringUtils.isNotBlank(path.getPatch().getOperationId())) {
274 ids.add(path.getPatch().getOperationId());
275 }
276 return ids;
277 }
278
279 private Optional<ApiResponses> getApiResponses(
280 final io.swagger.v3.oas.annotations.responses.ApiResponse[] responses, String[] classProduces,
281 String[] methodProduces, Components components) {
282
283 ApiResponses apiResponsesObject = new ApiResponses();
284 for (io.swagger.v3.oas.annotations.responses.ApiResponse response : responses) {
285 ApiResponse apiResponseObject = new ApiResponse();
286 if (StringUtils.isNotBlank(response.ref())) {
287 setRef(apiResponsesObject, response, apiResponseObject);
288 continue;
289 }
290 setDescription(response, apiResponseObject);
291 setExtensions(response, apiResponseObject);
292
293 SpringDocAnnotationsUtils.getContent(response.content(),
294 classProduces == null ? new String[0] : classProduces,
295 methodProduces == null ? new String[0] : methodProduces, null, components, null)
296 .ifPresent(apiResponseObject::content);
297 AnnotationsUtils.getHeaders(response.headers(), null).ifPresent(apiResponseObject::headers);
298
299 calculateHeader(apiResponseObject);
300 if (isResponseObject(apiResponseObject)) {
301 setLinks(response, apiResponseObject);
302 if (StringUtils.isNotBlank(response.responseCode())) {
303 apiResponsesObject.addApiResponse(response.responseCode(), apiResponseObject);
304 }
305 else {
306 apiResponsesObject._default(apiResponseObject);
307 }
308 }
309 }
310
311 return Optional.of(apiResponsesObject);
312 }
313
314 private boolean isResponseObject(ApiResponse apiResponseObject) {
315 return StringUtils.isNotBlank(apiResponseObject.getDescription()) || apiResponseObject.getContent() != null
316 || apiResponseObject.getHeaders() != null;
317 }
318
319 private void setLinks(io.swagger.v3.oas.annotations.responses.ApiResponse response, ApiResponse apiResponseObject) {
320 Map<String, Link> links = AnnotationsUtils.getLinks(response.links());
321 if (links.size() > 0) {
322 apiResponseObject.setLinks(links);
323 }
324 }
325
326 private void setDescription(io.swagger.v3.oas.annotations.responses.ApiResponse response,
327 ApiResponse apiResponseObject) {
328 if (StringUtils.isNotBlank(response.description())) {
329 apiResponseObject.setDescription(response.description());
330 }
331 else {
332 apiResponseObject.setDescription(DEFAULT_DESCRIPTION);
333 }
334 }
335
336 private void calculateHeader(ApiResponse apiResponseObject) {
337 Map<String, Header> headers = apiResponseObject.getHeaders();
338 if (!CollectionUtils.isEmpty(headers)) {
339 for (Map.Entry<String, Header> entry : headers.entrySet()) {
340 Header header = entry.getValue();
341 if (header.getSchema() == null) {
342 Schema<?> schema = AnnotationsUtils.resolveSchemaFromType(String.class, null, null);
343 header.setSchema(schema);
344 entry.setValue(header);
345 }
346 }
347 }
348 }
349
350 private void setRef(ApiResponses apiResponsesObject, io.swagger.v3.oas.annotations.responses.ApiResponse response,
351 ApiResponse apiResponseObject) {
352 apiResponseObject.set$ref(response.ref());
353 if (StringUtils.isNotBlank(response.responseCode())) {
354 apiResponsesObject.addApiResponse(response.responseCode(), apiResponseObject);
355 }
356 else {
357 apiResponsesObject._default(apiResponseObject);
358 }
359 }
360
361 private void setExtensions(io.swagger.v3.oas.annotations.responses.ApiResponse response,
362 ApiResponse apiResponseObject) {
363 if (response.extensions().length > 0) {
364 Map<String, Object> extensions = AnnotationsUtils.getExtensions(response.extensions());
365 extensions.forEach(apiResponseObject::addExtension);
366 }
367 }
368
369 private void buildResponse(Components components, io.swagger.v3.oas.annotations.Operation apiOperation,
370 Operation operation, MethodAttributes methodAttributes) {
371 getApiResponses(apiOperation.responses(), methodAttributes.getClassProduces(),
372 methodAttributes.getMethodProduces(), components).ifPresent(responses -> {
373 if (operation.getResponses() == null) {
374 operation.setResponses(responses);
375 }
376 else {
377 responses.forEach(operation.getResponses()::addApiResponse);
378 }
379 });
380 }
381
382 private Optional<List<String>> getStringListFromStringArray(String[] array) {
383 if (array == null) {
384 return Optional.empty();
385 }
386 List<String> list = new ArrayList<>();
387 boolean isEmpty = true;
388 for (String value : array) {
389 if (StringUtils.isNotBlank(value)) {
390 isEmpty = false;
391 }
392 list.add(value);
393 }
394 if (isEmpty) {
395 return Optional.empty();
396 }
397 return Optional.of(list);
398 }
399
400 public String getOperationId(String operationId, String oldOperationId, OpenAPI openAPI) {
401 if (StringUtils.isNotBlank(oldOperationId))
402 return this.getOperationId(oldOperationId, openAPI);
403 else
404 return this.getOperationId(operationId, openAPI);
405 }
406
407 }
408