#1438 - Support for HAL FORMS' placeholder template properties.
See the spec [0] for details. Significant rework of how an AffordanceModel and HalFormsAffordanceModel in particular translates into creating a representation that matches the affordance (see InputPayloadMetadata.createProperties(…)). [0] https://rwcbook.github.io/hal-forms/#_code_placeholder_code
This commit is contained in:
@@ -19,11 +19,14 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -134,20 +137,35 @@ public abstract class AffordanceModel {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o)
|
||||
if (this == o) {
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AffordanceModel that = (AffordanceModel) o;
|
||||
return Objects.equals(this.name, that.name) && Objects.equals(this.link, that.link)
|
||||
&& this.httpMethod == that.httpMethod && Objects.equals(this.input, that.input)
|
||||
&& Objects.equals(this.queryMethodParameters, that.queryMethodParameters)
|
||||
|
||||
return Objects.equals(this.name, that.name) //
|
||||
&& Objects.equals(this.link, that.link) //
|
||||
&& this.httpMethod == that.httpMethod //
|
||||
&& Objects.equals(this.input, that.input) //
|
||||
&& Objects.equals(this.queryMethodParameters, that.queryMethodParameters) //
|
||||
&& Objects.equals(this.output, that.output);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.name, this.link, this.httpMethod, this.input, this.queryMethodParameters, this.output);
|
||||
@@ -190,6 +208,32 @@ 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.
|
||||
*
|
||||
@@ -197,7 +241,12 @@ public abstract class AffordanceModel {
|
||||
* @param target
|
||||
* @return
|
||||
*/
|
||||
<T extends PropertyMetadataConfigured<T> & Named> T applyTo(T target);
|
||||
default <T extends PropertyMetadataConfigured<T> & Named> T applyTo(T target) {
|
||||
|
||||
return getPropertyMetadata(target.getName()) //
|
||||
.map(it -> target.apply(it)) //
|
||||
.orElse(target);
|
||||
}
|
||||
|
||||
<T extends Named> T customize(T target, Function<PropertyMetadata, T> customizer);
|
||||
|
||||
@@ -262,14 +311,23 @@ public abstract class AffordanceModel {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o)
|
||||
if (this == o) {
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DelegatingInputPayloadMetadata that = (DelegatingInputPayloadMetadata) o;
|
||||
|
||||
return Objects.equals(this.metadata, that.metadata);
|
||||
}
|
||||
|
||||
@@ -278,6 +336,7 @@ public abstract class AffordanceModel {
|
||||
return Objects.hash(this.metadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AffordanceModel.DelegatingInputPayloadMetadata(metadata=" + this.metadata + ")";
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata;
|
||||
import org.springframework.hateoas.AffordanceModel.Named;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadata;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadataConfigured;
|
||||
|
||||
/**
|
||||
* {@link InputPayloadMetadata} implementation based on a Java type.
|
||||
@@ -46,18 +45,6 @@ class TypeBasedPayloadMetadata implements InputPayloadMetadata {
|
||||
properties.collect(Collectors.toMap(PropertyMetadata::getName, Function.identity())));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.PayloadMetadata#customize(T)
|
||||
*/
|
||||
@Override
|
||||
public <T extends PropertyMetadataConfigured<T> & Named> T applyTo(T target) {
|
||||
|
||||
PropertyMetadata metadata = this.properties.get(target.getName());
|
||||
|
||||
return metadata == null ? target : target.apply(metadata);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel.PayloadMetadata#customize(org.springframework.hateoas.AffordanceModel.Named, java.util.function.Function)
|
||||
|
||||
@@ -17,19 +17,25 @@ 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.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
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}.
|
||||
@@ -41,52 +47,119 @@ class HalFormsAffordanceModel extends AffordanceModel {
|
||||
|
||||
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(POST, PUT, PATCH);
|
||||
|
||||
private final List<HalFormsProperty> inputProperties;
|
||||
|
||||
public HalFormsAffordanceModel(String name, Link link, HttpMethod httpMethod, InputPayloadMetadata inputType,
|
||||
List<QueryParameter> queryMethodParameters, PayloadMetadata outputType) {
|
||||
|
||||
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
|
||||
this.inputProperties = determineInputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the input's domain type to extract the {@link Affordance}'s properties. Then transform them into a list of
|
||||
* {@link HalFormsProperty} objects.
|
||||
* Applies the given customizer to all {@link HalFormsProperty} of this model.
|
||||
*
|
||||
* @param customizer must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private List<HalFormsProperty> determineInputs() {
|
||||
public List<HalFormsProperty> getProperties(HalFormsConfiguration configuration, MessageResolver resolver) {
|
||||
|
||||
if (!ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return getInput().stream() //
|
||||
.map(PropertyMetadata::getName) //
|
||||
.map(it -> new HalFormsProperty() //
|
||||
.withName(it)) //
|
||||
.collect(Collectors.toList());
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
public List<HalFormsProperty> getInputProperties() {
|
||||
return this.inputProperties;
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
private static class I18nedPropertyMetadata implements MessageSourceResolvable {
|
||||
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof HalFormsAffordanceModel))
|
||||
return false;
|
||||
if (!super.equals(o))
|
||||
return false;
|
||||
HalFormsAffordanceModel that = (HalFormsAffordanceModel) o;
|
||||
return Objects.equals(this.inputProperties, that.inputProperties);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), inputProperties);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,11 @@ import java.util.Objects;
|
||||
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;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
@@ -35,14 +38,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
@JsonInclude(Include.NON_DEFAULT)
|
||||
final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsProperty>, Named {
|
||||
|
||||
private final String name;
|
||||
private @JsonInclude(Include.NON_DEFAULT) final boolean readOnly;
|
||||
private final String value;
|
||||
private @JsonInclude(Include.NON_EMPTY) final String prompt;
|
||||
private final String regex;
|
||||
private final boolean templated;
|
||||
private @JsonInclude(Include.NON_DEFAULT) final boolean required;
|
||||
private final boolean multi;
|
||||
private final String name, value, prompt, regex, placeholder;
|
||||
private final boolean templated, multi;
|
||||
private final @JsonInclude(Include.NON_DEFAULT) boolean readOnly, required;
|
||||
|
||||
HalFormsProperty() {
|
||||
|
||||
@@ -54,21 +52,23 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
this.templated = false;
|
||||
this.required = false;
|
||||
this.multi = false;
|
||||
this.placeholder = null;
|
||||
}
|
||||
|
||||
private HalFormsProperty(String name, boolean readOnly, String value, String prompt, String regex, boolean templated,
|
||||
boolean required, boolean multi) {
|
||||
boolean required, boolean multi, String placeholder) {
|
||||
|
||||
Assert.notNull(name, "name must not be null!");
|
||||
|
||||
this.name = name;
|
||||
this.readOnly = readOnly;
|
||||
this.value = value;
|
||||
this.prompt = prompt;
|
||||
this.prompt = StringUtils.hasText(prompt) ? prompt : null;
|
||||
this.regex = regex;
|
||||
this.templated = templated;
|
||||
this.required = required;
|
||||
this.multi = multi;
|
||||
this.placeholder = StringUtils.hasText(placeholder) ? placeholder : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +107,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.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +120,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.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +133,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.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +146,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.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,7 +159,7 @@ 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.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +172,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.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +185,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.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +198,20 @@ 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.required, multi, this.placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal placeholder}.
|
||||
*
|
||||
* @param placeholder
|
||||
* @return
|
||||
*/
|
||||
HalFormsProperty withPlaceholder(String placeholder) {
|
||||
|
||||
return this.placeholder == placeholder ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, placeholder);
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@@ -226,6 +239,11 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
return this.regex;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
boolean hasRegex() {
|
||||
return StringUtils.hasText(regex);
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
boolean isTemplated() {
|
||||
return this.templated;
|
||||
@@ -241,30 +259,63 @@ final class HalFormsProperty implements PropertyMetadataConfigured<HalFormsPrope
|
||||
return this.multi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof HalFormsProperty))
|
||||
return false;
|
||||
HalFormsProperty that = (HalFormsProperty) o;
|
||||
return this.readOnly == that.readOnly && this.templated == that.templated && this.required == that.required
|
||||
&& this.multi == that.multi && Objects.equals(this.name, that.name) && Objects.equals(this.value, that.value)
|
||||
&& Objects.equals(this.prompt, that.prompt) && Objects.equals(this.regex, that.regex);
|
||||
@JsonProperty
|
||||
public String getPlaceholder() {
|
||||
return this.placeholder;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof HalFormsProperty)) {
|
||||
return false;
|
||||
}
|
||||
HalFormsProperty that = (HalFormsProperty) o;
|
||||
|
||||
return this.readOnly == that.readOnly //
|
||||
&& this.templated == that.templated //
|
||||
&& this.required == that.required //
|
||||
&& this.multi == that.multi //
|
||||
&& Objects.equals(this.name, that.name) //
|
||||
&& Objects.equals(this.value, that.value) //
|
||||
&& Objects.equals(this.prompt, that.prompt) //
|
||||
&& Objects.equals(this.regex, that.regex) //
|
||||
&& Objects.equals(this.placeholder, that.placeholder);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
return Objects.hash(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi);
|
||||
this.multi, this.placeholder);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "HalFormsProperty(name=" + this.name + ", readOnly=" + this.readOnly + ", value=" + this.value + ", prompt="
|
||||
+ this.prompt + ", regex=" + this.regex + ", templated=" + this.templated + ", required=" + this.required
|
||||
+ ", multi=" + this.multi + ")";
|
||||
return "HalFormsProperty(name=" + this.name //
|
||||
+ ", readOnly=" + this.readOnly //
|
||||
+ ", value=" + this.value //
|
||||
+ ", prompt=" + this.prompt //
|
||||
+ ", regex=" + this.regex //
|
||||
+ ", templated=" + this.templated //
|
||||
+ ", required=" + this.required //
|
||||
+ ", multi=" + this.multi //
|
||||
+ ", placeholder=" + this.placeholder + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,23 +174,43 @@ final class HalFormsTemplate {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o)
|
||||
if (this == o) {
|
||||
return true;
|
||||
if (!(o instanceof HalFormsTemplate))
|
||||
}
|
||||
|
||||
if (!(o instanceof HalFormsTemplate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HalFormsTemplate that = (HalFormsTemplate) o;
|
||||
return Objects.equals(this.title, that.title) && this.httpMethod == that.httpMethod
|
||||
&& Objects.equals(this.properties, that.properties) && Objects.equals(this.contentTypes, that.contentTypes);
|
||||
|
||||
return Objects.equals(this.title, that.title) //
|
||||
&& this.httpMethod == that.httpMethod //
|
||||
&& Objects.equals(this.properties, that.properties) //
|
||||
&& Objects.equals(this.contentTypes, that.contentTypes);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.title, this.httpMethod, this.properties, this.contentTypes);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HalFormsTemplate(title=" + this.title + ", httpMethod=" + this.httpMethod + ", properties="
|
||||
+ this.properties + ", contentTypes=" + this.contentTypes + ")";
|
||||
|
||||
@@ -15,19 +15,16 @@
|
||||
*/
|
||||
package org.springframework.hateoas.mediatype.hal.forms;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadata;
|
||||
import org.springframework.hateoas.IanaLinkRelations;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
@@ -75,15 +72,8 @@ class HalFormsTemplateBuilder {
|
||||
.filter(it -> !it.hasHttpMethod(HttpMethod.GET)) //
|
||||
.forEach(it -> {
|
||||
|
||||
PropertyCustomizations propertyCustomizations = forMetadata(it.getInput());
|
||||
|
||||
List<HalFormsProperty> propertiesWithPrompt = it.getInputProperties().stream() //
|
||||
.map(property -> propertyCustomizations.apply(property)) //
|
||||
.map(property -> it.hasHttpMethod(HttpMethod.PATCH) ? property.withRequired(false) : property)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
HalFormsTemplate template = HalFormsTemplate.forMethod(it.getHttpMethod()) //
|
||||
.withProperties(propertiesWithPrompt);
|
||||
.withProperties(it.getProperties(configuration, resolver));
|
||||
|
||||
template = applyTo(template, TemplateTitle.of(it, templates.isEmpty()));
|
||||
templates.put(templates.isEmpty() ? "default" : it.getName(), template);
|
||||
@@ -92,46 +82,14 @@ class HalFormsTemplateBuilder {
|
||||
return templates;
|
||||
}
|
||||
|
||||
public PropertyCustomizations forMetadata(InputPayloadMetadata metadata) {
|
||||
return new PropertyCustomizations(metadata);
|
||||
}
|
||||
|
||||
public HalFormsTemplate applyTo(HalFormsTemplate template, HalFormsTemplateBuilder.TemplateTitle templateTitle) {
|
||||
private HalFormsTemplate applyTo(HalFormsTemplate template, HalFormsTemplateBuilder.TemplateTitle templateTitle) {
|
||||
|
||||
return Optional.ofNullable(resolver.resolve(templateTitle)) //
|
||||
.map(template::withTitle) //
|
||||
.orElse(template);
|
||||
}
|
||||
|
||||
class PropertyCustomizations {
|
||||
|
||||
private final InputPayloadMetadata metadata;
|
||||
|
||||
public PropertyCustomizations(InputPayloadMetadata metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
private HalFormsProperty apply(HalFormsProperty property) {
|
||||
|
||||
String message = resolver.resolve(PropertyPrompt.of(metadata, property));
|
||||
|
||||
HalFormsProperty withPrompt = Optional.ofNullable(message) //
|
||||
.map(it -> property.withPrompt(it)) //
|
||||
.orElse(property);
|
||||
|
||||
HalFormsProperty withConfig = metadata.getPropertyMetadata(withPrompt.getName()) //
|
||||
.flatMap(it -> applyConfig(it, withPrompt)) //
|
||||
.orElse(withPrompt);
|
||||
|
||||
return metadata.applyTo(withConfig);
|
||||
}
|
||||
|
||||
private Optional<HalFormsProperty> applyConfig(PropertyMetadata metadata, HalFormsProperty property) {
|
||||
return configuration.getTypePatternFor(metadata.getType()).map(property::withRegex);
|
||||
}
|
||||
}
|
||||
|
||||
static class TemplateTitle implements MessageSourceResolvable {
|
||||
private static class TemplateTitle implements MessageSourceResolvable {
|
||||
|
||||
private static final String TEMPLATE_TEMPLATE = "_templates.%s.title";
|
||||
|
||||
@@ -184,53 +142,4 @@ class HalFormsTemplateBuilder {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static class PropertyPrompt implements MessageSourceResolvable {
|
||||
|
||||
private static final String PROMPT_TEMPLATE = "%s._prompt";
|
||||
|
||||
private final InputPayloadMetadata metadata;
|
||||
private final HalFormsProperty property;
|
||||
|
||||
private PropertyPrompt(InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
|
||||
this.metadata = metadata;
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
public static PropertyPrompt of(InputPayloadMetadata metadata, HalFormsProperty property) {
|
||||
return new PropertyPrompt(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(PROMPT_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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ class HalFormsTemplateBuilderUnitTest {
|
||||
@CsvSource({ "number, [0-9]{16}", "overridden, foo", "annotated, bar" })
|
||||
void detectsRegularExpressionsOnProperties(String propertyName, String expected) {
|
||||
|
||||
HalFormsConfiguration configuration = new HalFormsConfiguration();
|
||||
configuration.registerPattern(CreditCardNumber.class, "[0-9]{16}");
|
||||
HalFormsConfiguration configuration = new HalFormsConfiguration() //
|
||||
.withPattern(CreditCardNumber.class, "[0-9]{16}");
|
||||
|
||||
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, MessageResolver.DEFAULTS_ONLY);
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
|
||||
@BeforeEach
|
||||
void setUpModule() {
|
||||
|
||||
LocaleContextHolder.setLocale(Locale.US);
|
||||
|
||||
LinkRelationProvider provider = new DelegatingLinkRelationProvider(new AnnotationLinkRelationProvider(),
|
||||
HalTestUtils.DefaultLinkRelationProvider.INSTANCE);
|
||||
|
||||
@@ -528,6 +530,33 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
|
||||
assertValueForPath(unwrappedExample, "$.firstname", "john");
|
||||
}
|
||||
|
||||
@ParameterizedTest // #1438
|
||||
@ValueSource(strings = { "firstname._placeholder", //
|
||||
"HalFormsPayload.firstname._placeholder", //
|
||||
"org.springframework.hateoas.mediatype.hal.forms.Jackson2HalFormsIntegrationTest$HalFormsPayload.firstname._placeholder" })
|
||||
void usesResourceBundleToCreatePropertyPlaceholder(String key) {
|
||||
|
||||
StaticMessageSource source = new StaticMessageSource();
|
||||
source.addMessage(key, Locale.US, "Property placeholder");
|
||||
|
||||
Link link = Affordances.of(Link.of("some:link")) //
|
||||
.afford(HttpMethod.POST) //
|
||||
.withInput(HalFormsPayload.class) //
|
||||
.toLink();
|
||||
|
||||
EntityModel<HalFormsPayload> model = EntityModel.of(new HalFormsPayload(), link);
|
||||
ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
|
||||
|
||||
assertThatCode(() -> {
|
||||
|
||||
String promptString = JsonPath.compile("$._templates.default.properties[0].placeholder") //
|
||||
.read(mapper.writeValueAsString(model));
|
||||
|
||||
assertThat(promptString).isEqualTo("Property placeholder");
|
||||
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
private void assertThatPathDoesNotExist(Object toMarshall, String path) throws Exception {
|
||||
|
||||
ObjectMapper mapper = getCuriedObjectMapper();
|
||||
@@ -553,8 +582,6 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
|
||||
|
||||
private void verifyResolvedTitle(String resourceBundleKey) throws Exception {
|
||||
|
||||
LocaleContextHolder.setLocale(Locale.US);
|
||||
|
||||
StaticMessageSource messageSource = new StaticMessageSource();
|
||||
messageSource.addMessage(resourceBundleKey, Locale.US, "Foobar's title!");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user