#1132 - HAL link relation rendering now applies Jackson's PropertyNamingStrategy.

The rendering of HAL's link relations now applies Jackson's PropertyNamingStrategy if one is configured except it's a IANA link relation. This behavior can be opt out from by setting HalConfiguration.withApplyPropertyNamingStrategy(false).

LinkRelation now has a ….map(…) method allowing the post-processing of it given a Function<String, String>. By default, that's applied to all relations that are not IANA ones. HalLinkRelation applies this transformation to only the local part of the relation. Tweaked IanaLinkRelations.isIanaLinkRelation(…) to do a simple ….contains(…) check first before we stream over all values to apply a case insensitive check.
This commit is contained in:
Oliver Drotbohm
2019-12-06 12:05:02 +01:00
parent 8f1f355c9f
commit a2290c692a
12 changed files with 249 additions and 55 deletions

View File

@@ -786,8 +786,8 @@ public class IanaLinkRelations {
Assert.notNull(relation, "Link relation must not be null!");
return LINK_RELATIONS.stream() //
.anyMatch(it -> it.value().equalsIgnoreCase(relation.value()));
return LINK_RELATIONS.contains(relation) //
|| LINK_RELATIONS.stream().anyMatch(it -> it.isSameAs(relation));
}

View File

@@ -16,6 +16,7 @@
package org.springframework.hateoas;
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
@@ -75,4 +76,28 @@ public interface LinkRelation {
return this.value().equalsIgnoreCase(relation.value());
}
/**
* Returns a new {@link LinkRelation} with its relation mapped by the given function, unless it is an IANA one.
* Implementors are encouraged to override this method to redeclare the return type to be itself.
*
* @param mapper must not be {@literal null}.
* @return
* @see IanaLinkRelations
*/
default LinkRelation map(Function<String, String> mapper) {
if (mapper == Function.<String> identity() || IanaLinkRelations.isIanaRel(this)) {
return this;
}
Assert.notNull(mapper, "Mapper must not be null!");
String source = value();
String mapped = mapper.apply(source);
return source.equals(mapped) //
? this //
: LinkRelation.of(mapper.apply(value()));
}
}

View File

