#811 - Added ability to override single link render mode per link relation.

HalConfiguration now exposes a withSingleLinkRenderModeFor(…) taking a path pattern to be rendered in the also given RenderSingleLinks mode. It takes patterns as link relations are either plain strings or valid URIs.

Simplified HAL link list rendering in Jackson2HalModule avoiding double nesting of collections before rendering.
This commit is contained in:
Oliver Drotbohm
2019-02-21 16:34:07 +01:00
parent 943e9afc08
commit aea4c4c91f
4 changed files with 192 additions and 33 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,21 +18,98 @@ package org.springframework.hateoas.hal;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Wither;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
/**
* HAL specific configuration.
*
* @author Greg Turnquist
* @author Oliver Gierke
* @author Oliver Drotbohm
*/
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class HalConfiguration {
private @Wither @Getter RenderSingleLinks renderSingleLinks = RenderSingleLinks.AS_SINGLE;
private static final PathMatcher MATCHER = new AntPathMatcher();
/**
* Configures how to render links in case there is exactly one defined for a given link relation in general. By
* default, this single link will be rendered as nested document.
*/
private final @Wither @Getter RenderSingleLinks renderSingleLinks;
private final @Wither(AccessLevel.PRIVATE) Map<String, RenderSingleLinks> singleLinksPerPattern;
/**
* Creates a new default {@link HalConfiguration} rendering single links as immediate sub-document.
*/
public HalConfiguration() {
this.renderSingleLinks = RenderSingleLinks.AS_SINGLE;
this.singleLinksPerPattern = new LinkedHashMap<>();
}
/**
* Configures how to render a single link for a given particular {@link LinkRelation}. This will override what has
* been configured via {@link #withRenderSingleLinks(RenderSingleLinks)} for that particular link relation.
*
* @param relation must not be {@literal null}.
* @param renderSingleLinks must not be {@literal null}.
* @return
*/
public HalConfiguration withRenderSingleLinksFor(LinkRelation relation, RenderSingleLinks renderSingleLinks) {
Assert.notNull(relation, "Link relation must not be null!");
Assert.notNull(renderSingleLinks, "RenderSingleLinks must not be null!");
return withRenderSingleLinksFor(relation.value(), renderSingleLinks);
}
/**
* Configures how to render a single link for the given link relation pattern, i.e. this can be either a fixed link
* relation (like {@code search}), take wildcards to e.g. match links of a given curie (like {@code acme:*}) or even
* complete URIs (like {@code http://api.acme.com/foo/**}).
*
* @param pattern must not be {@literal null}.
* @param renderSingleLinks must not be {@literal null}.
* @return @see PathMatcher
*/
public HalConfiguration withRenderSingleLinksFor(String pattern, RenderSingleLinks renderSingleLinks) {
Map<String, RenderSingleLinks> map = new LinkedHashMap<>(singleLinksPerPattern);
map.put(pattern, renderSingleLinks);
return withSingleLinksPerPattern(map);
}
/**
* Returns which render mode to use to render a single link for the given {@link LinkRelation}.
*
* @param relation must not be {@literal null}.
* @return
*/
RenderSingleLinks getSingleLinkRenderModeFor(LinkRelation relation) {
return singleLinksPerPattern.entrySet().stream() //
.filter(entry -> MATCHER.match(entry.getKey(), relation.value())) //
.map(Entry::getValue) //
.findFirst() //
.orElse(renderSingleLinks);
}
/**
* Configuration option how to render single links of a given {@link LinkRelation}.
*
* @author Oliver Drotbohm
*/
public enum RenderSingleLinks {
/**

View File

@@ -21,7 +21,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -183,7 +182,7 @@ public class Jackson2HalModule extends SimpleModule {
if (!skipCuries && prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<>();
curies.add(curieProvider.getCurieInformation(Links.of(links)));
curies.addAll(curieProvider.getCurieInformation(Links.of(links)));
sortedLinks.put(HalLinkRelation.CURIES, curies);
}
@@ -420,40 +419,24 @@ public class Jackson2HalModule extends SimpleModule {
return;
}
if (list.size() == 1 && this.halConfiguration.getRenderSingleLinks() == RenderSingleLinks.AS_SINGLE) {
serializeContents(list.iterator(), jgen, provider);
Object firstElement = list.get(0);
if (!HalLink.class.isInstance(firstElement)) {
serializeContents(list, jgen, provider);
return;
}
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
}
HalLink halLink = HalLink.class.cast(firstElement);
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
if (list.size() == 1
&& halConfiguration.getSingleLinkRenderModeFor(halLink.getLink().getRel()).equals(RenderSingleLinks.AS_SINGLE)) {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
serializeContents(halLink, jgen, provider);
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider)
throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
return;
}
return serializer;
serializeContents(list, jgen, provider);
}
/*
@@ -504,6 +487,24 @@ public class Jackson2HalModule extends SimpleModule {
throws JsonMappingException {
return new OptionalListJackson2Serializer(property, halConfiguration);
}
private void serializeContents(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
getOrLookupSerializerFor(value, provider).serialize(value, jgen, provider);
}
private JsonSerializer<Object> getOrLookupSerializerFor(Object value, SerializerProvider provider)
throws JsonMappingException {
Class<? extends Object> type = value.getClass();
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
}
public static class HalLinkListDeserializer extends ContainerDeserializerBase<List<Link>> {