diff --git a/affordances/README.adoc b/affordances/README.adoc index ad9f624..b684f1a 100644 --- a/affordances/README.adoc +++ b/affordances/README.adoc @@ -59,7 +59,7 @@ Here is the basic definition: ---- @Data @Entity -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor class Employee { diff --git a/affordances/src/main/java/org/springframework/hateoas/examples/Employee.java b/affordances/src/main/java/org/springframework/hateoas/examples/Employee.java index a046d5d..c789be7 100644 --- a/affordances/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/affordances/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -38,7 +38,7 @@ */ @Data @Entity -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor class Employee { diff --git a/api-evolution/README.adoc b/api-evolution/README.adoc index 8ec3bea..008c3fc 100644 --- a/api-evolution/README.adoc +++ b/api-evolution/README.adoc @@ -83,19 +83,19 @@ Start by adding a *root node* for clients to start from. The idea is to find rel [source,java] ---- @GetMapping("/") -public ResourceSupport root() { +public RepresentationModel root() { - ResourceSupport rootResource = new ResourceSupport(); + 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; + return rootResource; } ---- -Since the root node only needs to serve links, creating a bare `ResourceSupport` object is quite sufficient. +Since the root node only needs to serve links, creating a bare `RepresentationModel` object is quite sufficient. * Add a link to `EmployeeController.root` for a proper *self* link * It also adds a link to `EmployeeController.findAll` and names it *employees*. @@ -116,7 +116,7 @@ public ResponseEntity> newEmployee(@RequestBody Employee e } ---- -This API can now process *PUT* requests, deserializing JSON found in the HTTP request body into an `Employee` record. +This API can now process *POST* requests, deserializing JSON found in the HTTP request body into an `Employee` record. From there, it will save it using `EmployeeRepository.save`, getting back a record that includes the id. @@ -274,7 +274,7 @@ It isn't necessary to post ALL of the Thymeleaf template `index.html`, but the c ---- This shows the employee data being served up inside an HTML table. -* `th:each="employee : ${employees}"` lets your iterate over each one. +* `th:each="employee : ${employees}"` lets you iterate over each one. * `th:text="${employee.content.name}"` navigates the `EntityModel` structure (remmeber, you're iterating over each entry of `CollectionModel<>`). * `${employee.links}` gives each entry access to a Spring HATEOAS `Link`. * `` lets you show the end user each link, both name and URI. diff --git a/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/Employee.java b/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/Employee.java index bf6066f..3b0838e 100644 --- a/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -29,7 +29,7 @@ * @author Greg Turnquist */ @Data -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity class Employee { diff --git a/basics/README.adoc b/basics/README.adoc index 08a5267..dae5cf5 100644 --- a/basics/README.adoc +++ b/basics/README.adoc @@ -14,7 +14,7 @@ The cornerstone of any example is the domain object: ---- @Data @Entity -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) class Employee { diff --git a/hypermedia/README.adoc b/hypermedia/README.adoc index 45fa255..e40bef7 100644 --- a/hypermedia/README.adoc +++ b/hypermedia/README.adoc @@ -102,7 +102,7 @@ class Employee { } ---- -This is very similar to what you saw in *Basics*, except that now there is a 1-to-1 JPA relationship in the *manager* field. +This is very similar to what you saw in *Basics*, except that now there is a 1-to-Many JPA relationship in the *manager* field. The constructor call has also been updated. Finally, the same stack overflow is blocked from this end by also putting a `@JsonIgnore` Jackson annotation on the *manager* field. @@ -166,7 +166,7 @@ class ManagerController { } ---- -This controller should look familar, since it's almost identical to `EmployeeController` as seen in link:../api-evolution[API Evolution]. +This controller should look familiar, since it's almost identical to `EmployeeController` as seen in link:../api-evolution[API Evolution]. You have simply swapped */employees* with */managers* and plugged in `ManagerRepository` and `ManagerResourceAssembler`. IMPORTANT: It's not a requirement to use a `ResourceAssembler`. But having one place to define all links for a given domain object @@ -198,11 +198,17 @@ class EmployeeController { */ @GetMapping("/managers/{id}/employees") public ResponseEntity>> findEmployees(@PathVariable long id) { - return ResponseEntity.ok( - assembler.toCollectionModel(repository.findByManagerId(id))); + CollectionModel> collectionModel = assembler + .toCollectionModel(repository.findByManagerId(id)); + + Links newLinks = collectionModel.getLinks().merge(Links.MergeMode.REPLACE_BY_REL, + linkTo(methodOn(EmployeeController.class).findEmployees(id)).withSelfRel()); + + return ResponseEntity.ok(CollectionModel.of(collectionModel.getContent(), newLinks)); } } ---- +The replacement of the self link resolves a link:https://github.com/spring-projects/spring-hateoas-examples/issues/29[wrong self link issue] We've added another route, but how are we getting the data? Oh yeah, we need to add another finder! @@ -259,8 +265,8 @@ method and invoking `super.addLinks()` in order to include those links. Then you IMPORTANT: You can either _add_ to the links defined by `SimpleIdentifiableRepresentationModelAssembler` as shown, or you can totally replace them by _not_ invoking `super.addLinks()`. Your choice. -There is a corresponding combination of a route/repository finder/assembler to allow an employee to find his or her manager. It's left as an exericise -for you to discover it in `ManagerController`, `ManagerRepository`, and `EmployeeResourceAssembler`. +There is a corresponding combination of a route/repository finder/assembler to allow an employee to find his or her manager. It's left as an exercise +for you to discover it in `ManagerController`, `ManagerRepository`, and `EmployeeRepresentationModelAssembler`. == Augmenting Representations @@ -268,7 +274,7 @@ Some critics of REST will point to certain toolkits or coded solutions and argue a relational set of tables that through 3NF (3rd Normal Form) split up data between a parent/child relationship. In essence, part of the data is in the parent table, part in the child table. The parent table's data is shown along with a link to navigate to the child table's data. -This is a false comparison, because REST wholely supports merging data if it makes sense. In DDD, such items are referred to as *aggregates*. +This is a false comparison, because REST wholly supports merging data if it makes sense. In DDD, such items are referred to as *aggregates*. Nothing about a REST resource is confined by the rules of 3NF, written forty years ago. That can simply be shortfall of certain toolkits (but not Spring HATEOAS!) @@ -325,11 +331,11 @@ public ResponseEntity>> findAll @GetMapping("/employees/{id}/detailed") public ResponseEntity> findDetailedEmployee(@PathVariable Long id) { - Employee employee = repository.findOne(id); - - return ResponseEntity.ok( - employeeWithManagerResourceAssembler.toEntityModel( - new EmployeeWithManager(employee))); + return repository.findById(id) // + .map(EmployeeWithManager::new) // + .map(employeeWithManagerResourceAssembler::toModel) // + .map(ResponseEntity::ok) // + .orElse(ResponseEntity.notFound().build()); } ---- @@ -430,7 +436,6 @@ In order to "start at the top" and hop, you must include a `RootController`: [source,java] ---- @RestController -@RestController class RootController { @GetMapping("/") @@ -448,7 +453,7 @@ class RootController { } ---- -Because there is no data at the top, just links, returning back a `ResourceSupport` is perfect. This allows defining all the top links. +Because there is no data at the top, just links, returning back a `ResponseEntity` is perfect. This allows defining all the top links. And it's easy to go into the various `ResourceAssemblers` and add a link back to the top as needed. It's up to you to see which bits of hypermedia serve such a link. @@ -468,8 +473,7 @@ was gone, we can add a DTO to represent the old format based on `Manager` like t [source,java] ---- /** - * Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an after - * thought. + * Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an afterthought. * * @author Greg Turnquist */ @@ -541,7 +545,7 @@ it leverages the `ManagerController`. Is that a good idea or a bad idea? Again, there are tradeoffs. This example is meant to illustrate other options. In this case, leveraging `ManagerController` -allows all links to be generated courtesy of the `ManagerResourceAssembler`. When a `ResponseEntity>` object +allows all links to be generated courtesy of the `ManagerRepresentationModelAssembler`. When a `ResponseEntity>` object is returned by the controller, its wrapped REST resource is extracted by Spring MVC's `getBody()` method. A new `Supervisor` REST resource is constructed by injecting the `Manager` into a `Supervisor` DTO. The provided links are diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java index 407a2fd..55f42c3 100644 --- a/hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java @@ -15,12 +15,13 @@ */ package org.springframework.hateoas.examples; -import java.util.Arrays; - import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; +import java.util.Arrays; +import java.util.Collections; + /** * @author Greg Turnquist */ @@ -49,7 +50,7 @@ CommandLineRunner initDatabase(EmployeeRepository employeeRepository, ManagerRep Employee sam = employeeRepository.save(new Employee("Sam", "gardener", saruman)); - saruman.setEmployees(Arrays.asList(sam)); + saruman.setEmployees(Collections.singletonList(sam)); managerRepository.save(saruman); }; diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/Employee.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/Employee.java index 71b4ae1..03df411 100644 --- a/hypermedia/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -20,10 +20,7 @@ import java.util.Optional; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.OneToOne; +import javax.persistence.*; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -42,7 +39,7 @@ class Employee { /** * To break the recursive, bi-directional relationship, don't serialize {@literal manager}. */ - @JsonIgnore @OneToOne private Manager manager; + @JsonIgnore @ManyToOne private Manager manager; Employee(String name, String role, Manager manager) { diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java index 44cade9..8e5b986 100644 --- a/hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java @@ -15,17 +15,15 @@ */ package org.springframework.hateoas.examples; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.Value; import java.util.List; import java.util.stream.Collectors; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; - /** - * Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an after - * thought. + * Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an afterthought. * * @author Greg Turnquist */ diff --git a/simplified/README.adoc b/simplified/README.adoc index 15a9c58..31903f3 100644 --- a/simplified/README.adoc +++ b/simplified/README.adoc @@ -12,7 +12,7 @@ The cornerstone of any example is the domain object: ---- @Data @Entity -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor class Employee { @@ -30,7 +30,7 @@ This domain object includes: * `@Data` - Lombok annotation to define a mutable value object * `@Entity` - JPA annotation to make the object storagable in a classic SQL engine (H2 in this example) -* `@NoArgsConstructor(PRIVATE)` - Lombok annotation to create an empty constructor call to appease Jackson, but which is private and not usable to our app's code. +* `@NoArgsConstructor(PROTECTED)` - Lombok annotation to create an empty constructor call to appease JPA, but which is protected and not usable to our app's code. * `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios == Accessing Data diff --git a/simplified/src/main/java/org/springframework/hateoas/examples/Employee.java b/simplified/src/main/java/org/springframework/hateoas/examples/Employee.java index a83a5ee..7895f1c 100644 --- a/simplified/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/simplified/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -38,7 +38,7 @@ */ @Data @Entity -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor class Employee {