Polishing.

This commit is contained in:
Greg Turnquist
2019-03-03 11:05:27 -06:00
parent 15db5a0513
commit e8de8268ad
40 changed files with 443 additions and 463 deletions

View File

@@ -28,13 +28,14 @@ import org.springframework.stereotype.Component;
class DatabaseLoader {
/**
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run
* only after the app is operational, the database will be up.
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run only after the app
* is operational, the database will be up.
*
* @param repository
*/
@Bean
CommandLineRunner init(EmployeeRepository repository) {
return args -> {
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
repository.save(new Employee("Bilbo", "Baggins", "burglar"));

View File

@@ -25,18 +25,14 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum.
*
* {@code @Data} - Generates getters, setters, toString, hash, and equals functions
* {@code @Entity} - JPA annotation to flag this class for DB persistence
* {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum. {@code @Data} -
* Generates getters, setters, toString, hash, and equals functions {@code @Entity} - JPA annotation to flag this class
* for DB persistence {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
* {@code @AllArgsConstructor} - Create a constructor with all args to support testing
*
* {@code @JsonIgnoreProperties(ignoreUnknow=true)}
* When converting JSON to Java, ignore any unrecognized attributes. This is critical for REST because it
* encourages adding new fields in later versions that won't break. It also allows things like _links to be
* ignore as well, meaning HAL documents can be fetched and later posted to the server without adjustment.
*
* {@code @JsonIgnoreProperties(ignoreUnknow=true)} When converting JSON to Java, ignore any unrecognized attributes.
* This is critical for REST because it encourages adding new fields in later versions that won't break. It also allows
* things like _links to be ignore as well, meaning HAL documents can be fetched and later posted to the server without
* adjustment.
*
* @author Greg Turnquist
*/
@@ -46,8 +42,7 @@ import javax.persistence.Id;
@AllArgsConstructor
class Employee {
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue private Long id;
private String firstName;
private String lastName;
private String role;

View File

@@ -23,10 +23,10 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -45,7 +45,6 @@ class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
@@ -53,17 +52,17 @@ class EmployeeController {
ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
List<EntityModel<Employee>> employeeResources = StreamSupport.stream(repository.findAll().spliterator(), false)
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")
))
.collect(Collectors.toList());
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.collect(Collectors.toList());
return ResponseEntity.ok(new CollectionModel<>(employeeResources,
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))));
return ResponseEntity.ok(new CollectionModel<>( //
employeeResources, //
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))));
}
@PostMapping("/employees")
@@ -72,35 +71,33 @@ class EmployeeController {
Employee savedEmployee = repository.save(employee);
return new EntityModel<>(savedEmployee,
linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, savedEmployee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(savedEmployee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")
).getLink(IanaLinkRelations.SELF)
.map(Link::getHref)
.map(href -> {
try {
return new URI(href);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
})
.map(uri -> ResponseEntity.noContent().location(uri).build())
.orElse(ResponseEntity.badRequest().body("Unable to create " + employee));
linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, savedEmployee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(savedEmployee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")).getLink(IanaLinkRelations.SELF)
.map(Link::getHref) //
.map(href -> {
try {
return new URI(href);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}) //
.map(uri -> ResponseEntity.noContent().location(uri).build())
.orElse(ResponseEntity.badRequest().body("Unable to create " + employee));
}
@GetMapping("/employees/{id}")
ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
return repository.findById(id)
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")
))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
@PutMapping("/employees/{id}")
@@ -112,21 +109,19 @@ class EmployeeController {
Employee updatedEmployee = repository.save(employeeToUpdate);
return new EntityModel<>(updatedEmployee,
linkTo(methodOn(EmployeeController.class).findOne(updatedEmployee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, updatedEmployee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(updatedEmployee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")
).getLink(IanaLinkRelations.SELF)
.map(Link::getHref)
.map(href -> {
try {
return new URI(href);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
})
.map(uri -> ResponseEntity.noContent().location(uri).build())
.orElse(ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate));
linkTo(methodOn(EmployeeController.class).findOne(updatedEmployee.getId())).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, updatedEmployee.getId())))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(updatedEmployee.getId()))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")).getLink(IanaLinkRelations.SELF)
.map(Link::getHref).map(href -> {
try {
return new URI(href);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}) //
.map(uri -> ResponseEntity.noContent().location(uri).build()) //
.orElse(ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate));
}
@DeleteMapping("/employees/{id}")

View File

@@ -41,100 +41,96 @@ import org.springframework.test.web.servlet.MockMvc;
*/
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeController.class)
@Import({HypermediaConfiguration.class})
@Import({ HypermediaConfiguration.class })
public class EmployeeControllerTests {
@Autowired
private MockMvc mvc;
@Autowired private MockMvc mvc;
@MockBean
private EmployeeRepository repository;
@MockBean private EmployeeRepository repository;
@Test
public void getAllShouldFetchAHalFormsEmbeddedDocument() throws Exception {
given(repository.findAll()).willReturn(
Arrays.asList(
new Employee(1L, "Frodo", "Baggins", "ring bearer"),
given(repository.findAll()).willReturn(Arrays.asList( //
new Employee(1L, "Frodo", "Baggins", "ring bearer"), //
new Employee(2L, "Bilbo", "Baggins", "burglar")));
mvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_FORMS_JSON_VALUE + ";charset=UTF-8"))
mvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON_VALUE)) //
.andDo(print()) //
.andExpect(status().isOk()) //
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_FORMS_JSON_VALUE + ";charset=UTF-8"))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.method", is("put")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.method", is("put")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.method", is("put")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.method", is("put")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._templates.default.method", is("post")))
.andExpect(jsonPath("$._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._templates.default.method", is("post")))
.andExpect(jsonPath("$._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees")));
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees")));
}
@Test
public void getOneShouldFetchASingleHalFormsDocument() throws Exception {
given(repository.findById(any())).willReturn(
Optional.of(new Employee(1L, "Frodo", "Baggins", "ring bearer")));
given(repository.findById(any())).willReturn(Optional.of(new Employee(1L, "Frodo", "Baggins", "ring bearer")));
mvc.perform(get("/employees/1").accept(MediaTypes.HAL_FORMS_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_FORMS_JSON_VALUE + ";charset=UTF-8"))
mvc.perform(get("/employees/1").accept(MediaTypes.HAL_FORMS_JSON_VALUE)) //
.andDo(print()) //
.andExpect(status().isOk()) //
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_FORMS_JSON_VALUE + ";charset=UTF-8"))
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.firstName", is("Frodo")))
.andExpect(jsonPath("$.lastName", is("Baggins")))
.andExpect(jsonPath("$.role", is("ring bearer")))
.andExpect(jsonPath("$.id", is(1))) //
.andExpect(jsonPath("$.firstName", is("Frodo"))) //
.andExpect(jsonPath("$.lastName", is("Baggins"))) //
.andExpect(jsonPath("$.role", is("ring bearer")))
.andExpect(jsonPath("$._templates.default.method", is("put")))
.andExpect(jsonPath("$._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._links.employees.href", is("http://localhost/employees")));
.andExpect(jsonPath("$._templates.default.method", is("put")))
.andExpect(jsonPath("$._templates.default.properties[0].name", is("firstName")))
.andExpect(jsonPath("$._templates.default.properties[0].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[1].name", is("id")))
.andExpect(jsonPath("$._templates.default.properties[1].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[2].name", is("lastName")))
.andExpect(jsonPath("$._templates.default.properties[2].required", is(true)))
.andExpect(jsonPath("$._templates.default.properties[3].name", is("role")))
.andExpect(jsonPath("$._templates.default.properties[3].required", is(true)))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._links.employees.href", is("http://localhost/employees")));
}
}

