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.io.IOException;
22 import java.lang.reflect.ParameterizedType;
23 import java.lang.reflect.Type;
24 import java.lang.reflect.WildcardType;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31
32 import com.fasterxml.jackson.annotation.JsonView;
33 import io.swagger.v3.core.util.AnnotationsUtils;
34 import io.swagger.v3.core.util.Json;
35 import io.swagger.v3.oas.annotations.Hidden;
36 import io.swagger.v3.oas.annotations.enums.Explode;
37 import io.swagger.v3.oas.annotations.media.ExampleObject;
38 import io.swagger.v3.oas.models.Components;
39 import io.swagger.v3.oas.models.examples.Example;
40 import io.swagger.v3.oas.models.media.ArraySchema;
41 import io.swagger.v3.oas.models.media.FileSchema;
42 import io.swagger.v3.oas.models.media.ObjectSchema;
43 import io.swagger.v3.oas.models.media.Schema;
44 import io.swagger.v3.oas.models.parameters.Parameter;
45 import org.apache.commons.lang3.StringUtils;
46
47 import org.springframework.core.MethodParameter;
48 import org.springframework.core.annotation.AnnotationUtils;
49 import org.springframework.http.codec.multipart.FilePart;
50 import org.springframework.web.multipart.MultipartFile;
51
52 @SuppressWarnings("rawtypes")
53 public class GenericParameterBuilder {
54
55     private static final List<Class<?>> FILE_TYPES = new ArrayList<>();
56
57     private static final List<Class> ANNOTATIOSN_TO_IGNORE = new ArrayList<>();
58
59     static {
60         FILE_TYPES.add(MultipartFile.class);
61         ANNOTATIOSN_TO_IGNORE.add(Hidden.class);
62     }
63
64     private final PropertyResolverUtils propertyResolverUtils;
65
66     public GenericParameterBuilder(PropertyResolverUtils propertyResolverUtils) {
67         this.propertyResolverUtils = propertyResolverUtils;
68     }
69
70     public static void addFileType(Class<?>... classes) {
71         FILE_TYPES.addAll(Arrays.asList(classes));
72     }
73
74     public static void addAnnotationsToIgnore(Class<?>... classes) {
75         ANNOTATIOSN_TO_IGNORE.addAll(Arrays.asList(classes));
76     }
77
78     public static void removeAnnotationsToIgnore(Class<?>... classes) {
79         List classesToIgnore = Arrays.asList(classes);
80         if (ANNOTATIOSN_TO_IGNORE.containsAll(classesToIgnore))
81             ANNOTATIOSN_TO_IGNORE.removeAll(Arrays.asList(classes));
82     }
83
84     Parameter mergeParameter(List<Parameter> existingParamDoc, Parameter paramCalcul) {
85         Parameter result = paramCalcul;
86         if (paramCalcul != null && paramCalcul.getName() != null) {
87             final String name = paramCalcul.getName();
88             Parameter paramDoc = existingParamDoc.stream().filter(p -> name.equals(p.getName())).findAny().orElse(null);
89             if (paramDoc != null) {
90                 mergeParameter(paramCalcul, paramDoc);
91                 result = paramDoc;
92             }
93             else
94                 existingParamDoc.add(result);
95         }
96         return result;
97     }
98
99     private void mergeParameter(Parameter paramCalcul, Parameter paramDoc) {
100         if (StringUtils.isBlank(paramDoc.getDescription()))
101             paramDoc.setDescription(paramCalcul.getDescription());
102
103         if (StringUtils.isBlank(paramDoc.getIn()))
104             paramDoc.setIn(paramCalcul.getIn());
105
106         if (paramDoc.getExample() == null)
107             paramDoc.setExample(paramCalcul.getExample());
108
109         if (paramDoc.getDeprecated() == null)
110             paramDoc.setDeprecated(paramCalcul.getDeprecated());
111
112         if (paramDoc.getRequired() == null)
113             paramDoc.setRequired(paramCalcul.getRequired());
114
115         if (paramDoc.getAllowEmptyValue() == null)
116             paramDoc.setAllowEmptyValue(paramCalcul.getAllowEmptyValue());
117
118         if (paramDoc.getAllowReserved() == null)
119             paramDoc.setAllowReserved(paramCalcul.getAllowReserved());
120
121         if (StringUtils.isBlank(paramDoc.get$ref()))
122             paramDoc.set$ref(paramDoc.get$ref());
123
124         if (paramDoc.getSchema() == null)
125             paramDoc.setSchema(paramCalcul.getSchema());
126
127         if (paramDoc.getExamples() == null)
128             paramDoc.setExamples(paramCalcul.getExamples());
129
130         if (paramDoc.getExtensions() == null)
131             paramDoc.setExtensions(paramCalcul.getExtensions());
132
133         if (paramDoc.getStyle() == null)
134             paramDoc.setStyle(paramCalcul.getStyle());
135
136         if (paramDoc.getExplode() == null)
137             paramDoc.setExplode(paramCalcul.getExplode());
138     }
139
140     Parameter buildParameterFromDoc(io.swagger.v3.oas.annotations.Parameter parameterDoc,
141             Components components, JsonView jsonView) {
142         Parameter parameter = new Parameter();
143         if (StringUtils.isNotBlank(parameterDoc.description()))
144             parameter.setDescription(propertyResolverUtils.resolve(parameterDoc.description()));
145         if (StringUtils.isNotBlank(parameterDoc.name()))
146             parameter.setName(propertyResolverUtils.resolve(parameterDoc.name()));
147         if (StringUtils.isNotBlank(parameterDoc.in().toString()))
148             parameter.setIn(parameterDoc.in().toString());
149         if (StringUtils.isNotBlank(parameterDoc.example())) {
150             try {
151                 parameter.setExample(Json.mapper().readTree(parameterDoc.example()));
152             }
153             catch (IOException e) {
154                 parameter.setExample(parameterDoc.example());
155             }
156         }
157         if (parameterDoc.deprecated())
158             parameter.setDeprecated(parameterDoc.deprecated());
159         if (parameterDoc.required())
160             parameter.setRequired(parameterDoc.required());
161         if (parameterDoc.allowEmptyValue())
162             parameter.setAllowEmptyValue(parameterDoc.allowEmptyValue());
163         if (parameterDoc.allowReserved())
164             parameter.setAllowReserved(parameterDoc.allowReserved());
165
166         setSchema(parameterDoc, components, jsonView, parameter);
167         setExamples(parameterDoc, parameter);
168         setExtensions(parameterDoc, parameter);
169         setParameterStyle(parameter, parameterDoc);
170         setParameterExplode(parameter, parameterDoc);
171
172         return parameter;
173     }
174
175     private void setSchema(io.swagger.v3.oas.annotations.Parameter parameterDoc, Components components, JsonView jsonView, Parameter parameter) {
176         if (StringUtils.isNotBlank(parameterDoc.ref()))
177             parameter.$ref(parameterDoc.ref());
178         else {
179             Schema schema = AnnotationsUtils.getSchemaFromAnnotation(parameterDoc.schema(), components, jsonView).orElse(null);
180             if (schema == null) {
181                 if (parameterDoc.content().length > 0) {
182                     if (AnnotationsUtils.hasSchemaAnnotation(parameterDoc.content()[0].schema()))
183                         schema = AnnotationsUtils.getSchemaFromAnnotation(parameterDoc.content()[0].schema(), components, jsonView).orElse(null);
184                     else if (AnnotationsUtils.hasArrayAnnotation(parameterDoc.content()[0].array()))
185                         schema = AnnotationsUtils.getArraySchema(parameterDoc.content()[0].array(), components, jsonView).orElse(null);
186                 }
187                 else
188                     schema = AnnotationsUtils.getArraySchema(parameterDoc.array(), components, jsonView).orElse(null);
189             }
190             parameter.setSchema(schema);
191         }
192     }
193
194     Schema calculateSchema(Components components, ParameterInfo parameterInfo, RequestBodyInfo requestBodyInfo, JsonView jsonView) {
195         Schema schemaN;
196         String paramName = parameterInfo.getpName();
197         MethodParameter methodParameter = parameterInfo.getMethodParameter();
198         Class type = methodParameter.getParameterType();
199
200         if (parameterInfo.getParameterModel() == null || parameterInfo.getParameterModel().getSchema() == null) {
201             if (isFile(type)) {
202                 schemaN = getFileSchema(requestBodyInfo);
203                 schemaN.addProperties(paramName, new FileSchema());
204                 return schemaN;
205             }
206             else if (methodParameter.getGenericParameterType() instanceof ParameterizedType) {
207                 ParameterizedType parameterizedType = (ParameterizedType) methodParameter.getGenericParameterType();
208                 if (isFile(parameterizedType))
209                     return extractFileSchema(paramName, requestBodyInfo);
210                 schemaN = SpringDocAnnotationsUtils.extractSchema(components, methodParameter.getGenericParameterType(), jsonView, methodParameter.getParameterAnnotations());
211             }
212             else
213                 schemaN = SpringDocAnnotationsUtils.resolveSchemaFromType(methodParameter.getParameterType(), components, jsonView, methodParameter.getParameterAnnotations());
214         }
215         else
216             schemaN = parameterInfo.getParameterModel().getSchema();
217
218         if (requestBodyInfo != null) {
219             if (requestBodyInfo.getMergedSchema() != null) {
220                 requestBodyInfo.getMergedSchema().addProperties(paramName, schemaN);
221                 schemaN = requestBodyInfo.getMergedSchema();
222             }
223             else
224                 requestBodyInfo.addProperties(paramName, schemaN);
225         }
226
227         return schemaN;
228     }
229
230     public boolean isFile(MethodParameter methodParameter) {
231         if (methodParameter.getGenericParameterType() instanceof ParameterizedType) {
232             Type type = methodParameter.getGenericParameterType();
233             ParameterizedType parameterizedType = (ParameterizedType) type;
234             return isFile(parameterizedType);
235         }
236         else {
237             Class type = methodParameter.getParameterType();
238             return isFile(type);
239         }
240     }
241
242     public boolean isAnnotationToIgnore(MethodParameter parameter) {
243         return ANNOTATIOSN_TO_IGNORE.stream().anyMatch(
244                 annotation -> parameter.getParameterAnnotation(annotation) != null
245                         || AnnotationUtils.findAnnotation(parameter.getParameterType(), annotation) != null);
246     }
247
248     private Schema extractFileSchema(String paramName, RequestBodyInfo requestBodyInfo) {
249         Schema schemaN = getFileSchema(requestBodyInfo);
250         ArraySchema schemaFile = new ArraySchema();
251         schemaFile.items(new FileSchema());
252         schemaN.addProperties(paramName, new ArraySchema().items(new FileSchema()));
253         return schemaN;
254     }
255
256     private Schema getFileSchema(RequestBodyInfo requestBodyInfo) {
257         Schema schemaN;
258         if (requestBodyInfo.getMergedSchema() != null)
259             schemaN = requestBodyInfo.getMergedSchema();
260         else {
261             schemaN = new ObjectSchema();
262             requestBodyInfo.setMergedSchema(schemaN);
263         }
264         return schemaN;
265     }
266
267     private boolean isFile(ParameterizedType parameterizedType) {
268         Type type = parameterizedType.getActualTypeArguments()[0];
269         if (MultipartFile.class.getName().equals(type.getTypeName())
270                 || FilePart.class.getName().equals(type.getTypeName())) {
271             return true;
272         }
273         else if (type instanceof WildcardType) {
274             WildcardType wildcardType = (WildcardType) type;
275             Type[] upperBounds = wildcardType.getUpperBounds();
276             return MultipartFile.class.getName().equals(upperBounds[0].getTypeName());
277         }
278         return false;
279     }
280
281     private void setExamples(io.swagger.v3.oas.annotations.Parameter parameterDoc, Parameter parameter) {
282         Map<String, Example> exampleMap = new HashMap<>();
283         if (parameterDoc.examples().length == 1 && StringUtils.isBlank(parameterDoc.examples()[0].name())) {
284             Optional<Example> exampleOptional = AnnotationsUtils.getExample(parameterDoc.examples()[0]);
285             exampleOptional.ifPresent(parameter::setExample);
286         }
287         else {
288             for (ExampleObject exampleObject : parameterDoc.examples()) {
289                 AnnotationsUtils.getExample(exampleObject)
290                         .ifPresent(example -> exampleMap.put(exampleObject.name(), example));
291             }
292         }
293         if (exampleMap.size() > 0) {
294             parameter.setExamples(exampleMap);
295         }
296     }
297
298     private void setExtensions(io.swagger.v3.oas.annotations.Parameter parameterDoc, Parameter parameter) {
299         if (parameterDoc.extensions().length > 0) {
300             Map<String, Object> extensionMap = AnnotationsUtils.getExtensions(parameterDoc.extensions());
301             extensionMap.forEach(parameter::addExtension);
302         }
303     }
304
305     private void setParameterExplode(Parameter parameter, io.swagger.v3.oas.annotations.Parameter p) {
306         if (isExplodable(p)) {
307             if (Explode.TRUE.equals(p.explode())) {
308                 parameter.setExplode(Boolean.TRUE);
309             }
310             else if (Explode.FALSE.equals(p.explode())) {
311                 parameter.setExplode(Boolean.FALSE);
312             }
313         }
314     }
315
316     private void setParameterStyle(Parameter parameter, io.swagger.v3.oas.annotations.Parameter p) {
317         if (StringUtils.isNotBlank(p.style().toString())) {
318             parameter.setStyle(Parameter.StyleEnum.valueOf(p.style().toString().toUpperCase()));
319         }
320     }
321
322     private boolean isExplodable(io.swagger.v3.oas.annotations.Parameter p) {
323         io.swagger.v3.oas.annotations.media.Schema schema = p.schema();
324         boolean explode = true;
325         Class<?> implementation = schema.implementation();
326         if (implementation == Void.class && !schema.type().equals("object") && !schema.type().equals("array")) {
327             explode = false;
328         }
329         return explode;
330     }
331
332     private boolean isFile(Class type) {
333         return FILE_TYPES.stream().anyMatch(clazz -> clazz.isAssignableFrom(type));
334     }
335 }
336