#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,
|
||||
|
||||
Reference in New Issue
Block a user