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