From 6126b81ca4bdca30e190de06ed1195f88b5c1db6 Mon Sep 17 00:00:00 2001 From: Tomasz Wielga Date: Fri, 22 Jan 2016 16:56:55 +0100 Subject: [PATCH] support for embedding HAL resources. serialization only. --- .../hateoas/EmbeddedResource.java | 20 ++++++ .../hateoas/ResourceSupport.java | 43 +++++++++-- .../hateoas/hal/Jackson2HalModule.java | 71 +++++++++++++++++++ .../hateoas/hal/ResourceSupportMixin.java | 7 ++ .../hateoas/hal/ResourcesMixin.java | 8 +++ .../hateoas/client/TraversonTest.java | 5 +- .../hal/Jackson2HalIntegrationTest.java | 52 ++++++++++++++ 7 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/EmbeddedResource.java diff --git a/src/main/java/org/springframework/hateoas/EmbeddedResource.java b/src/main/java/org/springframework/hateoas/EmbeddedResource.java new file mode 100644 index 000000000..d2997377e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/EmbeddedResource.java @@ -0,0 +1,20 @@ +package org.springframework.hateoas; + +public class EmbeddedResource { + + private String rel; + private Object resource; + + public EmbeddedResource(String rel, Object resource) { + this.rel = rel; + this.resource = resource; + } + + public String getRel() { + return rel; + } + + public Object getResource() { + return resource; + } +} diff --git a/src/main/java/org/springframework/hateoas/ResourceSupport.java b/src/main/java/org/springframework/hateoas/ResourceSupport.java index 64de72076..ea5ee6be1 100755 --- a/src/main/java/org/springframework/hateoas/ResourceSupport.java +++ b/src/main/java/org/springframework/hateoas/ResourceSupport.java @@ -17,14 +17,18 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.xml.bind.annotation.XmlElement; +import com.fasterxml.jackson.annotation.JsonInclude; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.util.CollectionUtils; /** * Base class for DTOs to collect links. @@ -35,8 +39,11 @@ public class ResourceSupport implements Identifiable { private final List links; + private final List embeddedResources; + public ResourceSupport() { this.links = new ArrayList(); + embeddedResources = new ArrayList(); } /** @@ -58,14 +65,21 @@ public void add(Link link) { } /** - * Adds all given {@link Link}s to the resource. + * Adds all given {@link Link}s or {@link EmbeddedResource}s to the resource. * - * @param links + * @param linksOrEmdeddedResource */ - public void add(Iterable links) { - Assert.notNull(links, "Given links must not be null!"); - for (Link candidate : links) { - add(candidate); + public void add(Iterable linksOrEmdeddedResource) { + Assert.notNull(linksOrEmdeddedResource, "Given objects must not be null!"); + for (Object candidate : linksOrEmdeddedResource) { + if (candidate instanceof Link) { + add((Link) candidate); + } else if (candidate instanceof EmbeddedResource) { + add((EmbeddedResource) candidate); + } else { + throw new ClassCastException( + "Only " + Link.class.getName() + " or " + EmbeddedResource.class.getName() + " allowed"); + } } } @@ -133,7 +147,22 @@ public Link getLink(String rel) { return null; } - /* + public void add(EmbeddedResource embedded) { + Assert.notNull(embedded, "Resource must not be null!"); + this.embeddedResources.add(embedded); + } + + public void add(EmbeddedResource... embeddedResources) { + this.embeddedResources.addAll(Arrays.asList(embeddedResources)); + } + + @JsonProperty("embedded") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public List getEmbeddedResources() { + return embeddedResources; + } + + /* * (non-Javadoc) * @see java.lang.Object#toString() */ diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index bb0f4a822..d855b7b3b 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -28,6 +28,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.context.NoSuchMessageException; import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.hateoas.EmbeddedResource; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.RelProvider; @@ -382,6 +383,75 @@ protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { } } + /** + * Custom {@link JsonSerializer} to render {@link EmbeddedResource}s in HAL compatible JSON. Renders the list as a Map. + * + * @author Tomasz Wielga + */ + public static class HalEmbeddedResourcesSerializer extends ContainerSerializer> implements ContextualSerializer { + + private final BeanProperty property; + + public HalEmbeddedResourcesSerializer() { + this(null); + } + + public HalEmbeddedResourcesSerializer(BeanProperty property) { + + super(Collection.class, false); + + this.property = property; + } + + @Override + public void serialize(Collection value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonGenerationException { + + Map embeddeds = new HashMap(); + for (EmbeddedResource embedded : value) { + embeddeds.put(embedded.getRel(), embedded.getResource()); + } + + Object currentValue = jgen.getCurrentValue(); + + provider.findValueSerializer(Map.class, property).serialize(embeddeds, jgen, provider); + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException { + return new HalEmbeddedResourcesSerializer(property); + } + + @Override + public JavaType getContentType() { + return null; + } + + @Override + public JsonSerializer getContentSerializer() { + return null; + } + + public boolean isEmpty(Collection value) { + return isEmpty(null, value); + } + + public boolean isEmpty(SerializerProvider provider, Collection value) { + return value.isEmpty(); + } + + @Override + public boolean hasSingleElement(Collection value) { + return value.size() == 1; + } + + @Override + protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { + return null; + } + } + /** * Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. Renders the {@link Link} as * immediate object if we have a single one or as array if we have multiple ones. @@ -689,6 +759,7 @@ public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, Assert.notNull(resolver, "RelProvider must not be null!"); this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper)); + this.instanceMap.put(HalEmbeddedResourcesSerializer.class, new HalEmbeddedResourcesSerializer()); this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper, messageSource)); } diff --git a/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java b/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java index 70a19114e..ad3413cb5 100644 --- a/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java @@ -19,6 +19,7 @@ import javax.xml.bind.annotation.XmlElement; +import org.springframework.hateoas.EmbeddedResource; import org.springframework.hateoas.Link; import org.springframework.hateoas.ResourceSupport; @@ -37,4 +38,10 @@ abstract class ResourceSupportMixin extends ResourceSupport { @JsonSerialize(using = Jackson2HalModule.HalLinkListSerializer.class) @JsonDeserialize(using = Jackson2HalModule.HalLinkListDeserializer.class) public abstract List getLinks(); + + @Override + @JsonProperty("_embedded") + @JsonInclude(Include.NON_EMPTY) + @JsonSerialize(using = Jackson2HalModule.HalEmbeddedResourcesSerializer.class) + public abstract List getEmbeddedResources(); } diff --git a/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java index 941ec600c..65307693a 100644 --- a/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java @@ -16,9 +16,13 @@ package org.springframework.hateoas.hal; import java.util.Collection; +import java.util.List; import javax.xml.bind.annotation.XmlElement; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import org.springframework.hateoas.EmbeddedResource; import org.springframework.hateoas.Resources; import com.fasterxml.jackson.annotation.JsonProperty; @@ -36,4 +40,8 @@ public abstract class ResourcesMixin extends Resources { @JsonDeserialize(using = Jackson2HalModule.HalResourcesDeserializer.class) public abstract Collection getContent(); + @Override + @JsonIgnore + public abstract List getEmbeddedResources(); + } diff --git a/src/test/java/org/springframework/hateoas/client/TraversonTest.java b/src/test/java/org/springframework/hateoas/client/TraversonTest.java index 0f3075720..7f58b884c 100644 --- a/src/test/java/org/springframework/hateoas/client/TraversonTest.java +++ b/src/test/java/org/springframework/hateoas/client/TraversonTest.java @@ -35,6 +35,7 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; import org.springframework.hateoas.client.Traverson.TraversalBuilder; import org.springframework.hateoas.core.JsonPathLinkDiscoverer; import org.springframework.http.HttpHeaders; @@ -364,9 +365,9 @@ public void doesNotDoubleEncodeURI() { this.traverson = new Traverson(URI.create(server.rootResource() + "/springagram"), MediaTypes.HAL_JSON); - Resource itemResource = traverson.// + Resources itemResource = traverson.// follow(rel("items").withParameters(Collections.singletonMap("projection", "no images"))).// - toObject(Resource.class); + toObject(Resources.class); assertThat(itemResource.hasLink("self"), is(true)); assertThat(itemResource.getLink("self").expand().getHref(), diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index df858c8df..01fd82afe 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -26,12 +26,14 @@ import java.util.Locale; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.StaticMessageSource; import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest; +import org.springframework.hateoas.EmbeddedResource; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.PagedResources; @@ -77,6 +79,10 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg static final String LINK_WITH_TITLE = "{\"_links\":{\"ns:foobar\":{\"href\":\"target\",\"title\":\"Foobar's title!\"}}}"; + static final String RESOURCE_WITH_SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"related\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}"; + static final String RESOURCE_WITH_SINGLE_EMBEDDED_RESOURCE_COLLECTION_REFERENCE = "{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"relatedCollection\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; + static final String RESOURCE_WITH_MULTIPLE_EMBEDDED_RESOURCES_REFERENCE = "{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"related\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},\"relatedCollection\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; + @Before public void setUpModule() { @@ -379,6 +385,52 @@ public void rendersTitleIfMessageSourceResolvesLocalKey() throws Exception { verifyResolvedTitle("_links.foobar.title"); } + @Test + public void rendersResourceWithSingleEmbeddedResource() throws Exception { + + Resource simplePojoResource = new Resource(new SimplePojo("test1", 1), new Link("localhost")); + simplePojoResource.add(new EmbeddedResource("related", new Resource(new SimplePojo("test1", 1), new Link("localhost")))); + + assertThat(write(simplePojoResource), is(RESOURCE_WITH_SINGLE_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void rendersResourceWithSingleEmbeddedResourceCollection() throws Exception { + + Resource simplePojoResource = new Resource(new SimplePojo("test1", 1), new Link("localhost")); + simplePojoResource.add(new EmbeddedResource( + "relatedCollection", + Arrays.asList( + new Resource(new SimplePojo("test1", 1), new Link("localhost")), + new Resource(new SimplePojo("test1", 1), new Link("localhost")) + ) + )); + + assertThat(write(simplePojoResource), is(RESOURCE_WITH_SINGLE_EMBEDDED_RESOURCE_COLLECTION_REFERENCE)); + } + + @Test + public void rendersResourceWithMultipleEmbeddedResources() throws Exception { + + Resource simplePojoResource = new Resource(new SimplePojo("test1", 1), new Link("localhost")); + simplePojoResource.add(new EmbeddedResource("related", new Resource(new SimplePojo("test1", 1), new Link("localhost")))); + simplePojoResource.add(new EmbeddedResource( + "relatedCollection", + Arrays.asList( + new Resource(new SimplePojo("test1", 1), new Link("localhost")), + new Resource(new SimplePojo("test1", 1), new Link("localhost")) + ) + )); + + assertThat(write(simplePojoResource), is(RESOURCE_WITH_MULTIPLE_EMBEDDED_RESOURCES_REFERENCE)); + } + + @Ignore("The functionality not yet implemented") + @Test + public void deserializesSingleEmbeddedResource() throws Exception { + } + + private static void verifyResolvedTitle(String resourceBundleKey) throws Exception { LocaleContextHolder.setLocale(Locale.US);