View File

@@ -19,8 +19,8 @@ import lombok.Data;
import lombok.NoArgsConstructor;
/**
* An updated domain object on the client side. It doesn't need all the backward compatible bits that the new
* server needs (unless this becomes a service of its own).
* An updated domain object on the client side. It doesn't need all the backward compatible bits that the new server
* needs (unless this becomes a service of its own).
*
* @author Greg Turnquist
*/

View File

@@ -48,11 +48,9 @@ public class HomeController {
}
/**
* Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then
* "hopping" to the {@literal employees} rel.
*
* NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry with
* the Thymeleaf template.
* Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then "hopping" to the
* {@literal employees} rel. NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry
* with the Thymeleaf template.
*
* @param model
* @return
@@ -62,9 +60,10 @@ public class HomeController {
public String index(Model model) throws URISyntaxException {
Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON);
CollectionModel<EntityModel<Employee>> employees = client
.follow("employees")
.toObject(new CollectionModelType<EntityModel<Employee>>(){});
CollectionModel<EntityModel<Employee>> employees = client //
.follow("employees") //
.toObject(new CollectionModelType<EntityModel<Employee>>() {});
model.addAttribute("employee", new Employee());
model.addAttribute("employees", employees);
@@ -73,11 +72,9 @@ public class HomeController {
}
/**
* Instead of putting the creation link from the remote service in the template (a security concern),
* have a local route for {@literal POST} requests. Gather up the information, and form a remote call,
* using {@link Traverson} to fetch the {@literal employees} {@link Link}.
*
* Once a new employee is created, redirect back to the root URL.
* Instead of putting the creation link from the remote service in the template (a security concern), have a local
* route for {@literal POST} requests. Gather up the information, and form a remote call, using {@link Traverson} to
* fetch the {@literal employees} {@link Link}. Once a new employee is created, redirect back to the root URL.
*
* @param employee
* @return
@@ -87,9 +84,7 @@ public class HomeController {
public String newEmployee(@ModelAttribute Employee employee) throws URISyntaxException {
Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON);
Link employeesLink = client
.follow("employees")
.asLink();
Link employeesLink = client.follow("employees").asLink();
this.rest.postForEntity(employeesLink.expand().getHref(), employee, Employee.class);

View File

@@ -28,8 +28,8 @@ import javax.persistence.Id;
import org.springframework.util.StringUtils;
/**
* An updated domain object where {@literal name} has been replaced by {@literal firstName} and {@literal} lastName.
* To easy migration, we need to support the old {@literal name} field with a getter and a setter.
* An updated domain object where {@literal name} has been replaced by {@literal firstName} and {@literal} lastName. To
* easy migration, we need to support the old {@literal name} field with a getter and a setter.
*
* @author Greg Turnquist
*/
@@ -38,8 +38,7 @@ import org.springframework.util.StringUtils;
@Entity
class Employee {
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue private Long id;
private String firstName;
private String lastName;
private String role;

View File

@@ -17,9 +17,9 @@ package org.springframework.hateoas.examples;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -46,10 +46,10 @@ class EmployeeController {
public RepresentationModel root() {
RepresentationModel rootResource = new RepresentationModel();
rootResource.add(
linkTo(methodOn(EmployeeController.class).root()).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
rootResource.add( //
linkTo(methodOn(EmployeeController.class).root()).withSelfRel(), //
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
return rootResource;
}
@@ -64,20 +64,19 @@ class EmployeeController {
Employee savedEmployee = repository.save(employee);
return savedEmployee.getId()
.map(id -> ResponseEntity
.created(linkTo(methodOn(EmployeeController.class).findOne(id)).toUri())
.body(assembler.toModel(savedEmployee)))
.orElse(ResponseEntity.notFound().build());
return savedEmployee.getId() //
.map(id -> ResponseEntity.created( //
linkTo(methodOn(EmployeeController.class).findOne(id)).toUri()).body(assembler.toModel(savedEmployee)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/employees/{id}")
public ResponseEntity<EntityModel<Employee>> findOne(@PathVariable Long id) {
return repository.findById(id)
.map(assembler::toModel)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
return repository.findById(id) //
.map(assembler::toModel) //
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -1,4 +1,4 @@
package org.springframework.hateoas.examples;/*
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,6 +13,7 @@ package org.springframework.hateoas.examples;/*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.examples;
import org.springframework.data.repository.CrudRepository;

View File

@@ -33,6 +33,7 @@ class InitDatabase {
@Bean
CommandLineRunner loadEmployees() {
return args -> {
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
repository.save(new Employee("Bilbo", "Baggins", "burglar"));

View File

@@ -48,11 +48,9 @@ public class HomeController {
}
/**
* Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then
* "hopping" to the {@literal employees} rel.
*
* NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry with
* the Thymeleaf template.
* Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then "hopping" to the
* {@literal employees} rel. NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry
* with the Thymeleaf template.
*
* @param model
* @return
@@ -62,9 +60,9 @@ public class HomeController {
public String index(Model model) throws URISyntaxException {
Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON);
CollectionModel<EntityModel<Employee>> employees = client
.follow("employees")
.toObject(new CollectionModelType<EntityModel<Employee>>(){});
CollectionModel<EntityModel<Employee>> employees = client //
.follow("employees") //
.toObject(new CollectionModelType<EntityModel<Employee>>() {});
model.addAttribute("employee", new Employee());
model.addAttribute("employees", employees);
@@ -73,11 +71,9 @@ public class HomeController {
}
/**
* Instead of putting the creation link from the remote service in the template (a security concern),
* have a local route for {@literal POST} requests. Gather up the information, and form a remote call,
* using {@link Traverson} to fetch the {@literal employees} {@link Link}.
*
* Once a new employee is created, redirect back to the root URL.
* Instead of putting the creation link from the remote service in the template (a security concern), have a local
* route for {@literal POST} requests. Gather up the information, and form a remote call, using {@link Traverson} to
* fetch the {@literal employees} {@link Link}. Once a new employee is created, redirect back to the root URL.
*
* @param employee
* @return
@@ -87,9 +83,9 @@ public class HomeController {
public String newEmployee(@ModelAttribute Employee employee) throws URISyntaxException {
Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON);
Link employeesLink = client
.follow("employees")
.asLink();
Link employeesLink = client //
.follow("employees") //
.asLink();
this.rest.postForEntity(employeesLink.expand().getHref(), employee, Employee.class);

View File

@@ -33,13 +33,12 @@ import javax.persistence.Id;
@Entity
class Employee {
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue private Long id;
private String name;
private String role;
Employee(String name, String role) {
this.name = name;
this.role = role;
}

View File

@@ -17,9 +17,9 @@ package org.springframework.hateoas.examples;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -47,9 +47,9 @@ class EmployeeController {
RepresentationModel rootResource = new RepresentationModel();
rootResource.add(
linkTo(methodOn(EmployeeController.class).root()).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
rootResource.add( //
linkTo(methodOn(EmployeeController.class).root()).withSelfRel(), //
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
return rootResource;
}
@@ -64,18 +64,18 @@ class EmployeeController {
Employee savedEmployee = repository.save(employee);
return ResponseEntity
.created(savedEmployee.getId()
.map(id -> linkTo(methodOn(EmployeeController.class).findOne(id)).toUri())
.orElseThrow(() -> new RuntimeException("Failed to create for some reason")))
.body(assembler.toModel(savedEmployee));
return ResponseEntity //
.created(savedEmployee.getId() //
.map(id -> linkTo(methodOn(EmployeeController.class).findOne(id)).toUri()) //
.orElseThrow(() -> new RuntimeException("Failed to create for some reason"))) //
.body(assembler.toModel(savedEmployee));
}
@GetMapping("/employees/{id}")
public EntityModel<Employee> findOne(@PathVariable Long id) {
return repository.findById(id)
.map(assembler::toModel)
.orElseThrow(() -> new RuntimeException("No employee '" + id + "' found"));
return repository.findById(id) //
.map(assembler::toModel) //
.orElseThrow(() -> new RuntimeException("No employee '" + id + "' found"));
}
}

View File

@@ -1,4 +1,4 @@
package org.springframework.hateoas.examples;/*
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,6 +13,7 @@ package org.springframework.hateoas.examples;/*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.examples;
import org.springframework.data.repository.CrudRepository;

View File

@@ -33,6 +33,7 @@ class InitDatabase {
@Bean
CommandLineRunner loadEmployees() {
return args -> {
repository.save(new Employee("Frodo", "ring bearer"));
repository.save(new Employee("Bilbo", "burglar"));

View File

@@ -28,13 +28,14 @@ import org.springframework.stereotype.Component;
class DatabaseLoader {
/**
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run
* only after the app is operational, the database will be up.
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run only after the app
* is operational, the database will be up.
*
* @param repository
*/
@Bean
CommandLineRunner init(EmployeeRepository repository) {
return args -> {
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
repository.save(new Employee("Bilbo", "Baggins", "burglar"));

View File

@@ -29,18 +29,14 @@ import javax.persistence.Id;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum.
*
* {@code @Data} - Generates getters, setters, toString, hash, and equals functions
* {@code @Entity} - JPA annotation to flag this class for DB persistence
* {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum. {@code @Data} -
* Generates getters, setters, toString, hash, and equals functions {@code @Entity} - JPA annotation to flag this class
* for DB persistence {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
* {@code @AllArgsConstructor} - Create a constructor with all args to support testing
*
* {@code @JsonIgnoreProperties(ignoreUnknow=true)}
* When converting JSON to Java, ignore any unrecognized attributes. This is critical for REST because it
* encourages adding new fields in later versions that won't break. It also allows things like _links to be
* ignore as well, meaning HAL documents can be fetched and later posted to the server without adjustment.
*
* {@code @JsonIgnoreProperties(ignoreUnknow=true)} When converting JSON to Java, ignore any unrecognized attributes.
* This is critical for REST because it encourages adding new fields in later versions that won't break. It also allows
* things like _links to be ignore as well, meaning HAL documents can be fetched and later posted to the server without
* adjustment.
*
* @author Greg Turnquist
*/
@@ -51,13 +47,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
class Employee {
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue private Long id;
private String firstName;
private String lastName;
private String role;
/**
@@ -79,14 +71,10 @@ class Employee {
}
/**
* This method will create another piece of data in the REST resource representation. These types
* of methods are key in supporting backward compatibility.
*
* By NOT removing old fields, and instead replacing them with methods like this, an API can evolve
* without breaking old clients.
*
* Because of {@code @JsonIgnoreProperties} settings above, this attribute will be ignore if sent back
* to the server, allowing API evolution.
* This method will create another piece of data in the REST resource representation. These types of methods are key
* in supporting backward compatibility. By NOT removing old fields, and instead replacing them with methods like
* this, an API can evolve without breaking old clients. Because of {@code @JsonIgnoreProperties} settings above, this
* attribute will be ignore if sent back to the server, allowing API evolution.
*
* @return
*/

View File

@@ -15,18 +15,17 @@
*/
package org.springframework.hateoas.examples;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* Spring Web {@link RestController} used to generate a REST API.
*
* Works by injecting an {@link EmployeeRepository} and an {@link EmployeeRepresentationModelAssembler} in the constructor, both
* of which are used to retrieve data from the database, and assemble a REST resource.
* Spring Web {@link RestController} used to generate a REST API. Works by injecting an {@link EmployeeRepository} and
* an {@link EmployeeRepresentationModelAssembler} in the constructor, both of which are used to retrieve data from the
* database, and assemble a REST resource.
*
* @author Greg Turnquist
*/
@@ -37,38 +36,38 @@ class EmployeeController {
private final EmployeeRepresentationModelAssembler assembler;
EmployeeController(EmployeeRepository repository, EmployeeRepresentationModelAssembler assembler) {
this.repository = repository;
this.assembler = assembler;
}
/**
* Look up all employees, and transform them into a REST collection resource using
* {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through
* Spring Web's {@link ResponseEntity} fluent API.
* {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through Spring Web's
* {@link ResponseEntity} fluent API.
*/
@GetMapping("/employees")
public ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
return ResponseEntity.ok(
this.assembler.toCollectionModel(this.repository.findAll()));
return ResponseEntity.ok( //
this.assembler.toCollectionModel(this.repository.findAll()));
}
/**
* Look up a single {@link Employee} and transform it into a REST resource using
* {@link EmployeeRepresentationModelAssembler#toModel(Object)}. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
* {@link EmployeeRepresentationModelAssembler#toModel(Object)}. Then return it through Spring Web's
* {@link ResponseEntity} fluent API.
*
* @param id
*/
@GetMapping("/employees/{id}")
public ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
return this.repository.findById(id)
.map(this.assembler::toModel)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
return this.repository.findById(id) //
.map(this.assembler::toModel) //
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -22,5 +22,4 @@ import org.springframework.data.repository.CrudRepository;
*
* @author Greg Turnquist
*/
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
interface EmployeeRepository extends CrudRepository<Employee, Long> {}

View File

@@ -26,8 +26,8 @@ class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentat
/**
* Link the {@link Employee} domain type to the {@link EmployeeController} using this
* {@link SimpleIdentifiableRepresentationModelAssembler} in order to generate both {@link org.springframework.hateoas.Resource}
* and {@link org.springframework.hateoas.CollectionModel}.
* {@link SimpleIdentifiableRepresentationModelAssembler} in order to generate both
* {@link org.springframework.hateoas.Resource} and {@link org.springframework.hateoas.CollectionModel}.
*/
EmployeeRepresentationModelAssembler() {
super(EmployeeController.class);

View File

@@ -25,7 +25,6 @@ import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
@@ -42,41 +41,39 @@ import org.springframework.test.web.servlet.MockMvc;
*/
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeController.class)
@Import({EmployeeRepresentationModelAssembler.class})
@Import({ EmployeeRepresentationModelAssembler.class })
public class EmployeeControllerTests {
@Autowired
private MockMvc mvc;
@Autowired private MockMvc mvc;
@MockBean
private EmployeeRepository repository;
@MockBean private EmployeeRepository repository;
@Test
public void getShouldFetchAHalDocument() throws Exception {
given(repository.findAll()).willReturn(
Arrays.asList(
new Employee(1L,"Frodo", "Baggins", "ring bearer"),
new Employee(2L,"Bilbo", "Baggins", "burglar")));
given(repository.findAll()).willReturn( //
Arrays.asList( //
new Employee(1L, "Frodo", "Baggins", "ring bearer"), //
new Employee(2L, "Bilbo", "Baggins", "burglar")));
mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE + ";charset=UTF-8"))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees")))
.andReturn();
mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) //
.andDo(print()) //
.andExpect(status().isOk()) //
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE + ";charset=UTF-8"))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees"))) //
.andReturn();
}
}

View File

@@ -31,15 +31,15 @@ import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.util.ReflectionUtils;
/**
* A {@link SimpleRepresentationModelAssembler} that mixes together a Spring web controller and a {@link RelProvider} to build links
* upon a certain strategy.
*
* A {@link SimpleRepresentationModelAssembler} that mixes together a Spring web controller and a {@link RelProvider} to
* build links upon a certain strategy.
*
* @author Greg Turnquist
*/
public class SimpleIdentifiableRepresentationModelAssembler<T> implements SimpleRepresentationModelAssembler<T> {
/**
* The Spring MVC class for the {@link Identifiable} from which links will be built.
* The Spring MVC class for the object from which links will be built.
*/
private final Class<?> controllerClass;
@@ -49,7 +49,7 @@ public class SimpleIdentifiableRepresentationModelAssembler<T> implements Simple
@Getter private final RelProvider relProvider;
/**
* A {@link Class} depicting the {@link Identifiable}'s type.
* A {@link Class} depicting the object's type.
*/
@Getter private final Class<?> resourceType;
@@ -63,7 +63,6 @@ public class SimpleIdentifiableRepresentationModelAssembler<T> implements Simple
* of information, resources can be defined.
*
* @see #setBasePath(String) to adjust base path to something like "/api"/
*
* @param controllerClass - Spring MVC controller to base links off of
* @param relProvider
*/
@@ -72,13 +71,15 @@ public class SimpleIdentifiableRepresentationModelAssembler<T> implements Simple
this.controllerClass = controllerClass;
this.relProvider = relProvider;
// Find the "T" type contained in "T extends Identifiable<?>", e.g. SimpleIdentifiableRepresentationModelAssembler<User> -> User
this.resourceType = GenericTypeResolver.resolveTypeArgument(this.getClass(), SimpleIdentifiableRepresentationModelAssembler.class);
// Find the "T" type contained in "T extends Identifiable<?>", e.g.
// SimpleIdentifiableRepresentationModelAssembler<User> -> User
this.resourceType = GenericTypeResolver.resolveTypeArgument(this.getClass(),
SimpleIdentifiableRepresentationModelAssembler.class);
}
/**
* Alternate constructor that falls back to {@link EvoInflectorRelProvider}.
*
*
* @param controllerClass
*/
public SimpleIdentifiableRepresentationModelAssembler(Class<?> controllerClass) {
@@ -86,8 +87,8 @@ public class SimpleIdentifiableRepresentationModelAssembler<T> implements Simple
}
/**
* Add single item self link based on {@link Identifiable} and link back to aggregate root of the {@literal T} domain
* type using {@link RelProvider#getCollectionResourceRelFor(Class)}}.
* Add single item self link based on the object and link back to aggregate root of the {@literal T} domain type using
* {@link RelProvider#getCollectionResourceRelFor(Class)}}.
*
* @param resource
*/
@@ -115,15 +116,12 @@ public class SimpleIdentifiableRepresentationModelAssembler<T> implements Simple
}
/**
* Build up a URI for the collection using the Spring web controller followed by the resource type transformed
* by the {@link RelProvider}.
*
* Assumption is that an {@literal EmployeeController} serving up {@literal Employee}
* objects will be serving resources at {@code /employees} and {@code /employees/1}.
*
* If this is not the case, simply override this method in your concrete instance, or resort to
* overriding {@link #addLinks(EntityModel)} and {@link #addLinks(CollectionModel)} where you have full control over exactly
* what links are put in the individual and collection resources.
* Build up a URI for the collection using the Spring web controller followed by the resource type transformed by the
* {@link RelProvider}. Assumption is that an {@literal EmployeeController} serving up {@literal Employee} objects
* will be serving resources at {@code /employees} and {@code /employees/1}. If this is not the case, simply override
* this method in your concrete instance, or resort to overriding {@link #addLinks(EntityModel)} and
* {@link #addLinks(CollectionModel)} where you have full control over exactly what links are put in the individual
* and collection resources.
*
* @return
*/
@@ -131,7 +129,8 @@ public class SimpleIdentifiableRepresentationModelAssembler<T> implements Simple
WebMvcLinkBuilder linkBuilder = linkTo(this.controllerClass);
for (String pathComponent : (getPrefix() + this.relProvider.getCollectionResourceRelFor(this.resourceType)).split("/")) {
for (String pathComponent : (getPrefix() + this.relProvider.getCollectionResourceRelFor(this.resourceType))
.split("/")) {
if (!pathComponent.isEmpty()) {
linkBuilder = linkBuilder.slash(pathComponent);
}

View File

@@ -30,6 +30,7 @@ class DatabaseLoader {
@Bean
CommandLineRunner initDatabase(EmployeeRepository employeeRepository, ManagerRepository managerRepository) {
return args -> {
/*
* Gather Gandalf's team
*/

View File

@@ -35,17 +35,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
@NoArgsConstructor
class Employee {
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue private Long id;
private String name;
private String role;
/**
* To break the recursive, bi-directional relationship, don't serialize {@literal manager}.
*/
@JsonIgnore
@OneToOne
private Manager manager;
@JsonIgnore @OneToOne private Manager manager;
Employee(String name, String role, Manager manager) {

View File

@@ -18,8 +18,8 @@ package org.springframework.hateoas.examples;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -36,7 +36,7 @@ class EmployeeController {
private final EmployeeWithManagerResourceAssembler employeeWithManagerResourceAssembler;
EmployeeController(EmployeeRepository repository, EmployeeRepresentationModelAssembler assembler,
EmployeeWithManagerResourceAssembler employeeWithManagerResourceAssembler) {
EmployeeWithManagerResourceAssembler employeeWithManagerResourceAssembler) {
this.repository = repository;
this.assembler = assembler;
@@ -45,30 +45,30 @@ class EmployeeController {
/**
* Look up all employees, and transform them into a REST collection resource using
* {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through
* Spring Web's {@link ResponseEntity} fluent API.
* {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through Spring Web's
* {@link ResponseEntity} fluent API.
*/
@GetMapping("/employees")
public ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
return ResponseEntity.ok(
assembler.toCollectionModel(repository.findAll()));
return ResponseEntity.ok(assembler.toCollectionModel(repository.findAll()));
}
/**
* Look up a single {@link Employee} and transform it into a REST resource using
* {@link EmployeeRepresentationModelAssembler#toModel(Object)}. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
* {@link EmployeeRepresentationModelAssembler#toModel(Object)}. Then return it through Spring Web's
* {@link ResponseEntity} fluent API.
*
* @param id
*/
@GetMapping("/employees/{id}")
public ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
return repository.findById(id)
.map(assembler::toModel)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
return repository.findById(id) //
.map(assembler::toModel) //
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
/**
@@ -79,27 +79,27 @@ class EmployeeController {
*/
@GetMapping("/managers/{id}/employees")
public ResponseEntity<CollectionModel<EntityModel<Employee>>> findEmployees(@PathVariable long id) {
return ResponseEntity.ok(
assembler.toCollectionModel(repository.findByManagerId(id)));
return ResponseEntity.ok(assembler.toCollectionModel(repository.findByManagerId(id)));
}
@GetMapping("/employees/detailed")
public ResponseEntity<CollectionModel<EntityModel<EmployeeWithManager>>> findAllDetailedEmployees() {
return ResponseEntity.ok(
employeeWithManagerResourceAssembler.toCollectionModel(
StreamSupport.stream(repository.findAll().spliterator(), false)
.map(EmployeeWithManager::new)
.collect(Collectors.toList())));
return ResponseEntity.ok( //
employeeWithManagerResourceAssembler.toCollectionModel( //
StreamSupport.stream(repository.findAll().spliterator(), false) //
.map(EmployeeWithManager::new) //
.collect(Collectors.toList())));
}
@GetMapping("/employees/{id}/detailed")
public ResponseEntity<EntityModel<EmployeeWithManager>> findDetailedEmployee(@PathVariable Long id) {
return repository.findById(id)
.map(EmployeeWithManager::new)
.map(employeeWithManagerResourceAssembler::toModel)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
return repository.findById(id) //
.map(EmployeeWithManager::new) //
.map(employeeWithManagerResourceAssembler::toModel) //
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -17,8 +17,8 @@ package org.springframework.hateoas.examples;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.SimpleIdentifiableRepresentationModelAssembler;
import org.springframework.stereotype.Component;
@@ -41,21 +41,21 @@ class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentat
public void addLinks(EntityModel<Employee> resource) {
/**
* Add some custom links to the default ones provided.
*
* NOTE: To replace default links, don't invoke {@literal super.addLinks()}.
* Add some custom links to the default ones provided. NOTE: To replace default links, don't invoke
* {@literal super.addLinks()}.
*/
super.addLinks(resource);
resource.getContent().getId()
.ifPresent(id -> {
// Add additional links
resource.add(linkTo(methodOn(ManagerController.class).findManager(id)).withRel("manager"));
resource.add(linkTo(methodOn(EmployeeController.class).findDetailedEmployee(id)).withRel("detailed"));
resource.getContent().getId() //
.ifPresent(id -> { //
// Add additional links
resource.add(linkTo(methodOn(ManagerController.class).findManager(id)).withRel("manager"));
resource.add(linkTo(methodOn(EmployeeController.class).findDetailedEmployee(id)).withRel("detailed"));
// Maintain a legacy link to support older clients not yet adjusted to the switch from "supervisor" to "manager".
resource.add(linkTo(methodOn(SupervisorController.class).findOne(id)).withRel("supervisor"));
});
// Maintain a legacy link to support older clients not yet adjusted to the switch from "supervisor" to
// "manager".
resource.add(linkTo(methodOn(SupervisorController.class).findOne(id)).withRel("supervisor"));
});
}
/**
@@ -65,7 +65,7 @@ class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentat
*/
@Override
public void addLinks(CollectionModel<EntityModel<Employee>> resources) {
super.addLinks(resources);
resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees"));

View File

@@ -26,16 +26,15 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
* @author Greg Turnquist
*/
@Value
@JsonPropertyOrder({"id", "name", "role", "manager"})
@JsonPropertyOrder({ "id", "name", "role", "manager" })
public class EmployeeWithManager {
@JsonIgnore
private final Employee employee;
@JsonIgnore private final Employee employee;
public Long getId() {
return this.employee.getId()
.orElseThrow(() -> new RuntimeException("Couldn't find anything."));
return this.employee.getId() //
.orElseThrow(() -> new RuntimeException("Couldn't find anything."));
}
public String getName() {

View File

@@ -36,7 +36,8 @@ class EmployeeWithManagerResourceAssembler implements SimpleRepresentationModelA
@Override
public void addLinks(EntityModel<EmployeeWithManager> resource) {
resource.add(linkTo(methodOn(EmployeeController.class).findDetailedEmployee(resource.getContent().getId())).withSelfRel());
resource.add(
linkTo(methodOn(EmployeeController.class).findDetailedEmployee(resource.getContent().getId())).withSelfRel());
resource.add(linkTo(methodOn(EmployeeController.class).findOne(resource.getContent().getId())).withRel("summary"));
resource.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees"));
}

View File

@@ -37,15 +37,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
@NoArgsConstructor
class Manager {
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue private Long id;
private String name;
/**
* To break the recursive, bi-directional interface, don't serialize {@literal employees}.
*/
@JsonIgnore
@OneToMany(mappedBy = "manager")
@JsonIgnore //
@OneToMany(mappedBy = "manager") //
private List<Employee> employees = new ArrayList<>();
Manager(String name) {

View File

@@ -15,8 +15,8 @@
*/
package org.springframework.hateoas.examples;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -39,30 +39,31 @@ class ManagerController {
/**
* Look up all managers, and transform them into a REST collection resource using
* {@link ManagerRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through
* Spring Web's {@link ResponseEntity} fluent API.
* {@link ManagerRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through Spring Web's
* {@link ResponseEntity} fluent API.
*/
@GetMapping("/managers")
ResponseEntity<CollectionModel<EntityModel<Manager>>> findAll() {
return ResponseEntity.ok(
assembler.toCollectionModel(repository.findAll()));
return ResponseEntity.ok( //
assembler.toCollectionModel(repository.findAll()));
}
/**
* Look up a single {@link Manager} and transform it into a REST resource using
* {@link ManagerRepresentationModelAssembler#toModel(Object)}. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
* {@link ManagerRepresentationModelAssembler#toModel(Object)}. Then return it through Spring Web's
* {@link ResponseEntity} fluent API.
*
* @param id
*/
@GetMapping("/managers/{id}")
ResponseEntity<EntityModel<Manager>> findOne(@PathVariable long id) {
return repository.findById(id)
.map(assembler::toModel)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
return repository.findById(id) //
.map(assembler::toModel) //
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
/**
@@ -73,7 +74,8 @@ class ManagerController {
*/
@GetMapping("/employees/{id}/manager")
ResponseEntity<EntityModel<Manager>> findManager(@PathVariable long id) {
return ResponseEntity.ok(
assembler.toModel(repository.findByEmployeesId(id)));
return ResponseEntity.ok( //
assembler.toModel(repository.findByEmployeesId(id)));
}
}

View File

@@ -17,8 +17,8 @@ package org.springframework.hateoas.examples;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.SimpleIdentifiableRepresentationModelAssembler;
import org.springframework.stereotype.Component;
@@ -33,7 +33,8 @@ class ManagerRepresentationModelAssembler extends SimpleIdentifiableRepresentati
}
/**
* Retain default links provided by {@link SimpleIdentifiableRepresentationModelAssembler}, but add extra ones to each {@link Manager}.
* Retain default links provided by {@link SimpleIdentifiableRepresentationModelAssembler}, but add extra ones to each
* {@link Manager}.
*
* @param resource
*/
@@ -44,11 +45,11 @@ class ManagerRepresentationModelAssembler extends SimpleIdentifiableRepresentati
*/
super.addLinks(resource);
resource.getContent().getId()
.ifPresent(id -> {
// Add custom link to find all managed employees
resource.add(linkTo(methodOn(EmployeeController.class).findEmployees(id)).withRel("employees"));
});
resource.getContent().getId() //
.ifPresent(id -> { //
// Add custom link to find all managed employees
resource.add(linkTo(methodOn(EmployeeController.class).findEmployees(id)).withRel("employees"));
});
}
/**

View File

@@ -35,7 +35,8 @@ class RootController {
resourceSupport.add(linkTo(methodOn(RootController.class).root()).withSelfRel());
resourceSupport.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
resourceSupport.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees"));
resourceSupport
.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees"));
resourceSupport.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers"));
return ResponseEntity.ok(resourceSupport);

View File

@@ -30,16 +30,15 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
* @author Greg Turnquist
*/
@Value
@JsonPropertyOrder({"id", "name", "employees"})
@JsonPropertyOrder({ "id", "name", "employees" })
class Supervisor {
@JsonIgnore
private final Manager manager;
@JsonIgnore private final Manager manager;
public Long getId() {
return this.manager.getId()
.orElseThrow(() -> new RuntimeException("Couldn't find anything"));
return this.manager.getId() //
.orElseThrow(() -> new RuntimeException("Couldn't find anything"));
}
public String getName() {
@@ -47,8 +46,9 @@ class Supervisor {
}
public List<String> getEmployees() {
return manager.getEmployees().stream()
.map(employee -> employee.getName() + "::" + employee.getRole())
.collect(Collectors.toList());
return manager.getEmployees().stream() //
.map(employee -> employee.getName() + "::" + employee.getRole()) //
.collect(Collectors.toList());
}
}

View File

@@ -22,8 +22,8 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* Represent an older controller that has since been replaced with {@link ManagerController}.
* This controller is used to provide legacy routes, i.e. backwards compatibility.
* Represent an older controller that has since been replaced with {@link ManagerController}. This controller is used to
* provide legacy routes, i.e. backwards compatibility.
*
* @author Greg Turnquist
*/
@@ -40,9 +40,10 @@ public class SupervisorController {
public ResponseEntity<EntityModel<Supervisor>> findOne(@PathVariable Long id) {
EntityModel<Manager> managerResource = controller.findOne(id).getBody();
EntityModel<Supervisor> supervisorResource = new EntityModel<>(
new Supervisor(managerResource.getContent()),
managerResource.getLinks());
EntityModel<Supervisor> supervisorResource = new EntityModel<>( //
new Supervisor(managerResource.getContent()), //
managerResource.getLinks());
return ResponseEntity.ok(supervisorResource);
}

25
pom.xml
View File

@@ -13,6 +13,20 @@
<inceptionYear>2017-2019</inceptionYear>
<contributors>
<contributor>
<name>Greg L. Turnquist</name>
<organization>Pivotal</organization>
<organizationUrl>https://spring.io</organizationUrl>
<roles>
<role>Project Lead</role>
</roles>
<timezone>-6</timezone>
<url>https://spring.io/team/gturnquist</url>
<email>gturnquist (at) pivotal.io</email>
</contributor>
</contributors>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
@@ -145,6 +159,17 @@
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-libs-snapshot</id>

View File

@@ -28,13 +28,14 @@ import org.springframework.stereotype.Component;
class DatabaseLoader {
/**
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run
* only after the app is operational, the database will be up.
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run only after the app
* is operational, the database will be up.
*
* @param repository
*/
@Bean
CommandLineRunner init(EmployeeRepository repository) {
return args -> {
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
repository.save(new Employee("Bilbo", "Baggins", "burglar"));

View File

@@ -25,18 +25,14 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum.
*
* {@code @Data} - Generates getters, setters, toString, hash, and equals functions
* {@code @Entity} - JPA annotation to flag this class for DB persistence
* {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum. {@code @Data} -
* Generates getters, setters, toString, hash, and equals functions {@code @Entity} - JPA annotation to flag this class
* for DB persistence {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
* {@code @AllArgsConstructor} - Create a constructor with all args to support testing
*
* {@code @JsonIgnoreProperties(ignoreUnknow=true)}
* When converting JSON to Java, ignore any unrecognized attributes. This is critical for REST because it
* encourages adding new fields in later versions that won't break. It also allows things like _links to be
* ignore as well, meaning HAL documents can be fetched and later posted to the server without adjustment.
*
* {@code @JsonIgnoreProperties(ignoreUnknow=true)} When converting JSON to Java, ignore any unrecognized attributes.
* This is critical for REST because it encourages adding new fields in later versions that won't break. It also allows
* things like _links to be ignore as well, meaning HAL documents can be fetched and later posted to the server without
* adjustment.
*
* @author Greg Turnquist
*/
@@ -46,8 +42,7 @@ import javax.persistence.Id;
@AllArgsConstructor
class Employee {
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue private Long id;
private String firstName;
private String lastName;
private String role;

View File

@@ -23,10 +23,10 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -50,21 +50,21 @@ class EmployeeController {
}
/**
* Look up all employees, and transform them into a REST collection resource.
* Then return them through Spring Web's {@link ResponseEntity} fluent API.
* Look up all employees, and transform them into a REST collection resource. Then return them through Spring Web's
* {@link ResponseEntity} fluent API.
*/
@GetMapping("/employees")
ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
List<EntityModel<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.collect(Collectors.toList());
.map(employee -> new EntityModel<>(employee, //
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), //
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) //
.collect(Collectors.toList());
return ResponseEntity.ok(
new CollectionModel<>(employees,
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
return ResponseEntity.ok( //
new CollectionModel<>(employees, //
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
}
@PostMapping("/employees")
@@ -73,32 +73,32 @@ class EmployeeController {
try {
Employee savedEmployee = repository.save(employee);
EntityModel<Employee> employeeResource = new EntityModel<>(savedEmployee,
linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel());
EntityModel<Employee> employeeResource = new EntityModel<>(savedEmployee, //
linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel());
return ResponseEntity
.created(new URI(employeeResource.getRequiredLink(IanaLinkRelations.SELF).getHref()))
.body(employeeResource);
return ResponseEntity //
.created(new URI(employeeResource.getRequiredLink(IanaLinkRelations.SELF).getHref())) //
.body(employeeResource);
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body("Unable to create " + employee);
}
}
/**
* Look up a single {@link Employee} and transform it into a REST resource. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
* Look up a single {@link Employee} and transform it into a REST resource. Then return it through Spring Web's
* {@link ResponseEntity} fluent API.
*
* @param id
*/
@GetMapping("/employees/{id}")
ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
return repository.findById(id)
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
return repository.findById(id) //
.map(employee -> new EntityModel<>(employee, //
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), //
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) //
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
/**
@@ -118,9 +118,7 @@ class EmployeeController {
Link newlyCreatedLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
try {
return ResponseEntity.noContent()
.location(new URI(newlyCreatedLink.getHref()))
.build();
return ResponseEntity.noContent().location(new URI(newlyCreatedLink.getHref())).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate);
}

View File

@@ -22,5 +22,4 @@ import org.springframework.data.repository.CrudRepository;
*
* @author Greg Turnquist
*/
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
interface EmployeeRepository extends CrudRepository<Employee, Long> {}

View File

@@ -42,37 +42,35 @@ import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(EmployeeController.class)
public class EmployeeControllerTests {
@Autowired
private MockMvc mvc;
@Autowired private MockMvc mvc;
@MockBean
private EmployeeRepository repository;
@MockBean private EmployeeRepository repository;
@Test
public void getShouldFetchAHalDocument() throws Exception {
given(repository.findAll()).willReturn(
Arrays.asList(
new Employee(1L,"Frodo", "Baggins", "ring bearer"),
new Employee(2L,"Bilbo", "Baggins", "burglar")));
given(repository.findAll()).willReturn( //
Arrays.asList( //
new Employee(1L, "Frodo", "Baggins", "ring bearer"), //
new Employee(2L, "Bilbo", "Baggins", "burglar")));
mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees")))
.andReturn();
mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) //
.andDo(print()) //
.andExpect(status().isOk()) //
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees"))) //
.andReturn();
}
}