#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:
Oliver Drotbohm
2021-01-19 17:39:22 +01:00
parent 6ad9012047
commit a16c54f7b3
8 changed files with 316 additions and 190 deletions

View File

@@ -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 + ")";
}

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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 + ")";
}
}

View File

@@ -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 + ")";

View File

@@ -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]);
}
}
}

View File

@@ -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);

View File

@@ -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!");