#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:
@@ -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));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user