1 /*
2  *
3  *  * Copyright 2019-2020 the original author or authors.
4  *  *
5  *  * Licensed under the Apache License, Version 2.0 (the "License");
6  *  * you may not use this file except in compliance with the License.
7  *  * You may obtain a copy of the License at
8  *  *
9  *  *      https://www.apache.org/licenses/LICENSE-2.0
10  *  *
11  *  * Unless required by applicable law or agreed to in writing, software
12  *  * distributed under the License is distributed on an "AS IS" BASIS,
13  *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  * See the License for the specific language governing permissions and
15  *  * limitations under the License.
16  *
17  */

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)  // if not set in root annotation
97             AnnotationsUtils.getExternalDocumentation(apiOperation.externalDocs())
98                     .ifPresent(operation::setExternalDocs);
99
100         // servers
101         AnnotationsUtils.getServers(apiOperation.servers())
102                 .ifPresent(servers -> servers.forEach(operation::addServersItem));
103
104         // build parameters
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         // RequestBody in Operation
112         requestBodyBuilder.buildRequestBodyFromDoc(apiOperation.requestBody(), methodAttributes.getClassConsumes(),
113                 methodAttributes.getMethodConsumes(), components, null).ifPresent(operation::setRequestBody);
114
115         // build response
116         buildResponse(components, apiOperation, operation, methodAttributes);
117
118         // security
119         securityParser.buildSecurityRequirement(apiOperation.security(), operation);
120
121         // Extensions in Operation
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                 // Do nothing here
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             // Make schema as string if empty
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.classnullnull);
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