From 0df0f2aab881edd1e2b50979060a904beac7e390 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 7 May 2013 19:08:03 +0200 Subject: [PATCH] #63 - Use collection relation types for embedded collections. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When embedding values into HAL representations we now correctly expose the collection relation type as exposed by RelProvider.getCollectionResourceRelFor(…). Added EmbeddedHalBuilder to ease building these kinds of nested maps easily and use the commonly shared code between Jackson 1 and 2 implementations. --- .../hateoas/hal/HalEmbeddedBuilder.java | 91 +++++++++++++++++++ .../hateoas/hal/Jackson1HalModule.java | 24 +---- .../hateoas/hal/Jackson2HalModule.java | 29 +----- .../hal/Jackson1HalIntegrationTest.java | 53 +++++++++-- .../hal/Jackson2HalIntegrationTest.java | 54 ++++++++--- .../hateoas/hal/SimpleAnnotatedPojo.java | 2 +- 6 files changed, 185 insertions(+), 68 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java diff --git a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java new file mode 100644 index 00000000..8fea5533 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.hal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.core.ObjectUtils; + +/** + * Builder class that allows collecting objects under the relation types defined for the objects but moving from the + * single resource relation to the collection one, once more than one object of the same type is added. + * + * @author Oliver Gierke + */ +class HalEmbeddedBuilder { + + private static final String DEFAULT_REL = "content"; + + private final Map> embeddeds = new HashMap>(); + private final RelProvider provider; + + /** + * Creates a new {@link HalEmbeddedBuilder} using the given {@link RelProvider}. + * + * @param provider can be {@literal null}. + */ + public HalEmbeddedBuilder(RelProvider provider) { + this.provider = provider; + } + + /** + * Adds the given value to the embeddeds. + * + * @param value + */ + public void add(Object value) { + + Class type = ObjectUtils.getResourceType(value); + String singleRel = getDefaultedRelFor(type, false); + List currentValue = embeddeds.get(singleRel); + + if (currentValue == null) { + ArrayList arrayList = new ArrayList(); + arrayList.add(value); + embeddeds.put(singleRel, arrayList); + } else if (currentValue.size() == 1) { + currentValue.add(value); + embeddeds.remove(singleRel); + embeddeds.put(getDefaultedRelFor(type, true), currentValue); + } else { + currentValue.add(value); + } + } + + private String getDefaultedRelFor(Class type, boolean forCollection) { + + if (provider == null) { + return DEFAULT_REL; + } + + String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getSingleResourceRelFor(type); + return rel == null ? DEFAULT_REL : rel; + } + + /** + * Returns the added objects keyed up by their relation types. + * + * @return + */ + public Map> asMap() { + return Collections.unmodifiableMap(embeddeds); + } +} diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java index 202e1e58..b6330b4f 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java @@ -59,7 +59,6 @@ import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; -import org.springframework.hateoas.core.ObjectUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -170,10 +169,6 @@ public class Jackson1HalModule extends SimpleModule { private final BeanProperty property; private final RelProvider relProvider; - public HalResourcesSerializer() { - this(null); - } - /** * Creates a new {@link HalLinkListSerializer}. */ @@ -196,23 +191,10 @@ public class Jackson1HalModule extends SimpleModule { public void serialize(Collection value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { - // sort resources according to their types - Map> sortedLinks = new HashMap>(); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider); for (Object resource : value) { - - Class type = ObjectUtils.getResourceType(resource); - String relation = relProvider == null ? "content" : relProvider.getSingleResourceRelFor(type); - - if (relation == null) { - relation = "content"; - } - - if (sortedLinks.get(relation) == null) { - sortedLinks.put(relation, new ArrayList()); - } - - sortedLinks.get(relation).add(resource); + builder.add(resource); } TypeFactory typeFactory = provider.getConfig().getTypeFactory(); @@ -223,7 +205,7 @@ public class Jackson1HalModule extends SimpleModule { MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null, null, provider.findKeySerializer(keyType, null), new OptionalListSerializer(property)); - serializer.serialize(sortedLinks, jgen, provider); + serializer.serialize(builder.asMap(), jgen, provider); } /* diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index edc3db2e..1a5c5231 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -29,7 +29,6 @@ import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; -import org.springframework.hateoas.core.ObjectUtils; import org.springframework.util.Assert; import com.fasterxml.jackson.core.JsonGenerationException; @@ -204,18 +203,9 @@ public class Jackson2HalModule extends SimpleModule { */ public static class HalResourcesSerializer extends ContainerSerializer> implements ContextualSerializer { - private static final String DEFAULT_REL = "content"; - private final BeanProperty property; private final RelProvider relProvider; - /** - * Creates a new {@link HalLinkListSerializer}. - */ - public HalResourcesSerializer() { - this(null); - } - public HalResourcesSerializer(RelProvider relPorvider) { this(null, relPorvider); } @@ -223,6 +213,7 @@ public class Jackson2HalModule extends SimpleModule { public HalResourcesSerializer(BeanProperty property, RelProvider relProvider) { super(Collection.class, false); + this.property = property; this.relProvider = relProvider; } @@ -237,22 +228,10 @@ public class Jackson2HalModule extends SimpleModule { public void serialize(Collection value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { - // sort resources according to their types - Map> sortedLinks = new HashMap>(); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider); for (Object resource : value) { - - Class type = ObjectUtils.getResourceType(resource); - String relation = relProvider == null ? DEFAULT_REL : relProvider.getSingleResourceRelFor(type); - - if (relation == null) { - relation = DEFAULT_REL; - } - - if (sortedLinks.get(relation) == null) { - sortedLinks.put(relation, new ArrayList()); - } - sortedLinks.get(relation).add(resource); + builder.add(resource); } TypeFactory typeFactory = provider.getConfig().getTypeFactory(); @@ -263,7 +242,7 @@ public class Jackson2HalModule extends SimpleModule { MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null, provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property)); - serializer.serialize(sortedLinks, jgen, provider); + serializer.serialize(builder.asMap(), jgen, provider); } @Override diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java index 84b97306..441b72ca 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java @@ -47,6 +47,7 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe 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 ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojo\":{\"text\":\"test1\",\"number\":1,\"_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\"}}}]}}"; @Before public void setUpModule() { @@ -160,11 +161,7 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe @Test public void rendersMultipleResourceResourcesAsEmbedded() throws Exception { - List> content = new ArrayList>(); - content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); - content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); - - Resources> resources = new Resources>(content); + Resources> resources = setupResources(); resources.add(new Link("localhost")); assertThat(write(resources), is(LIST_EMBEDDED_RESOURCE_REFERENCE)); @@ -173,11 +170,7 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe @Test public void deserializeMultipleResourceResourcesAsEmbedded() throws Exception { - List> content = new ArrayList>(); - content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); - content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); - - Resources> expected = new Resources>(content); + Resources> expected = setupResources(); expected.add(new Link("localhost")); Resources> result = mapper.readValue( @@ -216,4 +209,44 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe assertThat(result, is(expected)); } + + /** + * @see #63 + */ + @Test + public void serializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { + assertThat(write(setupAnnotatedResources()), is(ANNOTATED_EMBEDDED_RESOURCES_REFERENCE)); + } + + /** + * @see #63 + */ + @Test + public void deserializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { + + Resources> result = mapper.readValue( + ANNOTATED_EMBEDDED_RESOURCES_REFERENCE, + mapper.getTypeFactory().constructParametricType(Resources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + + assertThat(result, is(setupAnnotatedResources())); + } + + private static Resources> setupAnnotatedResources() { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimpleAnnotatedPojo("test2", 2), new Link("localhost"))); + + return new Resources>(content); + } + + private static Resources> setupResources() { + + List> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); + + return new Resources>(content); + } } diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index 595303dc..6fe7497b 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -47,6 +47,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg 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 ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojo\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}"; + static final String ANNOTATED_EMBEDDED_RESOURCES_REFERENCE = "{\"_links\":{},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; @Before public void setUpModule() { @@ -160,11 +161,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg @Test public void rendersMultipleResourceResourcesAsEmbedded() throws Exception { - List> content = new ArrayList>(); - content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); - content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); - - Resources> resources = new Resources>(content); + Resources> resources = setupResources(); resources.add(new Link("localhost")); assertThat(write(resources), is(LIST_EMBEDDED_RESOURCE_REFERENCE)); @@ -173,11 +170,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg @Test public void deserializesMultipleResourceResourcesAsEmbedded() throws Exception { - List> content = new ArrayList>(); - content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); - content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); - - Resources> expected = new Resources>(content); + Resources> expected = setupResources(); expected.add(new Link("localhost")); Resources> result = mapper.readValue( @@ -186,7 +179,6 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); assertThat(result, is(expected)); - } /** @@ -223,4 +215,44 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg assertThat(result, is(expected)); } + + /** + * @see #63 + */ + @Test + public void serializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { + assertThat(write(setupAnnotatedResources()), is(ANNOTATED_EMBEDDED_RESOURCES_REFERENCE)); + } + + /** + * @see #63 + */ + @Test + public void deserializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { + + Resources> result = mapper.readValue( + ANNOTATED_EMBEDDED_RESOURCES_REFERENCE, + mapper.getTypeFactory().constructParametricType(Resources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + + assertThat(result, is(setupAnnotatedResources())); + } + + private static Resources> setupAnnotatedResources() { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimpleAnnotatedPojo("test2", 2), new Link("localhost"))); + + return new Resources>(content); + } + + private static Resources> setupResources() { + + List> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); + + return new Resources>(content); + } } diff --git a/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java b/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java index f611a344..27dbb8d1 100644 --- a/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java +++ b/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java @@ -2,7 +2,7 @@ package org.springframework.hateoas.hal; import org.springframework.hateoas.core.Relation; -@Relation(value = "pojo", collectionRelation = "pojo") +@Relation(value = "pojo", collectionRelation = "pojos") public class SimpleAnnotatedPojo extends SimplePojo { public SimpleAnnotatedPojo() {