Skip to content

Commit 9fe11a2

Browse files
committed
Merge remote-tracking branch 'origin/main'
# Conflicts: # springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
2 parents 05957ce + 963ee22 commit 9fe11a2

14 files changed

Lines changed: 346 additions & 83 deletions

File tree

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,13 @@
3636
import java.lang.reflect.Method;
3737
import java.security.Principal;
3838
import java.time.ZoneId;
39-
import java.util.ArrayList;
40-
import java.util.Arrays;
41-
import java.util.Collection;
42-
import java.util.Collections;
43-
import java.util.HashMap;
44-
import java.util.Iterator;
45-
import java.util.LinkedHashMap;
46-
import java.util.List;
47-
import java.util.Locale;
48-
import java.util.Map;
39+
import java.util.*;
4940
import java.util.Map.Entry;
50-
import java.util.Objects;
51-
import java.util.Optional;
52-
import java.util.Set;
53-
import java.util.TimeZone;
5441
import java.util.stream.Collectors;
5542
import java.util.stream.Stream;
5643

5744
import com.fasterxml.jackson.annotation.JsonView;
5845
import io.swagger.v3.core.converter.AnnotatedType;
59-
import io.swagger.v3.core.util.PrimitiveType;
6046
import io.swagger.v3.oas.annotations.Parameters;
6147
import io.swagger.v3.oas.annotations.enums.ParameterIn;
6248
import io.swagger.v3.oas.models.Components;
@@ -416,8 +402,12 @@ else if (!RequestMethod.GET.equals(requestMethod) || OpenApiVersion.OPENAPI_3_1.
416402
* @param parametersDocMap the parameters doc map
417403
* @return the parameter linked hash map
418404
*/
419-
private LinkedHashMap<ParameterId, Parameter> getParameterLinkedHashMap(Components components, MethodAttributes methodAttributes, List<Parameter> operationParameters, Map<ParameterId, io.swagger.v3.oas.annotations.Parameter> parametersDocMap) {
420-
LinkedHashMap<ParameterId, Parameter> map = operationParameters.stream().collect(Collectors.toMap(ParameterId::new, parameter -> parameter, (u, v) -> {
405+
private LinkedHashMap<ParameterId, Parameter> getParameterLinkedHashMap(Components components,
406+
MethodAttributes methodAttributes,
407+
List<Parameter> operationParameters,
408+
Map<ParameterId, io.swagger.v3.oas.annotations.Parameter> parametersDocMap) {
409+
LinkedHashMap<ParameterId, Parameter> map = operationParameters.stream()
410+
.collect(Collectors.toMap(ParameterId::new, parameter -> parameter, (u, v) -> {
421411
LOGGER.warn(
422412
"Duplicate OpenAPI parameter detected: name='{}', in='{}'. Keeping the first found and ignoring the rest. " +
423413
"Declare the parameter only once.",
@@ -439,7 +429,8 @@ private LinkedHashMap<ParameterId, Parameter> getParameterLinkedHashMap(Componen
439429
long mumParamsWithName = map.keySet().stream().filter(parameterId1 -> parameterId.getpName().equals(parameterId1.getpName())).count();
440430
long mumParamsDocWithName = parametersDocMap.keySet().stream().filter(parameterId1 -> parameterId.getpName().equals(parameterId1.getpName())).count();
441431
if (mumParamsWithName == 1 && mumParamsDocWithName == 1) {
442-
Optional<ParameterId> parameterIdWithSameNameOptional = map.keySet().stream().filter(parameterId1 -> parameterId.getpName().equals(parameterId1.getpName())).findAny();
432+
Optional<ParameterId> parameterIdWithSameNameOptional = map.keySet().stream()
433+
.filter(parameterId1 -> parameterId.getpName().equals(parameterId1.getpName())).findAny();
443434
parameterIdWithSameNameOptional.ifPresent(parameterIdWithSameName -> {
444435
GenericParameterService.mergeParameter(map.get(parameterIdWithSameName), parameter);
445436
map.put(parameterIdWithSameName, parameter);
@@ -574,7 +565,8 @@ private void setParams(Operation operation, List<Parameter> operationParameters,
574565
* @return the boolean
575566
*/
576567
public boolean isValidParameter(Parameter parameter, MethodAttributes methodAttributes) {
577-
return parameter != null && (parameter.getName() != null || parameter.get$ref() != null) && !(ArrayUtils.contains(methodAttributes.getMethodConsumes(), APPLICATION_FORM_URLENCODED_VALUE) && ParameterIn.QUERY.toString().equals(parameter.getIn()));
568+
return parameter != null && (parameter.getName() != null || parameter.get$ref() != null) &&
569+
!(ArrayUtils.contains(methodAttributes.getMethodConsumes(), APPLICATION_FORM_URLENCODED_VALUE) && ParameterIn.QUERY.toString().equals(parameter.getIn()));
578570
}
579571

580572
/**
@@ -639,14 +631,7 @@ public Parameter buildParam(ParameterInfo parameterInfo, Components components,
639631
Schema<?> schema = parameterBuilder.calculateSchema(components, parameterInfo, null,
640632
jsonView);
641633
if (parameterInfo.getDefaultValue() != null && schema != null) {
642-
Object defaultValue = parameterInfo.getDefaultValue();
643-
// Cast default value
644-
PrimitiveType primitiveType = PrimitiveType.fromTypeAndFormat(schema.getType(), schema.getFormat());
645-
if (primitiveType != null) {
646-
Schema<?> primitiveSchema = primitiveType.createProperty();
647-
primitiveSchema.setDefault(parameterInfo.getDefaultValue());
648-
defaultValue = primitiveSchema.getDefault();
649-
}
634+
Object defaultValue = SpringDocAnnotationsUtils.castDefaultValue(schema, parameterInfo.getDefaultValue());
650635
if (defaultValue != null)
651636
schema.setDefault(defaultValue);
652637
}
@@ -664,7 +649,11 @@ public Parameter buildParam(ParameterInfo parameterInfo, Components components,
664649
* @param isParameterObject the is parameter object
665650
* @param openapiVersion the openapi version
666651
*/
667-
public void applyBeanValidatorAnnotations(final MethodParameter methodParameter, final Parameter parameter, final List<Annotation> annotations, final boolean isParameterObject, String openapiVersion) {
652+
public void applyBeanValidatorAnnotations(final MethodParameter methodParameter,
653+
final Parameter parameter,
654+
final List<Annotation> annotations,
655+
final boolean isParameterObject,
656+
String openapiVersion) {
668657
boolean annotatedNotNull = annotations != null && SchemaUtils.annotatedNotNull(annotations);
669658
if (annotatedNotNull && !isParameterObject) {
670659
parameter.setRequired(true);
@@ -760,18 +749,22 @@ private Map<ParameterId, io.swagger.v3.oas.annotations.Parameter> getApiParamete
760749
Class<?> declaringClass = method.getDeclaringClass();
761750

762751
Set<Parameters> apiParametersDoc = AnnotatedElementUtils.findAllMergedAnnotations(method, Parameters.class);
763-
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParametersMap = apiParametersDoc.stream().flatMap(x -> Stream.of(x.value())).collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
752+
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParametersMap = apiParametersDoc.stream()
753+
.flatMap(x -> Stream.of(x.value())).collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
764754

765755
Set<Parameters> apiParametersDocDeclaringClass = AnnotatedElementUtils.findAllMergedAnnotations(declaringClass, Parameters.class);
766-
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParametersDocDeclaringClassMap = apiParametersDocDeclaringClass.stream().flatMap(x -> Stream.of(x.value())).collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
756+
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParametersDocDeclaringClassMap = apiParametersDocDeclaringClass.stream()
757+
.flatMap(x -> Stream.of(x.value())).collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
767758
apiParametersMap.putAll(apiParametersDocDeclaringClassMap);
768759

769760
Set<io.swagger.v3.oas.annotations.Parameter> apiParameterDoc = AnnotatedElementUtils.findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.Parameter.class);
770-
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParameterDocMap = apiParameterDoc.stream().collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
761+
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParameterDocMap = apiParameterDoc.stream()
762+
.collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
771763
apiParametersMap.putAll(apiParameterDocMap);
772764

773765
Set<io.swagger.v3.oas.annotations.Parameter> apiParameterDocDeclaringClass = AnnotatedElementUtils.findAllMergedAnnotations(declaringClass, io.swagger.v3.oas.annotations.Parameter.class);
774-
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParameterDocDeclaringClassMap = apiParameterDocDeclaringClass.stream().collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
766+
LinkedHashMap<ParameterId, io.swagger.v3.oas.annotations.Parameter> apiParameterDocDeclaringClassMap = apiParameterDocDeclaringClass.stream()
767+
.collect(Collectors.toMap(ParameterId::new, x -> x, (e1, e2) -> e2, LinkedHashMap::new));
775768
apiParametersMap.putAll(apiParameterDocDeclaringClassMap);
776769

777770
return apiParametersMap;

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/PropertyResolverUtils.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,12 @@ public Map<String, Object> resolveExtensions(Locale locale, Map<String, Object>
234234
Map<String, Object> valueResolved = new HashMap<>();
235235
valueAsMap.forEach((key1, value1) -> {
236236
String key1Resolved = resolve(key1.toString(), locale);
237-
String value1Resolved = resolve(value1.toString(), locale);
237+
Object value1Resolved;
238+
if (value1 instanceof String valueAsString) {
239+
value1Resolved = resolve(valueAsString, locale);
240+
} else {
241+
value1Resolved = value1;
242+
}
238243
valueResolved.put(key1Resolved, value1Resolved);
239244
});
240245
extensionsResolved.put(keyResolved, valueResolved);

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SchemaUtils.java

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,15 @@
66
import java.lang.reflect.Field;
77
import java.lang.reflect.Method;
88
import java.math.BigDecimal;
9-
import java.util.Collection;
10-
import java.util.HashSet;
11-
import java.util.List;
12-
import java.util.Optional;
13-
import java.util.OptionalDouble;
14-
import java.util.OptionalInt;
15-
import java.util.OptionalLong;
16-
import java.util.Set;
9+
import java.util.*;
1710
import java.util.stream.Collectors;
1811

1912
import com.fasterxml.jackson.annotation.JsonProperty;
2013
import io.swagger.v3.oas.annotations.Parameter;
2114
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
2215
import io.swagger.v3.oas.models.media.Schema;
16+
import jakarta.validation.Constraint;
17+
import jakarta.validation.OverridesAttribute;
2318
import jakarta.validation.constraints.DecimalMax;
2419
import jakarta.validation.constraints.DecimalMin;
2520
import jakarta.validation.constraints.Max;
@@ -30,6 +25,7 @@
3025
import jakarta.validation.constraints.Positive;
3126
import jakarta.validation.constraints.PositiveOrZero;
3227
import jakarta.validation.constraints.Size;
28+
import org.hibernate.validator.constraints.Range;
3329
import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
3430

3531
import org.springframework.lang.Nullable;
@@ -229,7 +225,7 @@ public boolean fieldRequired(Field field, io.swagger.v3.oas.annotations.media.Sc
229225
* @param openapiVersion the openapi version
230226
*/
231227
public static void applyValidationsToSchema(Schema<?> schema, List<Annotation> annotations, String openapiVersion) {
232-
annotations.forEach(anno -> {
228+
removeComposingConstraints(annotations).forEach(anno -> {
233229
String annotationName = anno.annotationType().getSimpleName();
234230
if (annotationName.equals(Positive.class.getSimpleName())) {
235231
if (OpenApiVersion.OPENAPI_3_1.getVersion().equals(openapiVersion)) {
@@ -295,6 +291,10 @@ else if (OPENAPI_STRING_TYPE.equals(type)) {
295291
if (annotationName.equals(Pattern.class.getSimpleName())) {
296292
schema.setPattern(((Pattern) anno).regexp());
297293
}
294+
if (annotationName.equals(Range.class.getSimpleName())) {
295+
schema.setMinimum(BigDecimal.valueOf(((Range) anno).min()));
296+
schema.setMaximum(BigDecimal.valueOf(((Range) anno).max()));
297+
}
298298
});
299299
if (schema!=null && annotatedNotNull(annotations)) {
300300
String specVersion = schema.getSpecVersion().name();
@@ -304,6 +304,50 @@ else if (OPENAPI_STRING_TYPE.equals(type)) {
304304
}
305305
}
306306

307+
/**
308+
* Remove the composing constraints from the annotations. This is necessary since otherwise the annotations may
309+
* default to the composing constraints' default value (dependent on the annotation ordering).
310+
* An example is {@link Range} being a composed constraint for {@link Min} and {@link Max}.
311+
* So {@link Min} and {@link Max} are removed to ensure that the constraint values are read from {@link Range}.
312+
*
313+
* @param constraintAnnotations constraint annotations
314+
* @return the annotations where known composing constraints have been removed
315+
*/
316+
private static List<Annotation> removeComposingConstraints(List<Annotation> constraintAnnotations) {
317+
Set<Class<? extends Annotation>> composingTypes = new HashSet<>();
318+
for (Annotation ann : constraintAnnotations) {
319+
Class<? extends Annotation> type = ann.annotationType();
320+
List<Class<? extends Annotation>> annotationOverrides = findOverrides(ann);
321+
for (Annotation meta : type.getAnnotations()) {
322+
if (annotationOverrides.contains(meta.annotationType())) {
323+
composingTypes.add(meta.annotationType());
324+
}
325+
}
326+
}
327+
return constraintAnnotations.stream().filter(annotation -> !composingTypes.contains(annotation.annotationType())).toList();
328+
}
329+
330+
/**
331+
*
332+
* @param annotation the composed constraint annotation
333+
* @return the composing annotations that are overridden with {@link OverridesAttribute}
334+
*/
335+
private static List<Class<? extends Annotation>> findOverrides(Annotation annotation) {
336+
List<Class<? extends Annotation>> overriddenConstraintAnnotations = new ArrayList<>();
337+
338+
Class<? extends Annotation> type = annotation.annotationType();
339+
340+
for (Method method : type.getDeclaredMethods()) {
341+
OverridesAttribute oa = method.getAnnotation(OverridesAttribute.class);
342+
343+
if (oa != null) {
344+
overriddenConstraintAnnotations.add(oa.constraint());
345+
}
346+
}
347+
348+
return overriddenConstraintAnnotations;
349+
}
350+
307351
/**
308352
* Nullable from annotations boolean.
309353
*

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocAnnotationsUtils.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.io.IOException;
3030
import java.lang.annotation.Annotation;
3131
import java.lang.reflect.Type;
32+
import java.time.LocalDate;
3233
import java.util.ArrayList;
3334
import java.util.Arrays;
3435
import java.util.Collections;
@@ -50,6 +51,7 @@
5051
import io.swagger.v3.core.converter.ModelConverters;
5152
import io.swagger.v3.core.converter.ResolvedSchema;
5253
import io.swagger.v3.core.util.AnnotationsUtils;
54+
import io.swagger.v3.core.util.PrimitiveType;
5355
import io.swagger.v3.oas.annotations.Hidden;
5456
import io.swagger.v3.oas.annotations.media.Encoding;
5557
import io.swagger.v3.oas.annotations.media.ExampleObject;
@@ -468,6 +470,31 @@ private static boolean isArray(io.swagger.v3.oas.annotations.media.Content annot
468470
return isArray;
469471
}
470472

473+
/**
474+
* Attempt to cast the default value so that it matches the {@link Schema} type.
475+
* If the value cannot be cast then the provided default value is returned as-is.
476+
*
477+
* @param schema the schema that will have the default value
478+
* @param defaultValue the default value
479+
* @return the cast default value
480+
*/
481+
public static Object castDefaultValue(Schema schema, Object defaultValue) {
482+
if (schema != null && defaultValue != null) {
483+
PrimitiveType primitiveType = PrimitiveType.fromTypeAndFormat(schema.getType(), schema.getFormat());
484+
if (primitiveType != null) {
485+
Schema<?> primitiveSchema = primitiveType.createProperty();
486+
if (primitiveType.equals(PrimitiveType.DATE) && defaultValue instanceof LocalDate localDate) {
487+
defaultValue = localDate.toString();
488+
}
489+
primitiveSchema.setDefault(defaultValue);
490+
if (primitiveSchema.getDefault() != null) {
491+
defaultValue = primitiveSchema.getDefault();
492+
}
493+
}
494+
}
495+
return defaultValue;
496+
}
497+
471498
/**
472499
* Resolve default value object.
473500
*

springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app112/PersonController.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@
2424

2525
package test.org.springdoc.api.v30.app112;
2626

27+
import java.lang.annotation.Retention;
2728
import java.util.ArrayList;
2829
import java.util.List;
2930
import java.util.Random;
3031

3132
import jakarta.validation.Valid;
32-
import jakarta.validation.constraints.NotBlank;
33-
import jakarta.validation.constraints.NotNull;
34-
import jakarta.validation.constraints.Size;
33+
import jakarta.validation.constraints.*;
3534

35+
import org.hibernate.validator.constraints.Range;
3636
import org.springframework.validation.annotation.Validated;
3737
import org.springframework.web.bind.annotation.RequestBody;
3838
import org.springframework.web.bind.annotation.RequestMapping;
3939
import org.springframework.web.bind.annotation.RequestMethod;
4040
import org.springframework.web.bind.annotation.RequestParam;
4141
import org.springframework.web.bind.annotation.RestController;
4242

43+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
44+
4345
@RestController
4446
@Validated
4547
public class PersonController {
@@ -72,4 +74,21 @@ public List<Person> findByLastName(@RequestParam(name = "lastName", required = t
7274
return hardCoded;
7375

7476
}
77+
78+
@RequestMapping(path = "/persons", method = RequestMethod.GET)
79+
public List<Person> findPersons(
80+
@RequestParam(name = "setsOfShoes") @Range(min = 1, max = 4) int setsOfShoes,
81+
@RequestParam(name = "height") @Range(max = 200) int height,
82+
@RequestParam(name = "age") @Range(min = 2) int age,
83+
@RequestParam(name = "oneToTen") @ComposedInterfaceWithStaticDefinitions int oneToTen
84+
) {
85+
return List.of();
86+
87+
}
88+
89+
@Min(1)
90+
@Max(10)
91+
@Retention(RUNTIME)
92+
public @interface ComposedInterfaceWithStaticDefinitions {
93+
}
7594
}

springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app213/HelloController.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
name = "uri",
3232
value = "${api.extensions.properties.uri}/testcontroller/getTest"),
3333
@ExtensionProperty(name = "passthroughBehavior", value = "${api.extensions.properties.passthroughBehavior}"),
34-
@ExtensionProperty(name = "connectionType", value = "${api.extensions.properties.connectionType}") }) },
35-
34+
@ExtensionProperty(name = "connectionType", value = "${api.extensions.properties.connectionType}")
35+
})
36+
},
3637
title = "${api.info.title}",
3738
version = "${api.info.version}",
3839
description = "${api.info.description}",
@@ -57,7 +58,10 @@ public class HelloController {
5758
name = "uri",
5859
value = "${api.extensions.properties.uri}/testcontroller/getTest"),
5960
@ExtensionProperty(name = "passthroughBehavior", value = "${api.extensions.properties.passthroughBehavior}"),
60-
@ExtensionProperty(name = "connectionType", value = "${api.extensions.properties.connectionType}") }) })
61+
@ExtensionProperty(name = "connectionType", value = "${api.extensions.properties.connectionType}") ,
62+
@ExtensionProperty(name = "jsonProperty", parseValue = true, value = "{ \"key\": \"value\", \"key2\": \"value2\" }")
63+
})
64+
})
6165

6266
public PersonDTO queryMyDto() {
6367
// This return a PageImpl with the data, the method parameter 'query' is a pojo containg filter properties

0 commit comments

Comments
 (0)