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() {