From d77a4b65f5f670340de4e106e755b456c421e8bc Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 19 Aug 2015 16:35:04 +0200 Subject: [PATCH] #320 - Overhaul in curie rendering. The curie rendering has been improved in the following ways: - Resources now get their embeds inspected for namespaced keys and curies are added if at least one is found. This makes sure curies are also rendered if no namespaced link is registered. - Nested resources skip curies entirely, assuming they have been added to the root correctly. Had to change the order of _embedded and _links to make sure the embedded elements are processed first to tweak the links to indicate the need for curies. --- .../hateoas/hal/Jackson2HalModule.java | 146 ++++++++++++++---- .../hateoas/hal/ResourcesMixin.java | 2 + .../hal/Jackson2HalIntegrationTest.java | 35 ++--- 3 files changed, 131 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index 4fe4a058..87b5ebd3 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -78,6 +78,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory; public class Jackson2HalModule extends SimpleModule { private static final long serialVersionUID = 7806951456457932384L; + private static final Link CURIES_REQUIRED_DUE_TO_EMBEDS = new Link("__rel__", "¯\\_(ツ)_/¯"); public Jackson2HalModule() { @@ -106,20 +107,22 @@ public class Jackson2HalModule extends SimpleModule { * @author Alexander Baetz * @author Oliver Gierke */ - public static class HalLinkListSerializer extends ContainerSerializer> implements ContextualSerializer { + public static class HalLinkListSerializer extends ContainerSerializer>implements ContextualSerializer { private final BeanProperty property; private final CurieProvider curieProvider; + private final EmbeddedMapper mapper; - public HalLinkListSerializer(CurieProvider curieProvider) { - this(null, curieProvider); + public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper) { + this(null, curieProvider, mapper); } - public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider) { + public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper) { super(List.class, false); this.property = property; this.curieProvider = curieProvider; + this.mapper = mapper; } /* @@ -136,9 +139,22 @@ public class Jackson2HalModule extends SimpleModule { boolean prefixingRequired = curieProvider != null; boolean curiedLinkPresent = false; + boolean skipCuries = !jgen.getOutputContext().getParent().inRoot(); + + Object currentValue = jgen.getCurrentValue(); + + if (currentValue instanceof Resources) { + if (mapper.hasCuriedEmbed((Resources) currentValue)) { + curiedLinkPresent = true; + } + } for (Link link : value) { + if (link.equals(CURIES_REQUIRED_DUE_TO_EMBEDS)) { + continue; + } + String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel(); if (!link.getRel().equals(rel)) { @@ -153,7 +169,7 @@ public class Jackson2HalModule extends SimpleModule { sortedLinks.get(rel).add(link); } - if (prefixingRequired && curiedLinkPresent) { + if (!skipCuries && prefixingRequired && curiedLinkPresent) { ArrayList curies = new ArrayList(); curies.add(curieProvider.getCurieInformation(new Links(links))); @@ -179,7 +195,7 @@ public class Jackson2HalModule extends SimpleModule { @Override public JsonSerializer createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException { - return new HalLinkListSerializer(property, curieProvider); + return new HalLinkListSerializer(property, curieProvider, mapper); } /* @@ -234,27 +250,21 @@ public class Jackson2HalModule extends SimpleModule { * @author Alexander Baetz * @author Oliver Gierke */ - public static class HalResourcesSerializer extends ContainerSerializer> implements ContextualSerializer { + public static class HalResourcesSerializer extends ContainerSerializer>implements ContextualSerializer { private final BeanProperty property; - private final RelProvider relProvider; - private final CurieProvider curieProvider; - private final boolean enforceEmbeddedCollections; + private final EmbeddedMapper embeddedMapper; - public HalResourcesSerializer(RelProvider relPorvider, CurieProvider curieProvider, - boolean enforceEmbeddedCollections) { - this(null, relPorvider, curieProvider, enforceEmbeddedCollections); + public HalResourcesSerializer(EmbeddedMapper embeddedMapper) { + this(null, embeddedMapper); } - public HalResourcesSerializer(BeanProperty property, RelProvider relProvider, CurieProvider curieProvider, - boolean enforceEmbeddedCollections) { + public HalResourcesSerializer(BeanProperty property, EmbeddedMapper embeddedMapper) { super(Collection.class, false); this.property = property; - this.relProvider = relProvider; - this.curieProvider = curieProvider; - this.enforceEmbeddedCollections = enforceEmbeddedCollections; + this.embeddedMapper = embeddedMapper; } /* @@ -264,22 +274,27 @@ public class Jackson2HalModule extends SimpleModule { * org.codehaus.jackson.map.SerializerProvider) */ @Override - public void serialize(Collection value, JsonGenerator jgen, SerializerProvider provider) throws IOException, - JsonGenerationException { + public void serialize(Collection value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonGenerationException { - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, curieProvider, enforceEmbeddedCollections); + Map embeddeds = embeddedMapper.map(value); - for (Object resource : value) { - builder.add(resource); + Object currentValue = jgen.getCurrentValue(); + + if (currentValue instanceof ResourceSupport) { + + if (embeddedMapper.hasCuriedEmbed(value)) { + ((ResourceSupport) currentValue).add(CURIES_REQUIRED_DUE_TO_EMBEDS); + } } - provider.findValueSerializer(Map.class, property).serialize(builder.asMap(), jgen, provider); + provider.findValueSerializer(Map.class, property).serialize(embeddeds, jgen, provider); } @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - return new HalResourcesSerializer(property, relProvider, curieProvider, enforceEmbeddedCollections); + return new HalResourcesSerializer(property, embeddedMapper); } @Override @@ -554,8 +569,8 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) */ @Override - public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, - JsonProcessingException { + public List deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { List result = new ArrayList(); JsonDeserializer deser = ctxt.findRootValueDeserializer(contentType); @@ -600,12 +615,14 @@ public class Jackson2HalModule extends SimpleModule { this(resolver, curieProvider, true); } - public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, boolean enforceEmbeddedCollections) { + public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, + boolean enforceEmbeddedCollections) { + + EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections); Assert.notNull(resolver, "RelProvider must not be null!"); - this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(resolver, curieProvider, - enforceEmbeddedCollections)); - this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider)); + this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper)); + this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper)); } private Object findInstance(Class type) { @@ -724,4 +741,69 @@ public class Jackson2HalModule extends SimpleModule { } } } + + /** + * Helper to easily map embedded resources and find out whether they were curied. + * + * @author Oliver Gierke + */ + private static class EmbeddedMapper { + + private RelProvider relProvider; + private CurieProvider curieProvider; + private boolean preferCollectionRels; + + /** + * Creates a new {@link EmbeddedMapper} for the given {@link RelProvider}, {@link CurieProvider} and flag whether to + * prefer collection relations. + * + * @param relProvider must not be {@literal null}. + * @param curieProvider can be {@literal null}. + * @param preferCollectionRels + */ + public EmbeddedMapper(RelProvider relProvider, CurieProvider curieProvider, boolean preferCollectionRels) { + + Assert.notNull(relProvider, "RelProvider must not be null!"); + + this.relProvider = relProvider; + this.curieProvider = curieProvider; + this.preferCollectionRels = preferCollectionRels; + } + + /** + * Maps the given source elements as embedded values. + * + * @param source must not be {@literal null}. + * @return + */ + public Map map(Iterable source) { + + Assert.notNull(source, "Elements must not be null!"); + + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, curieProvider, preferCollectionRels); + + for (Object resource : source) { + builder.add(resource); + } + + return builder.asMap(); + } + + /** + * Returns whether the given source elements will be namespaced. + * + * @param source must not be {@literal null}. + * @return + */ + public boolean hasCuriedEmbed(Iterable source) { + + for (String rel : map(source).keySet()) { + if (rel.contains(":")) { + return true; + } + } + + return false; + } + } } diff --git a/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java index e95d0735..941ec600 100644 --- a/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java @@ -22,9 +22,11 @@ import javax.xml.bind.annotation.XmlElement; import org.springframework.hateoas.Resources; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +@JsonPropertyOrder({ "content", "links" }) public abstract class ResourcesMixin extends Resources { @Override diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index 1ff09e28..9ef8d147 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -52,14 +52,14 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}"; static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}"; - static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}"; - static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; - static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; + static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"content\":[\"first\",\"second\"]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}"; + static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}"; + static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}"; - static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; + static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}"; static final String ANNOTATED_EMBEDDED_RESOURCES_REFERENCE = "{\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; - static final String ANNOTATED_PAGED_RESOURCES = "{\"_links\":{\"next\":{\"href\":\"foo\"},\"prev\":{\"href\":\"bar\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"page\":{\"size\":2,\"totalElements\":4,\"totalPages\":2,\"number\":0}}"; + static final String ANNOTATED_PAGED_RESOURCES = "{\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"next\":{\"href\":\"foo\"},\"prev\":{\"href\":\"bar\"}},\"page\":{\"size\":2,\"totalElements\":4,\"totalPages\":2,\"number\":0}}"; static final Links PAGINATION_LINKS = new Links(new Link("foo", Link.REL_NEXT), new Link("bar", Link.REL_PREVIOUS)); @@ -142,8 +142,8 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg Resources expected = new Resources(content); expected.add(new Link("localhost")); - Resources result = mapper.readValue(SIMPLE_EMBEDDED_RESOURCE_REFERENCE, mapper.getTypeFactory() - .constructParametricType(Resources.class, String.class)); + Resources result = mapper.readValue(SIMPLE_EMBEDDED_RESOURCE_REFERENCE, + mapper.getTypeFactory().constructParametricType(Resources.class, String.class)); assertThat(result, is(expected)); @@ -170,8 +170,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg Resources> expected = new Resources>(content); expected.add(new Link("localhost")); - Resources> result = mapper.readValue( - SINGLE_EMBEDDED_RESOURCE_REFERENCE, + Resources> result = mapper.readValue(SINGLE_EMBEDDED_RESOURCE_REFERENCE, mapper.getTypeFactory().constructParametricType(Resources.class, mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); @@ -194,8 +193,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg Resources> expected = setupResources(); expected.add(new Link("localhost")); - Resources> result = mapper.readValue( - LIST_EMBEDDED_RESOURCE_REFERENCE, + Resources> result = mapper.readValue(LIST_EMBEDDED_RESOURCE_REFERENCE, mapper.getTypeFactory().constructParametricType(Resources.class, mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); @@ -229,8 +227,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg Resources> expected = new Resources>(content); expected.add(new Link("localhost")); - Resources> result = mapper.readValue( - ANNOTATED_EMBEDDED_RESOURCE_REFERENCE, + Resources> result = mapper.readValue(ANNOTATED_EMBEDDED_RESOURCE_REFERENCE, mapper.getTypeFactory().constructParametricType(Resources.class, mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); @@ -251,8 +248,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg @Test public void deserializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { - Resources> result = mapper.readValue( - ANNOTATED_EMBEDDED_RESOURCES_REFERENCE, + Resources> result = mapper.readValue(ANNOTATED_EMBEDDED_RESOURCES_REFERENCE, mapper.getTypeFactory().constructParametricType(Resources.class, mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); @@ -272,8 +268,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg */ @Test public void deserializesPagedResource() throws Exception { - PagedResources> result = mapper.readValue( - ANNOTATED_PAGED_RESOURCES, + PagedResources> result = mapper.readValue(ANNOTATED_PAGED_RESOURCES, mapper.getTypeFactory().constructParametricType(PagedResources.class, mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); @@ -286,8 +281,8 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg @Test public void rendersCuriesCorrectly() throws Exception { - Resources resources = new Resources(Collections.emptySet(), new Link("foo"), new Link("bar", - "myrel")); + Resources resources = new Resources(Collections.emptySet(), new Link("foo"), + new Link("bar", "myrel")); assertThat(getCuriedObjectMapper().writeValueAsString(resources), is(CURIED_DOCUMENT)); }