diff --git a/src/main/java/org/springframework/hateoas/IanaLinkRelations.java b/src/main/java/org/springframework/hateoas/IanaLinkRelations.java index be0de5e4..bb753d36 100644 --- a/src/main/java/org/springframework/hateoas/IanaLinkRelations.java +++ b/src/main/java/org/springframework/hateoas/IanaLinkRelations.java @@ -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)); } diff --git a/src/main/java/org/springframework/hateoas/LinkRelation.java b/src/main/java/org/springframework/hateoas/LinkRelation.java index 0ff02cfa..5e4bcae7 100644 --- a/src/main/java/org/springframework/hateoas/LinkRelation.java +++ b/src/main/java/org/springframework/hateoas/LinkRelation.java @@ -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 mapper) { + + if (mapper == Function. 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())); + } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java index 621ddc8a..b56a088d 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java @@ -48,6 +48,12 @@ public class HalConfiguration { private final @Wither @Getter RenderSingleLinks renderSingleLinks; private final @Wither(AccessLevel.PRIVATE) Map 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; } /** diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java index 59e67bdb..4ca766ef 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java @@ -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 NO_TRANSFORMER = Function.identity(); private final Map 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 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); diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java index 0cd1c484..3546a303 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java @@ -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 mapper) { + + String mappedLocalPart = mapper.apply(localPart); + + return localPart.equals(mappedLocalPart) // + ? this // + : new HalLinkRelation(curie, mappedLocalPart); + } + /* * (non-Javadoc) * @see org.springframework.hateoas.LinkRelation#value() diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java index 49fdc21d..b85f392a 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java @@ -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 embeddeds = embeddedMapper.map(value); + EmbeddedMapper mapper = configuration.isApplyPropertyNamingStrategy() // + ? embeddedMapper.with(provider.getConfig().getPropertyNamingStrategy()) // + : embeddedMapper; + + Map 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 NO_OP = Function.identity(); + + private final @lombok.NonNull LinkRelationProvider relProvider; + private final CurieProvider curieProvider; + private final boolean preferCollectionRels; + + private Function 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. * 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 149df147..7b2ca1e5 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 @@ -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 embeddeds = embeddedMapper.map(value); + EmbeddedMapper mapper = configuration.isApplyPropertyNamingStrategy() // + ? embeddedMapper.with(provider.getConfig().getPropertyNamingStrategy()) // + : embeddedMapper; + + Map 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); } } } 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 f2e35804..848c93e9 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 @@ -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, diff --git a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java index 25c77e70..57b50663 100755 --- a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java @@ -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); diff --git a/src/test/java/org/springframework/hateoas/LinkRelationUnitTest.java b/src/test/java/org/springframework/hateoas/LinkRelationUnitTest.java index daf1a500..183370e5 100644 --- a/src/test/java/org/springframework/hateoas/LinkRelationUnitTest.java +++ b/src/test/java/org/springframework/hateoas/LinkRelationUnitTest.java @@ -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. */ diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalLinkRelationUnitTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalLinkRelationUnitTest.java index ee281f36..90049df4 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/HalLinkRelationUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalLinkRelationUnitTest.java @@ -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")); + } } diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java index 3da0f1ce..b54f09dc 100755 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java @@ -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 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.> read(result)) // + .forEach(it -> assertThat(it).containsKey("some_sample")); + + assertThat(JsonPath.compile("$._links").> read(result)) // + .containsKey(IanaLinkRelations.EDIT_FORM.value()); + } + + @Test // #1132 + void doesNotApplyPropertyNamingStrategyToLinkRelationsIfConfigurationOptsOut() throws Exception { + + CollectionModel 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.> 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);