Use HAL in the Spring HATEOAS-based sample

This commit is contained in:
Andy Wilkinson
2014-10-29 12:29:22 +00:00
parent 34dcef2201
commit 0574699fad
5 changed files with 84 additions and 47 deletions

View File

@@ -31,6 +31,8 @@ dependencies {
compile 'org.springframework.hateoas:spring-hateoas'
runtime 'com.h2database:h2'
runtime 'org.springframework.plugin:spring-plugin-core:1.1.0.RELEASE'
runtime 'org.atteo:evo-inflector:1.2'
documentationCompile 'com.jayway.jsonpath:json-path'
documentationCompile 'org.springframework.boot:spring-boot-starter-test'

View File

@@ -16,16 +16,16 @@
package com.example.notes;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.restdocs.core.RestDocumentation.document;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.restdocs.core.RestDocumentation.document;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
@@ -38,7 +38,6 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.restdocs.core.RestDocumentationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -74,11 +73,10 @@ public class GettingStartedDocumentation {
public void index() throws Exception {
document(
"index",
this.mockMvc
.perform(get("/").accept(MediaTypes.HAL_JSON))
this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("links[?(@.rel==notes)]", is(notNullValue())))
.andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue()))));
.andExpect(jsonPath("_links.notes", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue()))));
}
@Test
@@ -121,7 +119,7 @@ public class GettingStartedDocumentation {
this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue()))));
.andExpect(jsonPath("_links.tags", is(notNullValue()))));
}
String createTag() throws Exception, JsonProcessingException {
@@ -143,11 +141,9 @@ public class GettingStartedDocumentation {
void getTag(String tagLocation) throws Exception {
document(
"get-tag",
this.mockMvc
.perform(get(tagLocation))
.andExpect(status().isOk())
this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk())
.andExpect(jsonPath("name", is(notNullValue())))
.andExpect(jsonPath("links[?(@.rel==notes)]", is(notNullValue()))));
.andExpect(jsonPath("_links.notes", is(notNullValue()))));
}
String createTaggedNote(String tag) throws Exception {
@@ -160,8 +156,8 @@ public class GettingStartedDocumentation {
"create-tagged-note",
this.mockMvc
.perform(
post("/notes").contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(note)))
post("/notes").contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue())))
.andReturn().getResponse().getHeader("Location");
@@ -174,7 +170,7 @@ public class GettingStartedDocumentation {
this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue()))));
.andExpect(jsonPath("_links.tags", is(notNullValue()))));
}
void getTags(String taggedNoteLocation) throws Exception {
@@ -182,7 +178,7 @@ public class GettingStartedDocumentation {
.andReturn(), "tags");
document("get-tags",
this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk())
.andExpect(jsonPath("links[?(@.rel==tag)]", is(notNullValue()))));
.andExpect(jsonPath("_embedded.tags", hasSize(1))));
}
void tagExistingNote(String noteLocation, String tagLocation) throws Exception {
@@ -191,12 +187,10 @@ public class GettingStartedDocumentation {
document(
"tag-existing-note",
this.mockMvc
.perform(
patch(noteLocation).contentType(
MediaType.APPLICATION_JSON).content(
objectMapper.writeValueAsString(update)))
.andDo(print()).andExpect(status().isNoContent()));
this.mockMvc.perform(
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(update))).andExpect(
status().isNoContent()));
}
@@ -210,12 +204,12 @@ public class GettingStartedDocumentation {
.andReturn(), "tags");
document("get-tags-for-existing-note",
this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk())
.andExpect(jsonPath("links[?(@.rel==tag)]", is(notNullValue()))));
.andExpect(jsonPath("_embedded.tags", hasSize(1))));
}
private String getLink(MvcResult result, String rel)
private String getLink(MvcResult result, String href)
throws UnsupportedEncodingException {
return JsonPath.parse(result.getResponse().getContentAsString()).read(
"links[?(@.rel==" + rel + ")][0].href");
"_links.tags.href");
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2014 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 com.example.notes;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
public class NestedContentResource<T> extends ResourceSupport {
private final Resources<T> nested;
public NestedContentResource(Iterable<T> toNest) {
this.nested = new Resources<T>(toNest);
}
@JsonUnwrapped
public Resources<T> getNested() {
return this.nested;
}
}