@@ -48,6 +48,12 @@ public class HalConfiguration {
private final @Wither @Getter RenderSingleLinks renderSingleLinks;
private final @Wither(AccessLevel.PRIVATE) Map<String, RenderSingleLinks> singleLinksPerPattern;
/**
* Configures whether the Jackson property naming strategy is applied to link relations and within {@code _embedded}
* clauses.
*/
private final @Wither @Getter boolean applyPropertyNamingStrategy;
/**
* Creates a new default {@link HalConfiguration} rendering single links as immediate sub-document.
*/
@@ -55,6 +61,7 @@ public class HalConfiguration {
this.renderSingleLinks = RenderSingleLinks.AS_SINGLE;
this.singleLinksPerPattern = new LinkedHashMap<>();
this.applyPropertyNamingStrategy = true;
}
/**

View File

@@ -15,12 +15,17 @@
*/
package org.springframework.hateoas.mediatype.hal;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.Wither;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.LinkRelation;
@@ -37,15 +42,22 @@ import org.springframework.util.Assert;
* @author Oliver Gierke
* @author Dietrich Schulten
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
class HalEmbeddedBuilder {
private static final String INVALID_EMBEDDED_WRAPPER = "Embedded wrapper %s returned null for both the static rel and the rel target type! Make sure one of the two returns a non-null value!";
private static final Function<String, String> NO_TRANSFORMER = Function.identity();
private final Map<HalLinkRelation, Object> embeddeds = new HashMap<>();
private final LinkRelationProvider provider;
private final CurieProvider curieProvider;
private final EmbeddedWrappers wrappers;
/**
* Returns a {@link HalEmbeddedBuilder} with the given transformer
*/
private final @Wither Function<String, String> relationTransformer;
/**
* Creates a new {@link HalEmbeddedBuilder} using the given {@link LinkRelationProvider} and prefer collection rels
* flag.
@@ -61,6 +73,7 @@ class HalEmbeddedBuilder {
this.provider = provider;
this.curieProvider = curieProvider;
this.wrappers = new EmbeddedWrappers(preferCollectionRels);
this.relationTransformer = NO_TRANSFORMER;
}
/**
@@ -124,6 +137,8 @@ class HalEmbeddedBuilder {
? provider.getCollectionResourceRelFor(type) //
: provider.getItemResourceRelFor(type);
rel = relationTransformer == NO_TRANSFORMER ? rel : rel.map(relationTransformer);
return curieProvider != CurieProvider.NONE //
? curieProvider.getNamespacedRelFor(rel) //
: HalLinkRelation.of(rel);

View File

@@ -21,6 +21,7 @@ import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
import java.util.stream.Stream;
import org.springframework.context.MessageSourceResolvable;
@@ -151,6 +152,20 @@ public class HalLinkRelation implements LinkRelation, MessageSourceResolvable {
return curie != null;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkRelation#map(java.util.function.Function)
*/
@Override
public HalLinkRelation map(Function<String, String> mapper) {
String mappedLocalPart = mapper.apply(localPart);
return localPart.equals(mappedLocalPart) //
? this //
: new HalLinkRelation(curie, mappedLocalPart);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkRelation#value()

View File

@@ -15,6 +15,10 @@
*/
package org.springframework.hateoas.mediatype.hal;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
@@ -24,6 +28,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
@@ -49,6 +54,7 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.PropertyNamingStrategy.PropertyNamingStrategyBase;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
@@ -155,10 +161,15 @@ public class Jackson2HalModule extends SimpleModule {
Object currentValue = jgen.getCurrentValue();
if (currentValue instanceof CollectionModel) {
if (mapper.hasCuriedEmbed((CollectionModel<?>) currentValue)) {
curiedLinkPresent = true;
}
PropertyNamingStrategy propertyNamingStrategy = provider.getConfig().getPropertyNamingStrategy();
EmbeddedMapper transformingMapper = halConfiguration.isApplyPropertyNamingStrategy() //
? mapper.with(propertyNamingStrategy)
: mapper;
if (currentValue instanceof CollectionModel
&& transformingMapper.hasCuriedEmbed((CollectionModel<?>) currentValue)) {
curiedLinkPresent = true;
}
for (Link link : value) {
@@ -173,9 +184,11 @@ public class Jackson2HalModule extends SimpleModule {
curiedLinkPresent = true;
}
HalLinkRelation relation = transformingMapper.map(rel);
sortedLinks //
.computeIfAbsent(rel, key -> new ArrayList<>())//
.add(toHalLink(link));
.computeIfAbsent(relation, key -> new ArrayList<>())//
.add(toHalLink(link, relation));
links.add(link);
}
@@ -205,10 +218,7 @@ public class Jackson2HalModule extends SimpleModule {
* @param link must not be {@literal null}.
* @return
*/
private HalLink toHalLink(Link link) {
HalLinkRelation rel = HalLinkRelation.of(link.getRel());
private HalLink toHalLink(Link link, HalLinkRelation rel) {
return new HalLink(link, resolver.resolve(rel));
}
@@ -287,19 +297,22 @@ public class Jackson2HalModule extends SimpleModule {
private static final long serialVersionUID = 8030706944344625390L;
private final BeanProperty property;
private final EmbeddedMapper embeddedMapper;
private final HalConfiguration configuration;
private final BeanProperty property;
public HalResourcesSerializer(EmbeddedMapper embeddedMapper) {
this(null, embeddedMapper);
public HalResourcesSerializer(EmbeddedMapper embeddedMapper, HalConfiguration configuration) {
this(embeddedMapper, configuration, null);
}
public HalResourcesSerializer(@Nullable BeanProperty property, EmbeddedMapper embeddedMapper) {
public HalResourcesSerializer(EmbeddedMapper embeddedMapper, HalConfiguration configuration,
@Nullable BeanProperty property) {
super(TypeFactory.defaultInstance().constructType(Collection.class));
this.property = property;
this.embeddedMapper = embeddedMapper;
this.configuration = configuration;
this.property = property;
}
/*
@@ -312,13 +325,17 @@ public class Jackson2HalModule extends SimpleModule {
@SuppressWarnings("null")
public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
Map<HalLinkRelation, Object> embeddeds = embeddedMapper.map(value);
EmbeddedMapper mapper = configuration.isApplyPropertyNamingStrategy() //
? embeddedMapper.with(provider.getConfig().getPropertyNamingStrategy()) //
: embeddedMapper;
Map<HalLinkRelation, Object> embeddeds = mapper.map(value);
Object currentValue = jgen.getCurrentValue();
if (currentValue instanceof RepresentationModel) {
if (embeddedMapper.hasCuriedEmbed(value)) {
if (mapper.hasCuriedEmbed(value)) {
((RepresentationModel<?>) currentValue).add(CURIES_REQUIRED_DUE_TO_EMBEDS);
}
}
@@ -334,7 +351,7 @@ public class Jackson2HalModule extends SimpleModule {
@SuppressWarnings("null")
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalResourcesSerializer(property, embeddedMapper);
return new HalResourcesSerializer(embeddedMapper, configuration, property);
}
/*
@@ -748,7 +765,7 @@ public class Jackson2HalModule extends SimpleModule {
this.delegate = delegate;
this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper));
this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper, halConfiguration));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, resolver, halConfiguration));
}
@@ -880,27 +897,34 @@ public class Jackson2HalModule extends SimpleModule {
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class EmbeddedMapper {
private LinkRelationProvider relProvider;
private CurieProvider curieProvider;
private boolean preferCollectionRels;
private static final Function<String, String> NO_OP = Function.identity();
private final @lombok.NonNull LinkRelationProvider relProvider;
private final CurieProvider curieProvider;
private final boolean preferCollectionRels;
private Function<String, String> relationTransformer = Function.identity();
/**
* Creates a new {@link EmbeddedMapper} for the given {@link LinkRelationProvider}, {@link CurieProvider} and flag
* whether to prefer collection relations.
* Registers the given {@link PropertyNamingStrategy} with the current mapper to forward that strategy as relation
* transformer, so that {@link LinkRelation}s used as key for the embedding will be transformed using the given
* strategy.
*
* @param relProvider must not be {@literal null}.
* @param curieProvider must not be {@literal null}.
* @param preferCollectionRels
* @param strategy must not be {@literal null}.
* @return an {@link EmbeddedMapper} applying the given strategy when mapping embedded objects.
*/
public EmbeddedMapper(LinkRelationProvider relProvider, CurieProvider curieProvider, boolean preferCollectionRels) {
public EmbeddedMapper with(@Nullable PropertyNamingStrategy strategy) {
Assert.notNull(relProvider, "RelProvider must not be null!");
if (!(strategy instanceof PropertyNamingStrategyBase)) {
return this;
}
this.relProvider = relProvider;
this.curieProvider = curieProvider;
this.preferCollectionRels = preferCollectionRels;
return new EmbeddedMapper(relProvider, curieProvider, preferCollectionRels,
((PropertyNamingStrategyBase) strategy)::translate);
}
/**
@@ -913,13 +937,27 @@ public class Jackson2HalModule extends SimpleModule {
Assert.notNull(source, "Elements must not be null!");
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, curieProvider, preferCollectionRels);
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, curieProvider, preferCollectionRels) //
.withRelationTransformer(relationTransformer);
source.forEach(builder::add);
return builder.asMap();
}
/**
* Maps the given {@link HalLinkRelation} using the underlying relation transformer.
*
* @param source must not be {@literal null}.
* @return
*/
public HalLinkRelation map(LinkRelation source) {
Assert.notNull(source, "Link relation must not be null!");
return HalLinkRelation.of(relationTransformer == NO_OP ? source : source.map(relationTransformer));
}
/**
* Returns whether the given source elements will be namespaced.
*

View File

@@ -22,8 +22,10 @@ import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.HalLinkRelation;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.EmbeddedMapper;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.core.JsonGenerator;
@@ -215,20 +217,23 @@ class HalFormsSerializers {
private final BeanProperty property;
private final Jackson2HalModule.EmbeddedMapper embeddedMapper;
private final HalFormsTemplateBuilder customizations;
private final HalConfiguration configuration;
HalFormsCollectionModelSerializer(HalFormsTemplateBuilder customizations, @Nullable BeanProperty property,
Jackson2HalModule.EmbeddedMapper embeddedMapper) {
HalFormsCollectionModelSerializer(HalFormsTemplateBuilder customizations,
Jackson2HalModule.EmbeddedMapper embeddedMapper, HalConfiguration configuration,
@Nullable BeanProperty property) {
super(CollectionModel.class, false);
this.property = property;
this.embeddedMapper = embeddedMapper;
this.customizations = customizations;
this.configuration = configuration;
}
HalFormsCollectionModelSerializer(HalFormsTemplateBuilder customizations,
Jackson2HalModule.EmbeddedMapper embeddedMapper) {
this(customizations, null, embeddedMapper);
Jackson2HalModule.EmbeddedMapper embeddedMapper, HalConfiguration configuration) {
this(customizations, embeddedMapper, configuration, null);
}
/*
@@ -239,7 +244,11 @@ class HalFormsSerializers {
@SuppressWarnings("null")
public void serialize(CollectionModel<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<HalLinkRelation, Object> embeddeds = embeddedMapper.map(value);
EmbeddedMapper mapper = configuration.isApplyPropertyNamingStrategy() //
? embeddedMapper.with(provider.getConfig().getPropertyNamingStrategy()) //
: embeddedMapper;
Map<HalLinkRelation, Object> embeddeds = mapper.map(value);
HalFormsDocument<?> doc;
@@ -311,7 +320,7 @@ class HalFormsSerializers {
@SuppressWarnings("null")
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalFormsCollectionModelSerializer(customizations, property, embeddedMapper);
return new HalFormsCollectionModelSerializer(customizations, embeddedMapper, configuration, property);
}
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.EmbeddedMapper;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListDeserializer;
@@ -169,14 +170,15 @@ public class Jackson2HalFormsModule extends SimpleModule {
EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections);
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, accessor);
HalConfiguration halConfiguration = configuration.getHalConfiguration();
this.serializers.put(HalFormsRepresentationModelSerializer.class,
new HalFormsRepresentationModelSerializer(builder));
this.serializers.put(HalFormsEntityModelSerializer.class, new HalFormsEntityModelSerializer(builder));
this.serializers.put(HalFormsCollectionModelSerializer.class,
new HalFormsCollectionModelSerializer(builder, mapper));
new HalFormsCollectionModelSerializer(builder, mapper, halConfiguration));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, accessor, configuration.getHalConfiguration()));
new HalLinkListSerializer(curieProvider, mapper, accessor, halConfiguration));
}
public HalFormsHandlerInstantiator(LinkRelationProvider relProvider, CurieProvider curieProvider,

View File

@@ -19,6 +19,11 @@ import java.io.StringWriter;
import java.io.Writer;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -44,6 +49,16 @@ public abstract class AbstractJackson2MarshallingIntegrationTest {
.disable(MapperFeature.AUTO_DETECT_SETTERS);
}
protected ObjectMapper with(HalConfiguration configuration) {
ObjectMapper copy = mapper.copy();
copy.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
MessageResolver.DEFAULTS_ONLY, configuration));
return copy;
}
protected String write(Object object) throws Exception {
Writer writer = new StringWriter();
mapper.writeValue(writer, object);

View File

@@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*;
import lombok.Value;
import java.util.Arrays;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
@@ -58,6 +59,21 @@ class LinkRelationUnitTest {
assertThat(IanaLinkRelations.SELF.isSameAs(LinkRelation.of("seLF"))).isTrue();
}
@Test // #1132
void appliesMapper() {
assertThat(LinkRelation.of("someRelation").map(it -> it.toLowerCase(Locale.US))) //
.isEqualTo(LinkRelation.of("somerelation"));
}
@Test // #1132
void doesNotApplyMapperToIanaLinkRelations() {
LinkRelation mapped = IanaLinkRelations.TERMS_OF_SERVICE.map(it -> it.replace("-", "_"));
assertThat(mapped).isEqualTo(IanaLinkRelations.TERMS_OF_SERVICE);
}
/**
* Custom implementation of the {@link LinkRelation} interface.
*/

View File

@@ -106,4 +106,11 @@ class HalLinkRelationUnitTest {
assertThat(relation.getCodes()).containsExactly("_links.curie:relation.title", "_links.relation.title");
}
@Test // #1132
void mapsLocalPartOnly() {
assertThat(HalLinkRelation.curied("CURIE", "someRelation").map(String::toLowerCase)) //
.isEqualTo(HalLinkRelation.curied("CURIE", "somerelation"));
}
}

View File

@@ -24,6 +24,8 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -40,9 +42,14 @@ import org.springframework.hateoas.server.LinkRelationProvider;
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.server.core.Relation;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.jayway.jsonpath.JsonPath;
/**
* Integration tests for Jackson 2 HAL integration.
@@ -446,13 +453,13 @@ class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationT
@Test
void rendersSingleLinkAsArrayWhenConfigured() throws Exception {
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
MessageResolver.DEFAULTS_ONLY, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY)));
ObjectMapper mapper = with(new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY));
RepresentationModel<?> resourceSupport = new RepresentationModel<>();
resourceSupport.add(new Link("localhost").withSelfRel());
assertThat(write(resourceSupport)).isEqualTo("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}");
assertThat(mapper.writeValueAsString(resourceSupport))
.isEqualTo("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}");
}
/**
@@ -465,10 +472,7 @@ class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationT
original.add(new Link("/orders{?id}", "order"));
String serialized = mapper.writeValueAsString(original);
String expected = "{\"_links\":{\"order\":{\"href\":\"/orders{?id}\",\"templated\":true}}}";
assertThat(serialized).isEqualTo(expected);
assertThat(serialized).isEqualTo("{\"_links\":{\"order\":{\"href\":\"/orders{?id}\",\"templated\":true}}}");
RepresentationModel<?> deserialized = mapper.readValue(serialized, RepresentationModel.class);
@@ -478,11 +482,7 @@ class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationT
@Test // #811
void rendersSpecificRelWithSingleLinkAsArrayIfConfigured() throws Exception {
AnnotationLinkRelationProvider provider = new AnnotationLinkRelationProvider();
mapper
.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY,
new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY)));
ObjectMapper mapper = with(new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY));
RepresentationModel<?> resource = new RepresentationModel<>();
resource.add(new Link("/some-href", "foo"));
@@ -512,6 +512,51 @@ class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationT
}).doesNotThrowAnyException();
}
@Test // #1132
void forwardsPropertyNamingStrategyToNonIanaLinkRelations() throws JsonProcessingException {
CollectionModel<Object> model = new CollectionModel<>(Arrays.asList(new SomeSample()));
model.add(new Link("/foo", LinkRelation.of("someSample")));
model.add(new Link("/foo/form", IanaLinkRelations.EDIT_FORM));
ObjectMapper objectMapper = mapper.copy() //
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) //
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
String result = objectMapper.writeValueAsString(model);
Stream.of("$._embedded", "$._links") //
.map(JsonPath::compile) //
.map(it -> it.<Map<String, Object>> read(result)) //
.forEach(it -> assertThat(it).containsKey("some_sample"));
assertThat(JsonPath.compile("$._links").<Map<String, Object>> read(result)) //
.containsKey(IanaLinkRelations.EDIT_FORM.value());
}
@Test // #1132
void doesNotApplyPropertyNamingStrategyToLinkRelationsIfConfigurationOptsOut() throws Exception {
CollectionModel<Object> model = new CollectionModel<>(Arrays.asList(new SomeSample()));
model.add(new Link("/foo", LinkRelation.of("someSample")));
ObjectMapper mapper = with(new HalConfiguration().withApplyPropertyNamingStrategy(false)) //
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) //
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
String result = mapper.writeValueAsString(model);
Stream.of("$._embedded", "$._links") //
.map(JsonPath::compile) //
.map(it -> it.<Map<String, Object>> read(result)) //
.forEach(it -> assertThat(it).containsKey("someSample"));
}
@Relation(collectionRelation = "someSample")
static class SomeSample {
String name;
}
private void verifyResolvedTitle(String resourceBundleKey) throws Exception {
LocaleContextHolder.setLocale(Locale.US);