#1439 - Support for HAL FORMS min, max, minLength and maxLength attributes.
Introduce support for the min, max, minLength and maxLength fields for HAL FORMS property descriptors. Extended PropertyMetadata to capture those based on JSR-303 annotations. Pulled up InputPayloadMetadata.createProperties(…) into AffordanceModel directly. Deprecated InputPayloadMetadata.applyTo(…).
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -895,6 +895,13 @@
|
||||
<version>2.0.1.Final</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
<version>6.1.7.Final</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -137,6 +137,22 @@ public abstract class AffordanceModel {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link List} of properties based on the given creator.
|
||||
*
|
||||
* @param <T> the property type
|
||||
* @param creator a creator function that turns an {@link InputPayloadMetadata} and {@link PropertyMetadata} into a
|
||||
* property instance.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public <T> List<T> createProperties(BiFunction<InputPayloadMetadata, PropertyMetadata, T> creator) {
|
||||
|
||||
return input.stream()
|
||||
.map(it -> creator.apply(input, it))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
@@ -208,39 +224,15 @@ public abstract class AffordanceModel {
|
||||
: DelegatingInputPayloadMetadata.of(metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link List} of properties based on the given creator and customizer. The {@link PropertyMetadata} will
|
||||
* be applied to the instance returned from the creator before its handed to the customizer.
|
||||
*
|
||||
* @param <T> the property type
|
||||
* @param creator a creator function that turns a {@link PropertyMetadata} into a property instance.
|
||||
* @param customizer a {@link BiFunction} to apply after the {@link PropertyMetadata} has been applied to the
|
||||
* property instance.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
default <T extends PropertyMetadataConfigured<T> & Named> List<T> createProperties(
|
||||
Function<PropertyMetadata, T> creator,
|
||||
BiFunction<T, PropertyMetadata, T> customizer) {
|
||||
|
||||
Assert.notNull(creator, "Creator must not be null!");
|
||||
Assert.notNull(customizer, "Customizer must not be null!");
|
||||
|
||||
return stream().map(creator).map(it -> {
|
||||
|
||||
return getPropertyMetadata(it.getName())
|
||||
.map(metadata -> customizer.apply(it.apply(metadata), metadata))
|
||||
.orElse(it);
|
||||
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the {@link InputPayloadMetadata} to the given target.
|
||||
*
|
||||
* @param <T>
|
||||
* @param target
|
||||
* @return
|
||||
* @deprecated since 1.3, prefer setting up the model types via {@link #createProperties(Function)}
|
||||
*/
|
||||
@Deprecated
|
||||
default <T extends PropertyMetadataConfigured<T> & Named> T applyTo(T target) {
|
||||
|
||||
return getPropertyMetadata(target.getName()) //
|
||||
@@ -396,6 +388,50 @@ public abstract class AffordanceModel {
|
||||
* @return
|
||||
*/
|
||||
ResolvableType getType();
|
||||
|
||||
/**
|
||||
* Return the minimum value allowed for a numeric type.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
@Nullable
|
||||
default Long getMin() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum value allowed for a numeric type.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
@Nullable
|
||||
default Long getMax() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum length allowed for a string type.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
@Nullable
|
||||
default Long getMinLength() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum length allowed for a string type.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
@Nullable
|
||||
default Long getMaxLength() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,21 +20,13 @@ import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
@@ -56,7 +48,6 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
@@ -512,6 +503,14 @@ public class PropertyUtils {
|
||||
*/
|
||||
private static class Jsr303AwarePropertyMetadata extends DefaultPropertyMetadata {
|
||||
|
||||
private static final Optional<Class<? extends Annotation>> LENGTH_ANNOTATION;
|
||||
|
||||
static {
|
||||
|
||||
LENGTH_ANNOTATION = Optional.ofNullable(org.springframework.hateoas.support.ClassUtils
|
||||
.loadIfPresent("org.hibernate.validator.constraints.Length"));
|
||||
}
|
||||
|
||||
private final AnnotatedProperty property;
|
||||
|
||||
/**
|
||||
@@ -541,24 +540,65 @@ public class PropertyUtils {
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getPattern() {
|
||||
|
||||
MergedAnnotation<Pattern> annotation = property.getAnnotation(Pattern.class);
|
||||
|
||||
if (annotation.isPresent()) {
|
||||
return fromAnnotation(annotation);
|
||||
}
|
||||
|
||||
annotation = property.getTypeAnnotations().get(Pattern.class);
|
||||
|
||||
return annotation.isPresent() //
|
||||
? fromAnnotation(annotation) //
|
||||
: Optional.empty();
|
||||
return getAnnotationAttribute(Pattern.class, "regexp", String.class);
|
||||
}
|
||||
|
||||
private static Optional<String> fromAnnotation(MergedAnnotation<Pattern> annotation) {
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel.PropertyMetadata#getMin()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Long getMin() {
|
||||
return getAnnotationAttribute(Min.class, "value", Long.class).orElse(null);
|
||||
}
|
||||
|
||||
return Optional.of(annotation.getString("regexp")) //
|
||||
.filter(StringUtils::hasText);
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel.PropertyMetadata#getMax()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Long getMax() {
|
||||
return getAnnotationAttribute(Max.class, "value", Long.class).orElse(null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel.PropertyMetadata#getMinLength()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Long getMinLength() {
|
||||
return LENGTH_ANNOTATION.flatMap(it -> getAnnotationAttribute(it, "min", Integer.class))
|
||||
.map(Integer::longValue)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel.PropertyMetadata#getMaxLength()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Long getMaxLength() {
|
||||
return LENGTH_ANNOTATION.flatMap(it -> getAnnotationAttribute(it, "max", Integer.class))
|
||||
.map(Integer::longValue)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private <T> Optional<T> getAnnotationAttribute(Class<? extends Annotation> annotation, String attribute,
|
||||
Class<T> type) {
|
||||
|
||||
MergedAnnotation<? extends Annotation> mergedAnnotation = property.getAnnotation(annotation);
|
||||
|
||||
if (mergedAnnotation.isPresent()) {
|
||||
return mergedAnnotation.getValue(attribute, type);
|
||||
}
|
||||
|
||||
mergedAnnotation = property.getTypeAnnotations().get(annotation);
|
||||
|
||||
return mergedAnnotation.isPresent() ? mergedAnnotation.getValue(attribute, type) : Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,13 @@
|
||||
*/
|
||||
package org.springframework.hateoas.mediatype.hal.forms;
|
||||
|
||||
import static org.springframework.http.HttpMethod.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link AffordanceModel} for a HAL-FORMS {@link MediaType}.
|
||||
@@ -45,121 +31,8 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
class HalFormsAffordanceModel extends AffordanceModel {
|
||||
|
||||
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(POST, PUT, PATCH);
|
||||
|
||||
public HalFormsAffordanceModel(String name, Link link, HttpMethod httpMethod, InputPayloadMetadata inputType,
|
||||
List<QueryParameter> queryMethodParameters, PayloadMetadata outputType) {
|
||||
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given customizer to all {@link HalFormsProperty} of this model.
|
||||
*
|
||||
* @param customizer must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public List<HalFormsProperty> getProperties(HalFormsConfiguration configuration, MessageResolver resolver) {
|
||||
|
||||
if (!ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Function<PropertyMetadata, HalFormsProperty> creator = it -> {
|
||||
|
||||
HalFormsProperty property = new HalFormsProperty().withName(it.getName());
|
||||
|
||||
return configuration.getTypePatternFor(it.getType()) //
|
||||
.map(property::withRegex) //
|
||||
.orElse(property);
|
||||
};
|
||||
|
||||
return getInput().createProperties(creator, (property, metadata) -> {
|
||||
|
||||
return Optional.of(property)
|
||||
.map(it -> apply(it, I18nedPlaceholder::of, it::withPlaceholder, resolver))
|
||||
.map(it -> apply(it, I18nedPropertyPrompt::of, it::withPrompt, resolver))
|
||||
.map(it -> hasHttpMethod(HttpMethod.PATCH) ? it.withRequired(false) : it)
|
||||
.orElse(property);
|
||||
});
|
||||
}
|
||||
|
||||
private HalFormsProperty apply(HalFormsProperty property,
|
||||
BiFunction<InputPayloadMetadata, HalFormsProperty, I18nedPropertyMetadata> creator,
|
||||
Function<String, HalFormsProperty> application, MessageResolver resolver) {
|
||||
|
||||
InputPayloadMetadata metadata = getInput();
|
||||
I18nedPropertyMetadata source = creator.apply(metadata, property);
|
||||
String resolved = resolver.resolve(source);
|
||||
|
||||
return !StringUtils.hasText(resolved)
|
||||
? property
|
||||
: application.apply(resolved);
|
||||
}
|
||||
|
||||
private static class I18nedPropertyMetadata implements MessageSourceResolvable {
|
||||
|
||||
private final String template;
|
||||
private final InputPayloadMetadata metadata;
|
||||
private final HalFormsProperty property;
|
||||
|
||||
protected I18nedPropertyMetadata(String template, InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
|
||||
this.template = template;
|
||||
this.metadata = metadata;
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.MessageSourceResolvable#getDefaultMessage()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public String getDefaultMessage() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.MessageSourceResolvable#getCodes()
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public String[] getCodes() {
|
||||
|
||||
String globalCode = String.format(template, property.getName());
|
||||
|
||||
List<String> codes = new ArrayList<>();
|
||||
|
||||
metadata.getI18nCodes().stream() //
|
||||
.map(it -> String.format("%s.%s", it, globalCode)) //
|
||||
.forEach(codes::add);
|
||||
|
||||
codes.add(globalCode);
|
||||
|
||||
return codes.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private static class I18nedPropertyPrompt extends I18nedPropertyMetadata {
|
||||
|
||||
private I18nedPropertyPrompt(InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
super("%s._prompt", metadata, property);
|
||||
}
|
||||
|
||||
public static I18nedPropertyPrompt of(InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
return new I18nedPropertyPrompt(metadata, property);
|
||||
}
|
||||
}
|
||||
|
||||
private static class I18nedPlaceholder extends I18nedPropertyMetadata {
|
||||
|
||||
private I18nedPlaceholder(InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
super("%s._placeholder", metadata, property);
|
||||
}
|
||||
|
||||
public static I18nedPlaceholder of(InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
return new I18nedPlaceholder(metadata, property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
package org.springframework.hateoas.mediatype.hal.forms;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.hateoas.AffordanceModel.Named;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadata;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadataConfigured;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -36,11 +35,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
* @see https://mamund.site44.com/misc/hal-forms/
|
||||
*/
|
||||
@JsonInclude(Include.NON_DEFAULT)
|
||||
final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsProperty>, Named {
|
||||
final class HalFormsProperty implements Named {
|
||||
|
||||
private final String name, value, prompt, regex, placeholder;
|
||||
private final boolean templated, multi;
|
||||
private final @JsonInclude(Include.NON_DEFAULT) boolean readOnly, required;
|
||||
private final @Nullable Long min, max, minLength, maxLength;
|
||||
|
||||
HalFormsProperty() {
|
||||
|
||||
@@ -53,10 +53,15 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
this.required = false;
|
||||
this.multi = false;
|
||||
this.placeholder = null;
|
||||
this.min = null;
|
||||
this.max = null;
|
||||
this.minLength = null;
|
||||
this.maxLength = null;
|
||||
}
|
||||
|
||||
private HalFormsProperty(String name, boolean readOnly, String value, String prompt, String regex, boolean templated,
|
||||
boolean required, boolean multi, String placeholder) {
|
||||
boolean required, boolean multi, String placeholder, @Nullable Long min, @Nullable Long max,
|
||||
@Nullable Long minLength, @Nullable Long maxLength) {
|
||||
|
||||
Assert.notNull(name, "name must not be null!");
|
||||
|
||||
@@ -69,6 +74,10 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
this.required = required;
|
||||
this.multi = multi;
|
||||
this.placeholder = StringUtils.hasText(placeholder) ? placeholder : null;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.minLength = minLength;
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,20 +90,6 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
return new HalFormsProperty().withName(name);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.PropertyMetadataAware#apply(org.springframework.hateoas.mediatype.PropertyUtils.PropertyMetadata)
|
||||
*/
|
||||
public HalFormsProperty apply(PropertyMetadata metadata) {
|
||||
|
||||
HalFormsProperty customized = withRequired(metadata.isRequired()) //
|
||||
.withReadOnly(metadata.isReadOnly());
|
||||
|
||||
return metadata.getPattern() //
|
||||
.map(customized::withRegex) //
|
||||
.orElse(customized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing the {@literal name}.
|
||||
*
|
||||
@@ -107,7 +102,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.name == name ? this
|
||||
: new HalFormsProperty(name, this.readOnly, this.value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +115,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.readOnly == readOnly ? this
|
||||
: new HalFormsProperty(this.name, readOnly, this.value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +128,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.value == value ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +141,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.prompt == prompt ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,7 +154,17 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.regex == regex ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, regex, this.templated, this.required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing the {@literal regex}.
|
||||
*
|
||||
* @param regex
|
||||
* @return
|
||||
*/
|
||||
HalFormsProperty withRegex(Optional<String> regex) {
|
||||
return regex.map(it -> withRegex(it)).orElse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +177,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.templated == templated ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, templated, this.required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +190,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.required == required ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated, required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +203,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.multi == multi ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, multi, this.placeholder);
|
||||
this.required, multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,9 +216,65 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
|
||||
return this.placeholder == placeholder ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, placeholder);
|
||||
this.required, this.multi, placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal min}.
|
||||
*
|
||||
* @param min
|
||||
* @return
|
||||
*/
|
||||
HalFormsProperty withMin(@Nullable Long min) {
|
||||
|
||||
return Objects.equals(this.min, min) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal max}.
|
||||
*
|
||||
* @param max
|
||||
* @return
|
||||
*/
|
||||
HalFormsProperty withMax(@Nullable Long max) {
|
||||
|
||||
return Objects.equals(this.max, max) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal minLength}.
|
||||
*
|
||||
* @param minLength
|
||||
* @return
|
||||
*/
|
||||
HalFormsProperty withMinLength(@Nullable Long minLength) {
|
||||
|
||||
return Objects.equals(this.minLength, minLength) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal maxLength}.
|
||||
*
|
||||
* @param maxLength
|
||||
* @return
|
||||
*/
|
||||
HalFormsProperty withMaxLength(@Nullable Long maxLength) {
|
||||
|
||||
return Objects.equals(this.maxLength, maxLength) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, this.minLength, maxLength);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel.Named#getName()
|
||||
*/
|
||||
@JsonProperty
|
||||
public String getName() {
|
||||
return this.name;
|
||||
@@ -264,6 +325,42 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
return this.placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the min
|
||||
*/
|
||||
@Nullable
|
||||
@JsonProperty
|
||||
public Long getMin() {
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the max
|
||||
*/
|
||||
@Nullable
|
||||
@JsonProperty
|
||||
public Long getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the minLength
|
||||
*/
|
||||
@Nullable
|
||||
@JsonProperty
|
||||
public Long getMinLength() {
|
||||
return minLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maxLength
|
||||
*/
|
||||
@Nullable
|
||||
@JsonProperty
|
||||
public Long getMaxLength() {
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
@@ -298,7 +395,7 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
public int hashCode() {
|
||||
|
||||
return Objects.hash(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -316,6 +413,11 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
+ ", templated=" + this.templated //
|
||||
+ ", required=" + this.required //
|
||||
+ ", multi=" + this.multi //
|
||||
+ ", placeholder=" + this.placeholder + ")";
|
||||
+ ", placeholder=" + this.placeholder //
|
||||
+ ", min=" + this.min //
|
||||
+ ", max=" + this.max //
|
||||
+ ", minLength=" + this.minLength //
|
||||
+ ", maxLength=" + this.maxLength //
|
||||
+ ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.mediatype.hal.forms;
|
||||
|
||||
import static org.springframework.http.HttpMethod.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadata;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Factory to create {@link HalFormsProperty} instances.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.3
|
||||
* @soundtrack The Chicks - March March (Gaslighter)
|
||||
*/
|
||||
public class HalFormsPropertyFactory {
|
||||
|
||||
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(POST, PUT, PATCH);
|
||||
|
||||
private final HalFormsConfiguration configuration;
|
||||
private final MessageResolver resolver;
|
||||
|
||||
/**
|
||||
* Creates a new {@link HalFormsPropertyFactory} for the given {@link HalFormsConfiguration} and
|
||||
* {@link MessageResolver}.
|
||||
*
|
||||
* @param configuration must not be {@literal null}.
|
||||
* @param resolver must not be {@literal null}.
|
||||
*/
|
||||
public HalFormsPropertyFactory(HalFormsConfiguration configuration, MessageResolver resolver) {
|
||||
|
||||
Assert.notNull(configuration, "HalFormsConfiguration must not be null!");
|
||||
Assert.notNull(resolver, "MessageResolver must not be null!");
|
||||
|
||||
this.configuration = configuration;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link HalFormsProperty} from the given {@link HalFormsAffordanceModel}.
|
||||
*
|
||||
* @param model must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public List<HalFormsProperty> createProperties(HalFormsAffordanceModel model) {
|
||||
|
||||
Assert.notNull(model, "HalFormsModel must not be null!");
|
||||
|
||||
if (!ENTITY_ALTERING_METHODS.contains(model.getHttpMethod())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return model.createProperties((payload, metadata) -> {
|
||||
|
||||
HalFormsProperty property = new HalFormsProperty()
|
||||
.withName(metadata.getName())
|
||||
.withRequired(metadata.isRequired()) //
|
||||
.withReadOnly(metadata.isReadOnly())
|
||||
.withMin(metadata.getMin())
|
||||
.withMax(metadata.getMax())
|
||||
.withMinLength(metadata.getMinLength())
|
||||
.withMaxLength(metadata.getMaxLength())
|
||||
.withRegex(lookupRegex(metadata));
|
||||
|
||||
Function<String, I18nedPropertyMetadata> factory = I18nedPropertyMetadata.factory(payload, property);
|
||||
|
||||
return Optional.of(property)
|
||||
.map(it -> i18n(it, factory.apply("_placeholder"), it::withPlaceholder))
|
||||
.map(it -> i18n(it, factory.apply("_prompt"), it::withPrompt))
|
||||
.map(it -> model.hasHttpMethod(HttpMethod.PATCH) ? it.withRequired(false) : it)
|
||||
.orElse(property);
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<String> lookupRegex(PropertyMetadata metadata) {
|
||||
|
||||
Optional<String> pattern = metadata.getPattern();
|
||||
|
||||
if (pattern.isPresent()) {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
return configuration.getTypePatternFor(metadata.getType());
|
||||
}
|
||||
|
||||
private HalFormsProperty i18n(HalFormsProperty property, MessageSourceResolvable metadata,
|
||||
Function<String, HalFormsProperty> application) {
|
||||
|
||||
String resolved = resolver.resolve(metadata);
|
||||
|
||||
return !StringUtils.hasText(resolved)
|
||||
? property
|
||||
: application.apply(resolved);
|
||||
}
|
||||
|
||||
private static class I18nedPropertyMetadata implements MessageSourceResolvable {
|
||||
|
||||
private final String template;
|
||||
private final InputPayloadMetadata metadata;
|
||||
private final HalFormsProperty property;
|
||||
|
||||
private I18nedPropertyMetadata(String template, InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
|
||||
this.template = template;
|
||||
this.metadata = metadata;
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
public static Function<String, I18nedPropertyMetadata> factory(InputPayloadMetadata metadata,
|
||||
HalFormsProperty property) {
|
||||
return suffix -> new I18nedPropertyMetadata("%s.".concat(suffix), metadata, property);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.MessageSourceResolvable#getDefaultMessage()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public String getDefaultMessage() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.MessageSourceResolvable#getCodes()
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public String[] getCodes() {
|
||||
|
||||
String globalCode = String.format(template, property.getName());
|
||||
|
||||
List<String> codes = new ArrayList<>();
|
||||
|
||||
metadata.getI18nCodes().stream() //
|
||||
.map(it -> String.format("%s.%s", it, globalCode)) //
|
||||
.forEach(codes::add);
|
||||
|
||||
codes.add(globalCode);
|
||||
|
||||
return codes.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,13 @@ import org.springframework.util.Assert;
|
||||
|
||||
class HalFormsTemplateBuilder {
|
||||
|
||||
private final HalFormsConfiguration configuration;
|
||||
private final MessageResolver resolver;
|
||||
private final HalFormsPropertyFactory factory;
|
||||
|
||||
public HalFormsTemplateBuilder(HalFormsConfiguration configuration, MessageResolver resolver) {
|
||||
|
||||
this.configuration = configuration;
|
||||
this.resolver = resolver;
|
||||
this.factory = new HalFormsPropertyFactory(configuration, resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +73,7 @@ class HalFormsTemplateBuilder {
|
||||
.forEach(it -> {
|
||||
|
||||
HalFormsTemplate template = HalFormsTemplate.forMethod(it.getHttpMethod()) //
|
||||
.withProperties(it.getProperties(configuration, resolver));
|
||||
.withProperties(factory.createProperties(it));
|
||||
|
||||
template = applyTo(template, TemplateTitle.of(it, templates.isEmpty()));
|
||||
templates.put(templates.isEmpty() ? "default" : it.getName(), template);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.support;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class ClassUtils {
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> Class<T> loadIfPresent(String type) {
|
||||
|
||||
try {
|
||||
return (Class<T>) org.springframework.util.ClassUtils.forName(type,
|
||||
org.springframework.hateoas.support.ClassUtils.class.getClassLoader());
|
||||
} catch (ClassNotFoundException | LinkageError e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,14 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
@@ -66,10 +70,6 @@ class HalFormsTemplateBuilderUnitTest {
|
||||
@Test
|
||||
void allPropertiesAreOptionalForPatchRequests() throws Exception {
|
||||
|
||||
Affordances.of(Link.of("/example")) //
|
||||
.afford(HttpMethod.PATCH) //
|
||||
.withInput(RequiredProperty.class);
|
||||
|
||||
RequiredProperty model = new RequiredProperty();
|
||||
model.add(Affordances.of(Link.of("/example")) //
|
||||
.afford(HttpMethod.PATCH) //
|
||||
@@ -95,6 +95,26 @@ class HalFormsTemplateBuilderUnitTest {
|
||||
assertThat(template.getPropertyByName("name").map(HalFormsProperty::isRequired)).hasValue(true);
|
||||
}
|
||||
|
||||
@Test // #1439
|
||||
void considersMinandMaxAnnotations() {
|
||||
|
||||
Link link = Affordances.of(Link.of("/example")) //
|
||||
.afford(HttpMethod.POST) //
|
||||
.withInput(Payload.class) //
|
||||
.toLink();
|
||||
|
||||
HalFormsTemplate template = new HalFormsTemplateBuilder(new HalFormsConfiguration(),
|
||||
MessageResolver.DEFAULTS_ONLY).findTemplates(new RepresentationModel<>().add(link)).get("default");
|
||||
|
||||
Optional<HalFormsProperty> name = template.getPropertyByName("number");
|
||||
assertThat(name).map(HalFormsProperty::getMin).hasValue(2L);
|
||||
assertThat(name).map(HalFormsProperty::getMax).hasValue(5L);
|
||||
|
||||
Optional<HalFormsProperty> text = template.getPropertyByName("text");
|
||||
assertThat(text).map(HalFormsProperty::getMinLength).hasValue(2L);
|
||||
assertThat(text).map(HalFormsProperty::getMaxLength).hasValue(5L);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class PatternExample extends RepresentationModel<PatternExample> {
|
||||
|
||||
@@ -114,4 +134,15 @@ class HalFormsTemplateBuilderUnitTest {
|
||||
static class RequiredProperty extends RepresentationModel<RequiredProperty> {
|
||||
@NotNull String name;
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Payload {
|
||||
|
||||
@Min(2) //
|
||||
@Max(5) //
|
||||
Integer number;
|
||||
|
||||
@Length(min = 2, max = 5) //
|
||||
String text;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user