diff --git a/pom.xml b/pom.xml
index 82b05fd3..2850c7e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -759,6 +759,12 @@
test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
org.springframework
spring-test
diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java
index 871abb7b..dce2d065 100644
--- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java
+++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java
@@ -15,12 +15,17 @@
*/
package org.springframework.hateoas.mediatype.hal.forms;
+import lombok.RequiredArgsConstructor;
+
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
@@ -32,7 +37,9 @@ import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.hal.HalLinkRelation;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
import org.springframework.http.HttpMethod;
+import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
@@ -58,16 +65,18 @@ class HalFormsSerializers {
private static final long serialVersionUID = -7912243216469101379L;
+ private final MessageSourceAccessor accessor;
private final BeanProperty property;
- HalFormsResourceSerializer(@Nullable BeanProperty property) {
+ HalFormsResourceSerializer(MessageSourceAccessor accessor, @Nullable BeanProperty property) {
super(EntityModel.class, false);
+ this.accessor = accessor;
this.property = property;
}
- HalFormsResourceSerializer() {
- this(null);
+ HalFormsResourceSerializer(MessageSourceAccessor accessor) {
+ this(accessor, null);
}
/*
@@ -80,7 +89,7 @@ class HalFormsSerializers {
HalFormsDocument> doc = HalFormsDocument.forResource(value.getContent()) //
.withLinks(value.getLinks()) //
- .withTemplates(findTemplates(value));
+ .withTemplates(findTemplates(value, accessor));
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
@@ -134,7 +143,7 @@ class HalFormsSerializers {
@SuppressWarnings("null")
public JsonSerializer> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
- return new HalFormsResourceSerializer(property);
+ return new HalFormsResourceSerializer(accessor, property);
}
}
@@ -148,17 +157,20 @@ class HalFormsSerializers {
private final BeanProperty property;
private final Jackson2HalModule.EmbeddedMapper embeddedMapper;
+ private final MessageSourceAccessor accessor;
- HalFormsResourcesSerializer(@Nullable BeanProperty property, Jackson2HalModule.EmbeddedMapper embeddedMapper) {
+ HalFormsResourcesSerializer(MessageSourceAccessor accessor, @Nullable BeanProperty property,
+ Jackson2HalModule.EmbeddedMapper embeddedMapper) {
super(CollectionModel.class, false);
this.property = property;
this.embeddedMapper = embeddedMapper;
+ this.accessor = accessor;
}
- HalFormsResourcesSerializer(Jackson2HalModule.EmbeddedMapper embeddedMapper) {
- this(null, embeddedMapper);
+ HalFormsResourcesSerializer(MessageSourceAccessor accessor, Jackson2HalModule.EmbeddedMapper embeddedMapper) {
+ this(accessor, null, embeddedMapper);
}
/*
@@ -179,14 +191,14 @@ class HalFormsSerializers {
.withEmbedded(embeddeds) //
.withPageMetadata(((PagedModel>) value).getMetadata()) //
.withLinks(value.getLinks()) //
- .withTemplates(findTemplates(value));
+ .withTemplates(findTemplates(value, accessor));
} else {
doc = HalFormsDocument.empty() //
.withEmbedded(embeddeds) //
.withLinks(value.getLinks()) //
- .withTemplates(findTemplates(value));
+ .withTemplates(findTemplates(value, accessor));
}
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
@@ -241,7 +253,7 @@ class HalFormsSerializers {
@SuppressWarnings("null")
public JsonSerializer> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
- return new HalFormsResourcesSerializer(property, embeddedMapper);
+ return new HalFormsResourcesSerializer(accessor, property, embeddedMapper);
}
}
@@ -251,14 +263,16 @@ class HalFormsSerializers {
* @param resource
* @return
*/
- private static Map findTemplates(RepresentationModel> resource) {
+ private static Map findTemplates(RepresentationModel> resource,
+ MessageSourceAccessor accessor) {
if (!resource.hasLink(IanaLinkRelations.SELF)) {
return Collections.emptyMap();
}
Map templates = new HashMap<>();
- List affordances = resource.getLink(IanaLinkRelations.SELF).map(Link::getAffordances)
+ List affordances = resource.getLink(IanaLinkRelations.SELF) //
+ .map(Link::getAffordances) //
.orElse(Collections.emptyList());
affordances.stream() //
@@ -268,8 +282,14 @@ class HalFormsSerializers {
.peek(it -> validate(resource, it)) //
.forEach(it -> {
+ Class> type = it.getInputType().resolve(Object.class);
+
+ List propertiesWithPrompt = it.getInputProperties().stream() //
+ .map(property -> property.withPrompt(accessor.getMessage(PromptLookup.of(type, property))))
+ .collect(Collectors.toList());
+
HalFormsTemplate template = HalFormsTemplate.forMethod(it.getHttpMethod()) //
- .withProperties(it.getInputProperties());
+ .withProperties(propertiesWithPrompt);
/*
* First template in HAL-FORMS is "default".
@@ -280,6 +300,40 @@ class HalFormsSerializers {
return templates;
}
+ @RequiredArgsConstructor(staticName = "of")
+ static class PromptLookup implements MessageSourceResolvable {
+
+ private static final String PROMPT_TEMPLATE = "%s._prompt";
+
+ private final Class> type;
+ private final HalFormsProperty property;
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.context.MessageSourceResolvable#getDefaultMessage()
+ */
+ @Nullable
+ @Override
+ public String getDefaultMessage() {
+ return StringUtils.capitalize(property.getName());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.context.MessageSourceResolvable#getCodes()
+ */
+ @NonNull
+ @Override
+ public String[] getCodes() {
+
+ String globalCode = String.format(PROMPT_TEMPLATE, property.getName());
+ String localCode = String.format("%s.%s", type.getSimpleName(), globalCode);
+ String qualifiedCode = String.format("%s.%s", type.getName(), globalCode);
+
+ return new String[] { qualifiedCode, localCode, globalCode };
+ }
+ }
+
/**
* Verify that the resource's self link and the affordance's URI have the same relative path.
*
diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java
index 4da76004..f2cb3709 100644
--- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java
+++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java
@@ -23,6 +23,7 @@ import java.util.Map;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.CollectionModel;
+import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedModel;
@@ -82,13 +83,15 @@ class Jackson2HalFormsModule extends SimpleModule {
setMixInAnnotation(Link.class, LinkMixin.class);
setMixInAnnotation(RepresentationModel.class, RepresentationModelMixin.class);
+ setMixInAnnotation(EntityModel.class, EntityModelMixin.class);
setMixInAnnotation(CollectionModel.class, CollectionModelMixin.class);
setMixInAnnotation(PagedModel.class, PagedModelMixin.class);
setMixInAnnotation(MediaType.class, MediaTypeMixin.class);
-
- addSerializer(new HalFormsResourceSerializer());
}
+ @JsonSerialize(using = HalFormsResourceSerializer.class)
+ abstract class EntityModelMixin extends EntityModel {}
+
@JsonSerialize(using = HalFormsResourcesSerializer.class)
abstract class CollectionModelMixin extends CollectionModel {
@@ -160,7 +163,8 @@ class Jackson2HalFormsModule extends SimpleModule {
EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections);
- this.serializers.put(HalFormsResourcesSerializer.class, new HalFormsResourcesSerializer(mapper));
+ this.serializers.put(HalFormsResourceSerializer.class, new HalFormsResourceSerializer(accessor));
+ this.serializers.put(HalFormsResourcesSerializer.class, new HalFormsResourcesSerializer(accessor, mapper));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, accessor, halFormsConfiguration.toHalConfiguration()));
}
diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java
index ff023f44..fd5e0c48 100644
--- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java
+++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java
@@ -18,6 +18,8 @@ package org.springframework.hateoas.mediatype.hal.forms;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
+import lombok.Getter;
+
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -28,6 +30,8 @@ import java.util.Locale;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;
@@ -53,10 +57,12 @@ import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider;
import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider;
import org.springframework.hateoas.server.core.EmbeddedWrappers;
import org.springframework.hateoas.support.MappingUtils;
+import org.springframework.http.HttpMethod;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
+import com.jayway.jsonpath.JsonPath;
/**
* @author Greg Turnquist
@@ -79,7 +85,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator( //
- provider, CurieProvider.NONE, new MessageSourceAccessor(messageSource), true, new HalFormsConfiguration()));
+ provider, CurieProvider.NONE, new MessageSourceAccessor(messageSource, Locale.US), true,
+ new HalFormsConfiguration()));
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
}
@@ -393,6 +400,31 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
assertThat(deserialized).isEqualTo(original);
}
+ @ParameterizedTest // #979
+ @ValueSource(strings = { "firstname._prompt", //
+ "HalFormsPayload.firstname._prompt", //
+ "org.springframework.hateoas.mediatype.hal.forms.Jackson2HalFormsIntegrationTest$HalFormsPayload.firstname._prompt" })
+ public void usesResourceBundleToCreatePropertyPrompts(String key) {
+
+ StaticMessageSource source = new StaticMessageSource();
+ source.addMessage(key, Locale.US, "Vorname");
+
+ Link link = new Link("some:link") //
+ .andAffordance(HttpMethod.POST, HalFormsPayload.class, Collections.emptyList(), Object.class);
+
+ EntityModel model = new EntityModel<>(new HalFormsPayload(), link);
+ ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
+
+ assertThatCode(() -> {
+
+ String promptString = JsonPath.compile("$._templates.default.properties[0].prompt") //
+ .read(mapper.writeValueAsString(model));
+
+ assertThat(promptString).isEqualTo("Vorname");
+
+ }).doesNotThrowAnyException();
+ }
+
private void verifyResolvedTitle(String resourceBundleKey) throws Exception {
LocaleContextHolder.setLocale(Locale.US);
@@ -448,10 +480,16 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), provider,
- messageSource == null ? null : new MessageSourceAccessor(messageSource), true, new HalFormsConfiguration()));
+ messageSource == null ? null : new MessageSourceAccessor(messageSource, Locale.US), true,
+ new HalFormsConfiguration()));
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.setSerializationInclusion(Include.NON_NULL);
return mapper;
}
+
+ public static class HalFormsPayload {
+ private @Getter String firstname;
+
+ }
}