Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for embedding HAL resources. serialization only. #428

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/main/java/org/springframework/hateoas/EmbeddedResource.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
43 changes: 36 additions & 7 deletions src/main/java/org/springframework/hateoas/ResourceSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -35,8 +39,11 @@ public class ResourceSupport implements Identifiable<Link> {

private final List<Link> links;

private final List<EmbeddedResource> embeddedResources;

public ResourceSupport() {
this.links = new ArrayList<Link>();
embeddedResources = new ArrayList<EmbeddedResource>();
}

/**
Expand All @@ -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<Link> links) {
Assert.notNull(links, "Given links must not be null!");
for (Link candidate : links) {
add(candidate);
public void add(Iterable linksOrEmdeddedResource) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a compelling reason to avoid strong typing here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You cannot have both add(Iterable) and add(Iterable) as explained here: http://stackoverflow.com/questions/1998544/method-has-the-same-erasure-as-another-method-in-type
Leaving all add methods for Link and having addEmbeddeResource would kind of break a naming convention.
In my opinion it would be better to deprecate add in favour of addLink(s) and addEmbeddedResource(s). However this would be a change of a public interface of the class, so I thought it was not suitable for a simple PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO addEmbedded or addEmbeddedResource is a justified addition to the API.

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

Expand Down Expand Up @@ -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<EmbeddedResource> getEmbeddedResources() {
return embeddedResources;
}

/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Collection<EmbeddedResource>> 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<EmbeddedResource> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {

Map<String, Object> embeddeds = new HashMap<String, Object>();
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<EmbeddedResource> getContentSerializer() {
return null;
}

public boolean isEmpty(Collection<EmbeddedResource> value) {
return isEmpty(null, value);
}

public boolean isEmpty(SerializerProvider provider, Collection<EmbeddedResource> value) {
return value.isEmpty();
}

@Override
public boolean hasSingleElement(Collection<EmbeddedResource> value) {
return value.size() == 1;
}

@Override
protected ContainerSerializer<EmbeddedResource> _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.
Expand Down Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -37,4 +38,10 @@ abstract class ResourceSupportMixin extends ResourceSupport {
@JsonSerialize(using = Jackson2HalModule.HalLinkListSerializer.class)
@JsonDeserialize(using = Jackson2HalModule.HalLinkListDeserializer.class)
public abstract List<Link> getLinks();

@Override
@JsonProperty("_embedded")
@JsonInclude(Include.NON_EMPTY)
@JsonSerialize(using = Jackson2HalModule.HalEmbeddedResourcesSerializer.class)
public abstract List<EmbeddedResource> getEmbeddedResources();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,4 +40,8 @@ public abstract class ResourcesMixin<T> extends Resources<T> {
@JsonDeserialize(using = Jackson2HalModule.HalResourcesDeserializer.class)
public abstract Collection<T> getContent();

@Override
@JsonIgnore
public abstract List<EmbeddedResource> getEmbeddedResources();

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {

Expand Down Expand Up @@ -379,6 +385,52 @@ public void rendersTitleIfMessageSourceResolvesLocalKey() throws Exception {
verifyResolvedTitle("_links.foobar.title");
}

@Test
public void rendersResourceWithSingleEmbeddedResource() throws Exception {

Resource<SimplePojo> simplePojoResource = new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost"));
simplePojoResource.add(new EmbeddedResource("related", new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost"))));

assertThat(write(simplePojoResource), is(RESOURCE_WITH_SINGLE_EMBEDDED_RESOURCE_REFERENCE));
}

@Test
public void rendersResourceWithSingleEmbeddedResourceCollection() throws Exception {

Resource<SimplePojo> simplePojoResource = new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost"));
simplePojoResource.add(new EmbeddedResource(
"relatedCollection",
Arrays.asList(
new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")),
new Resource<SimplePojo>(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<SimplePojo> simplePojoResource = new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost"));
simplePojoResource.add(new EmbeddedResource("related", new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost"))));
simplePojoResource.add(new EmbeddedResource(
"relatedCollection",
Arrays.asList(
new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")),
new Resource<SimplePojo>(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);
Expand Down