diff --git a/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java b/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java index f47688e38..c1542b62f 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java +++ b/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java @@ -641,7 +641,7 @@ public Optional getPattern() { @Override public Number getMin() { - return getAnnotationAttribute(Size.class, "min", Number.class) // + return sizeAsNumericBound("min") // .or(() -> Optional.ofNullable(RANGE_ANNOTATION) .flatMap(it -> getAnnotationAttribute(it, "min", Number.class))) // .or(() -> getAnnotationAttribute(Min.class, "value", Number.class)) // @@ -657,7 +657,7 @@ public Number getMin() { @Override public Number getMax() { - return getAnnotationAttribute(Size.class, "max", Number.class) // + return sizeAsNumericBound("max") // .or(() -> Optional.ofNullable(RANGE_ANNOTATION) .flatMap(it -> getAnnotationAttribute(it, "max", Number.class))) // .or(() -> getAnnotationAttribute(Max.class, "value", Number.class)) // @@ -672,8 +672,10 @@ public Number getMax() { @Nullable @Override public Long getMinLength() { - return LENGTH_ANNOTATION.flatMap(it -> getAnnotationAttribute(it, "min", Integer.class)) // - .map(Integer::longValue) // + + return sizeAsLengthBound("min") // + .or(() -> LENGTH_ANNOTATION.flatMap(it -> getAnnotationAttribute(it, "min", Integer.class)) // + .map(Integer::longValue)) // .orElse(null); } @@ -684,8 +686,10 @@ public Long getMinLength() { @Nullable @Override public Long getMaxLength() { - return LENGTH_ANNOTATION.flatMap(it -> getAnnotationAttribute(it, "max", Integer.class)) // - .map(Integer::longValue) // + + return sizeAsLengthBound("max") // + .or(() -> LENGTH_ANNOTATION.flatMap(it -> getAnnotationAttribute(it, "max", Integer.class)) // + .map(Integer::longValue)) // .orElse(null); } @@ -737,6 +741,12 @@ private Optional parsePropertyAnnotationValue(Class { + // @Size constrains element/character count. For CharSequence properties that maps to a text + // input with minLength/maxLength rather than a numeric range control (GH-2531). + if (Size.class.equals(it.getKey()) && isCharSequenceProperty()) { + return Stream.empty(); + } + MergedAnnotation annotation = property.getAnnotation(it.getKey()); return annotation.isPresent() ? Stream.of(it.getValue()) : Stream.empty(); @@ -745,6 +755,36 @@ private Optional parsePropertyAnnotationValue(Class sizeAsNumericBound(String attribute) { + + if (isCharSequenceProperty()) { + return Optional.empty(); + } + + return getAnnotationAttribute(Size.class, attribute, Number.class); + } + + /** + * Map {@link Size} to {@code minLength}/{@code maxLength} for {@link CharSequence} properties (GH-2531). + */ + private Optional sizeAsLengthBound(String attribute) { + + if (!isCharSequenceProperty()) { + return Optional.empty(); + } + + return getAnnotationAttribute(Size.class, attribute, Number.class) // + .map(Number::longValue); + } + + private boolean isCharSequenceProperty() { + return CharSequence.class.isAssignableFrom(property.getType().resolve(Object.class)); + } + private Optional getAnnotationAttribute(Class annotation, String attribute, Class type) { diff --git a/src/test/java/org/springframework/hateoas/mediatype/PropertyUtilsTest.java b/src/test/java/org/springframework/hateoas/mediatype/PropertyUtilsTest.java index c5670fbd9..51fb47ea9 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/PropertyUtilsTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/PropertyUtilsTest.java @@ -238,6 +238,20 @@ void exposesMinAndMaxFromJsr303AtSizeAnnotation() { }); } + @Test // #2531 + void mapsSizeOnCharSequenceToMinAndMaxLength() { + + InputPayloadMetadata metadata = PropertyUtils.getExposedProperties(Jsr303SamplePayload.class); + + assertThat(getProperty(metadata, "named")).hasValueSatisfying(it -> { + assertThat(it.getInputType()).isEqualTo(HtmlInputType.TEXT_VALUE); + assertThat(it.getMinLength()).isEqualTo(0L); + assertThat(it.getMaxLength()).isEqualTo(256L); + assertThat(it.getMin()).isNull(); + assertThat(it.getMax()).isNull(); + }); + } + @Data @AllArgsConstructor @JsonIgnoreProperties({ "ignoreThisProperty" }) @@ -285,6 +299,7 @@ static class Jsr303SamplePayload { @NotBlank @Pattern(regexp = "\\w") String nonBlankPattern; TypeAnnotated annotated; @Size(min = 41, max = 4711) int sized; + @Size(max = 256) String named; } @Pattern(regexp = "regex")