Polishing.
This commit is contained in:
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class InitDatabase {
|
||||
|
||||
@Bean
|
||||
CommandLineRunner loadEmployees() {
|
||||
|
||||
return args -> {
|
||||
repository.save(new Employee("Frodo", "ring bearer"));
|
||||
repository.save(new Employee("Bilbo", "burglar"));
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class DatabaseLoader {
|
||||
@Bean
|
||||
CommandLineRunner initDatabase(EmployeeRepository employeeRepository, ManagerRepository managerRepository) {
|
||||
return args -> {
|
||||
|
||||
/*
|
||||
* Gather Gandalf's team
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
25
pom.xml
@@ -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>
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user