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)); }