#979 - HAL Forms prompts can now be configured via a resource bundle.

We now support defining the prompts to be used for properties via the Spring HATEOAS rest-messages resource bundle. We support global definitions via $propertyName._prompt as well as qualified ones ($packageName.)$typeName.$propertyName._prompt.
This commit is contained in:
Oliver Drotbohm
2019-06-26 23:27:57 +02:00
parent cd5232ae08
commit ceb406ae38
4 changed files with 121 additions and 19 deletions

View File

@@ -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<String, HalFormsTemplate> findTemplates(RepresentationModel<?> resource) {
private static Map<String, HalFormsTemplate> findTemplates(RepresentationModel<?> resource,
MessageSourceAccessor accessor) {
if (!resource.hasLink(IanaLinkRelations.SELF)) {
return Collections.emptyMap();
}
Map<String, HalFormsTemplate> templates = new HashMap<>();
List<Affordance> affordances = resource.getLink(IanaLinkRelations.SELF).map(Link::getAffordances)
List<Affordance> 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<HalFormsProperty> 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.
*

View File

@@ -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<T> extends EntityModel<T> {}
@JsonSerialize(using = HalFormsResourcesSerializer.class)
abstract class CollectionModelMixin<T> extends CollectionModel<T> {
@@ -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()));
}