View File

@@ -36,6 +36,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriTemplate;
import com.example.notes.NoteResourceAssembler.NoteResource;
import com.example.notes.TagResourceAssembler.TagResource;
@RestController
@RequestMapping("/notes")
@@ -47,17 +48,21 @@ public class NotesController {
private final NoteResourceAssembler noteResourceAssembler;
private final TagResourceAssembler tagResourceAssembler;
@Autowired
public NotesController(NoteRepository noteRepository, TagRepository tagRepository,
NoteResourceAssembler noteResourceAssembler) {
NoteResourceAssembler noteResourceAssembler, TagResourceAssembler tagResourceAssembler) {
this.noteRepository = noteRepository;
this.tagRepository = tagRepository;
this.noteResourceAssembler = noteResourceAssembler;
this.tagResourceAssembler = tagResourceAssembler;
}
@RequestMapping(method = RequestMethod.GET)
Iterable<NoteResource> all() {
return this.noteResourceAssembler.toResources(this.noteRepository.findAll());
NestedContentResource<NoteResource> all() {
return new NestedContentResource<NoteResource>(
this.noteResourceAssembler.toResources(this.noteRepository.findAll()));
}
@ResponseStatus(HttpStatus.CREATED)
@@ -85,12 +90,9 @@ public class NotesController {
@RequestMapping(value = "/{id}/tags", method = RequestMethod.GET)
ResourceSupport noteTags(@PathVariable("id") long id) {
ResourceSupport resource = new ResourceSupport();
Note note = this.noteRepository.findOne(id);
for (Tag tag : note.getTags()) {
resource.add(linkTo(TagsController.class).slash(tag.getId()).withRel("tag"));
}
return resource;
return new NestedContentResource<TagResource>(
this.tagResourceAssembler.toResources(this.noteRepository.findOne(id)
.getTags()));
}
@RequestMapping(value = "/{id}", method = RequestMethod.PATCH)

View File

@@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.example.notes.NoteResourceAssembler.NoteResource;
import com.example.notes.TagResourceAssembler.TagResource;
@RestController
@@ -38,17 +39,23 @@ public class TagsController {
private final TagRepository repository;
private final TagResourceAssembler resourceAssembler;
private final NoteResourceAssembler noteResourceAssembler;
private final TagResourceAssembler tagResourceAssembler;
@Autowired
public TagsController(TagRepository repository, TagResourceAssembler resourceAssembler) {
public TagsController(TagRepository repository,
NoteResourceAssembler noteResourceAssembler,
TagResourceAssembler tagResourceAssembler) {
this.repository = repository;
this.resourceAssembler = resourceAssembler;
this.noteResourceAssembler = noteResourceAssembler;
this.tagResourceAssembler = tagResourceAssembler;
}
@RequestMapping(method = RequestMethod.GET)
Iterable<TagResource> all() {
return this.resourceAssembler.toResources(this.repository.findAll());
NestedContentResource<TagResource> all() {
return new NestedContentResource<TagResource>(
this.tagResourceAssembler.toResources(this.repository.findAll()));
}
@ResponseStatus(HttpStatus.CREATED)
@@ -68,17 +75,13 @@ public class TagsController {
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
Resource<Tag> tag(@PathVariable("id") long id) {
Tag tag = this.repository.findOne(id);
return this.resourceAssembler.toResource(tag);
return this.tagResourceAssembler.toResource(tag);
}
@RequestMapping(value = "/{id}/notes", method = RequestMethod.GET)
ResourceSupport tagNotes(@PathVariable("id") long id) {
ResourceSupport resource = new ResourceSupport();
Tag tag = this.repository.findOne(id);
for (Note note : tag.getNotes()) {
resource.add(linkTo(NotesController.class).slash(note.getId())
.withRel("note"));
}
return resource;
return new NestedContentResource<NoteResource>(
this.noteResourceAssembler.toResources(this.repository.findOne(id)
.getNotes()));
}
}