Skip to content

Commit b381d43

Browse files
committed
#270 - Embed single resources inside HAL documents
The HAL spec shows an example of a single resource, embedded at the top level. Spring HATEOAS, up until this point, has only supports embedded for the purposes of collections.
1 parent ad801e9 commit b381d43

File tree

7 files changed

+132
-7
lines changed

7 files changed

+132
-7
lines changed

src/main/java/org/springframework/hateoas/ResourceSupport.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Arrays;
20+
import java.util.HashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123
import java.util.Optional;
2224
import java.util.stream.Collectors;
2325

@@ -26,6 +28,7 @@
2628
import org.springframework.util.Assert;
2729

2830
import com.fasterxml.jackson.annotation.JsonIgnore;
31+
import com.fasterxml.jackson.annotation.JsonInclude;
2932
import com.fasterxml.jackson.annotation.JsonProperty;
3033

3134
/**
@@ -36,9 +39,11 @@
3639
public class ResourceSupport implements Identifiable<Link> {
3740

3841
private final List<Link> links;
42+
private final Map<String, ResourceSupport> embeddedResources;
3943

4044
public ResourceSupport() {
4145
this.links = new ArrayList<>();
46+
this.embeddedResources = new HashMap<String, ResourceSupport>();
4247
}
4348

4449
/**
@@ -151,6 +156,24 @@ public List<Link> getLinks(String rel) {
151156
.collect(Collectors.toList());
152157
}
153158

159+
public void addEmbeddedResource(String rel, ResourceSupport embeddableResource) {
160+
this.embeddedResources.put(rel, embeddableResource);
161+
}
162+
163+
public boolean hasEmbeddedResources() {
164+
return !this.embeddedResources.isEmpty();
165+
}
166+
167+
@JsonProperty("embedded")
168+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
169+
public Map<String, ResourceSupport> getEmbeddedResources() {
170+
return this.embeddedResources;
171+
}
172+
173+
public ResourceSupport getEmbeddedResource(String rel) {
174+
return this.embeddedResources.get(rel);
175+
}
176+
154177
/*
155178
* (non-Javadoc)
156179
* @see java.lang.Object#toString()
@@ -188,4 +211,5 @@ public boolean equals(Object obj) {
188211
public int hashCode() {
189212
return this.links.hashCode();
190213
}
214+
191215
}

src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.hateoas.hal;
1717

1818
import java.util.List;
19+
import java.util.Map;
1920

2021
import javax.xml.bind.annotation.XmlElement;
2122

@@ -37,4 +38,10 @@ abstract class ResourceSupportMixin extends ResourceSupport {
3738
@JsonSerialize(using = Jackson2HalModule.HalLinkListSerializer.class)
3839
@JsonDeserialize(using = Jackson2HalModule.HalLinkListDeserializer.class)
3940
public abstract List<Link> getLinks();
41+
42+
@Override
43+
@XmlElement(name = "embedded")
44+
@JsonProperty("_embedded")
45+
@JsonInclude(Include.NON_EMPTY)
46+
public abstract Map<String, ResourceSupport> getEmbeddedResources();
4047
}

src/test/java/org/springframework/hateoas/Jackson2ResourceIntegrationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
import static org.assertj.core.api.Assertions.*;
44

5+
import java.util.Arrays;
6+
import java.util.List;
7+
58
import org.junit.Test;
69

10+
import org.springframework.hateoas.support.Author;
11+
712
import com.fasterxml.jackson.annotation.JsonAutoDetect;
813

914
/**

src/test/java/org/springframework/hateoas/ResourceSupportUnitTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import lombok.AllArgsConstructor;
21+
import lombok.Data;
22+
23+
import java.util.ArrayList;
2024
import java.util.Arrays;
25+
import java.util.List;
2126

2227
import org.junit.Test;
2328

29+
import org.springframework.hateoas.support.Author;
30+
2431
/**
2532
* Unit tests for {@link ResourceSupport}.
2633
*
@@ -173,4 +180,29 @@ public void addsLinksViaVarargs() {
173180
assertThat(support.hasLink("self")).isTrue();
174181
assertThat(support.hasLink("another")).isTrue();
175182
}
183+
184+
@Test
185+
public void addEmbeddedResource() {
186+
187+
// given
188+
ResourceSupport support = new ResourceSupport();
189+
190+
Author author = new Author("Alan Watts", "January 6, 1915", "November 16, 1973");
191+
author.add(new Link("/people/alan-watts").withSelfRel());
192+
193+
// when
194+
support.addEmbeddedResource("author", author);
195+
support.add(new Link("/blog-post").withSelfRel(), new Link("/people/alan-watts").withRel("author"));
196+
197+
// then
198+
assertThat(support.hasLink("self")).isTrue();
199+
assertThat(support.hasLink("author")).isTrue();
200+
201+
assertThat(support.hasEmbeddedResources()).isTrue();
202+
assertThat(support.getEmbeddedResource("author").hasLink("self")).isTrue();
203+
assertThat(support.getEmbeddedResource("author").getLinks()).contains(new Link("/people/alan-watts", "self"));
204+
assertThat(((Author) support.getEmbeddedResource("author")).getName()).isEqualTo("Alan Watts");
205+
assertThat(((Author) support.getEmbeddedResource("author")).getBorn()).isEqualTo("January 6, 1915");
206+
assertThat(((Author) support.getEmbeddedResource("author")).getDied()).isEqualTo("November 16, 1973");
207+
}
176208
}

src/test/java/org/springframework/hateoas/client/TraversonTest.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
import static org.hamcrest.Matchers.*;
2121
import static org.springframework.hateoas.client.Hop.*;
2222

23+
import lombok.AllArgsConstructor;
24+
import lombok.Data;
25+
import lombok.NoArgsConstructor;
26+
2327
import java.io.IOException;
2428
import java.net.URI;
2529
import java.util.Arrays;
@@ -35,8 +39,12 @@
3539
import org.springframework.hateoas.Link;
3640
import org.springframework.hateoas.MediaTypes;
3741
import org.springframework.hateoas.Resource;
42+
import org.springframework.hateoas.Resources;
3843
import org.springframework.hateoas.client.Traverson.TraversalBuilder;
3944
import org.springframework.hateoas.core.JsonPathLinkDiscoverer;
45+
import org.springframework.hateoas.mvc.TypeReferences;
46+
import org.springframework.hateoas.mvc.TypeReferences.ResourceType;
47+
import org.springframework.hateoas.mvc.TypeReferences.ResourcesType;
4048
import org.springframework.http.HttpHeaders;
4149
import org.springframework.http.HttpRequest;
4250
import org.springframework.http.MediaType;
@@ -371,9 +379,9 @@ public void doesNotDoubleEncodeURI() {
371379

372380
this.traverson = new Traverson(URI.create(server.rootResource() + "/springagram"), MediaTypes.HAL_JSON);
373381

374-
Resource<?> itemResource = traverson.//
382+
Resources<?> itemResource = traverson.//
375383
follow(rel("items").withParameters(Collections.singletonMap("projection", "no images"))).//
376-
toObject(Resource.class);
384+
toObject(Resources.class);
377385

378386
assertThat(itemResource.hasLink("self")).isTrue();
379387
assertThat(itemResource.getRequiredLink("self").expand().getHref())

src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@
3737
import org.springframework.hateoas.PagedResources.PageMetadata;
3838
import org.springframework.hateoas.Resource;
3939
import org.springframework.hateoas.ResourceSupport;
40+
import org.springframework.hateoas.ResourceSupportUnitTest;
4041
import org.springframework.hateoas.Resources;
4142
import org.springframework.hateoas.UriTemplate;
4243
import org.springframework.hateoas.core.AnnotationRelProvider;
4344
import org.springframework.hateoas.core.EmbeddedWrappers;
4445
import org.springframework.hateoas.hal.HalConfiguration.RenderSingleLinks;
4546
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
47+
import org.springframework.hateoas.support.Author;
4648

4749
import com.fasterxml.jackson.databind.ObjectMapper;
4850

@@ -58,14 +60,14 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
5860
static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
5961
static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
6062

61-
static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"content\":[\"first\",\"second\"]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
62-
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
63-
static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
63+
static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}";
64+
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
65+
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\"}}}]}}";
6466

65-
static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
67+
static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
6668
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\"}}}]}}";
6769

68-
static final String ANNOTATED_PAGED_RESOURCES = "{\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"_links\":{\"next\":{\"href\":\"foo\"},\"prev\":{\"href\":\"bar\"}},\"page\":{\"size\":2,\"totalElements\":4,\"totalPages\":2,\"number\":0}}";
70+
static final String ANNOTATED_PAGED_RESOURCES = "{\"_links\":{\"next\":{\"href\":\"foo\"},\"prev\":{\"href\":\"bar\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"page\":{\"size\":2,\"totalElements\":4,\"totalPages\":2,\"number\":0}}";
6971

7072
static final Links PAGINATION_LINKS = new Links(new Link("foo", Link.REL_NEXT), new Link("bar", Link.REL_PREVIOUS));
7173

@@ -127,6 +129,20 @@ public void rendersWithOneExtraRFC5988Attribute() throws Exception {
127129
assertThat(write(resourceSupport)).isEqualTo(SINGLE_WITH_ONE_EXTRA_ATTRIBUTES);
128130
}
129131

132+
@Test
133+
public void rendersEmbeddedResourceSupportAsEmbedded() throws Exception {
134+
135+
ResourceSupport resourceSupport = new ResourceSupport();
136+
137+
Author author = new Author("Alan Watts", "January 6, 1915", "November 16, 1973");
138+
author.add(new Link("/people/alan-watts").withSelfRel());
139+
140+
resourceSupport.addEmbeddedResource("author", author);
141+
resourceSupport.add(new Link("/blog-post").withSelfRel(), new Link("/people/alan-watts").withRel("author"));
142+
143+
assertThat(write(resourceSupport)).isEqualTo("{\"_links\":{\"self\":{\"href\":\"/blog-post\"},\"author\":{\"href\":\"/people/alan-watts\"}},\"_embedded\":{\"author\":{\"name\":\"Alan Watts\",\"born\":\"January 6, 1915\",\"died\":\"November 16, 1973\",\"_links\":{\"self\":{\"href\":\"/people/alan-watts\"}}}}}");
144+
}
145+
130146
@Test
131147
public void deserializeSingleLink() throws Exception {
132148
ResourceSupport expected = new ResourceSupport();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.support;
17+
18+
import lombok.AllArgsConstructor;
19+
import lombok.Data;
20+
21+
import org.springframework.hateoas.ResourceSupport;
22+
23+
/**
24+
* @author Greg Turnquist
25+
*/
26+
@Data
27+
@AllArgsConstructor
28+
public class Author extends ResourceSupport {
29+
30+
private String name;
31+
private String born;
32+
private String died;
33+
}

0 commit comments

Comments
 (0)