From 0574699fad391c33a5b40fbc15dbba3ef0c1ec23 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Oct 2014 12:29:22 +0000 Subject: [PATCH] Use HAL in the Spring HATEOAS-based sample --- rest-notes-spring-hateoas/build.gradle | 2 + .../notes/GettingStartedDocumentation.java | 44 ++++++++----------- .../example/notes/NestedContentResource.java | 36 +++++++++++++++ .../com/example/notes/NotesController.java | 20 +++++---- .../com/example/notes/TagsController.java | 29 ++++++------ 5 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java diff --git a/rest-notes-spring-hateoas/build.gradle b/rest-notes-spring-hateoas/build.gradle index eb992a86..61ccd7d4 100644 --- a/rest-notes-spring-hateoas/build.gradle +++ b/rest-notes-spring-hateoas/build.gradle @@ -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' diff --git a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java index ab876de1..ae231f53 100644 --- a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java +++ b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java @@ -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"); } } diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java b/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java new file mode 100644 index 00000000..00a61ee8 --- /dev/null +++ b/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java @@ -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 extends ResourceSupport { + + private final Resources nested; + + public NestedContentResource(Iterable toNest) { + this.nested = new Resources(toNest); + } + + @JsonUnwrapped + public Resources getNested() { + return this.nested; + } +} diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java b/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java index b61d91e4..cb9f655d 100644 --- a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java +++ b/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java @@ -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 all() { - return this.noteResourceAssembler.toResources(this.noteRepository.findAll()); + NestedContentResource all() { + return new NestedContentResource( + 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( + this.tagResourceAssembler.toResources(this.noteRepository.findOne(id) + .getTags())); } @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java b/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java index 9dc227e2..891e90fa 100644 --- a/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java +++ b/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java @@ -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 all() { - return this.resourceAssembler.toResources(this.repository.findAll()); + NestedContentResource all() { + return new NestedContentResource( + this.tagResourceAssembler.toResources(this.repository.findAll())); } @ResponseStatus(HttpStatus.CREATED) @@ -68,17 +75,13 @@ public class TagsController { @RequestMapping(value = "/{id}", method = RequestMethod.GET) Resource 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( + this.noteResourceAssembler.toResources(this.repository.findOne(id) + .getNotes())); } }