Skip to content

Sync README with updated code #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion affordances/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Here is the basic definition:
----
@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
class Employee {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
*/
@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
class Employee {

Expand Down
18 changes: 9 additions & 9 deletions api-evolution/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand All @@ -116,7 +116,7 @@ public ResponseEntity<EntityModel<Employee>> 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.

Expand Down Expand Up @@ -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<Employee>` structure (remmeber, you're iterating over each entry of `CollectionModel<>`).
* `${employee.links}` gives each entry access to a Spring HATEOAS `Link`.
* `<a th:text="${link.rel}" th:href="${link.href}" />` lets you show the end user each link, both name and URI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* @author Greg Turnquist
*/
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
class Employee {

Expand Down
2 changes: 1 addition & 1 deletion basics/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 21 additions & 17 deletions hypermedia/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -198,11 +198,17 @@ class EmployeeController {
*/
@GetMapping("/managers/{id}/employees")
public ResponseEntity<CollectionModel<EntityModel<Employee>>> findEmployees(@PathVariable long id) {
return ResponseEntity.ok(
assembler.toCollectionModel(repository.findByManagerId(id)));
CollectionModel<EntityModel<Employee>> 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!

Expand Down Expand Up @@ -259,16 +265,16 @@ 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

Some critics of REST will point to certain toolkits or coded solutions and argue that "hopping" can be inefficient. A common example is
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!)

Expand Down Expand Up @@ -325,11 +331,11 @@ public ResponseEntity<CollectionModel<EntityModel<EmployeeWithManager>>> findAll
@GetMapping("/employees/{id}/detailed")
public ResponseEntity<EntityModel<EmployeeWithManager>> 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());
}
----

Expand Down Expand Up @@ -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("/")
Expand All @@ -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<RepresentationModel>` 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.
Expand All @@ -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
*/
Expand Down Expand Up @@ -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<EntityModel<Manager>>` object
allows all links to be generated courtesy of the `ManagerRepresentationModelAssembler`. When a `ResponseEntity<EntityModel<Manager>>` 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
4 changes: 2 additions & 2 deletions simplified/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
*/
@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
class Employee {

Expand Down