diff --git a/affordances/README.adoc b/affordances/README.adoc index 1c3a9be..46d3441 100644 --- a/affordances/README.adoc +++ b/affordances/README.adoc @@ -1,6 +1,7 @@ = Spring HATEOAS - Affordances Example -This guide shows the opportunities to build hypermedia that http://amundsen.com/blog/archives/1109[affords] operations using https://rwcbook.github.io/hal-forms/[HAL-FORMS]. +This guide shows the opportunities to build hypermedia that http://amundsen.com/blog/archives/1109[affords] operations +using https://rwcbook.github.io/hal-forms/[HAL-FORMS]. Before proceeding, have you read these yet? @@ -14,28 +15,53 @@ NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writ == Building "affordances" -For starters, what is an *affordance*? It's what the hypermedia lets you _do_. With a HAL document, you are provided with data and links, but nothing else. +For starters, what is an *affordance*? Doing a little archeology, Mike Admundsen, a REST advocate, has +http://amundsen.com/blog/archives/1109[an article detailing the word's origins], going back at least to 1986: -This is the beauty of REST. By not having one megaspec, REST is able to adjust and adapt by adopting new mediatypes. So far, we've seen HAL, a lightweight -mediatype that inludes data and links. However, HAL doesn't illustrate what can be done with links. It's possible to use content negotation against -the links to see what REST verbs are supported, but even with that discovery, we still wouldn't know all the characteristics of a resource's properties. +[verse, The Ecological Approach to Visual Perception (Gibson)] +The affordances of the environment are what it offers ... what it provides or furnishes, either for good or ill. +The verb 'to afford' is found in the dictionary, but the noun 'affordance' is not. I have made it up (page 126). -HAL-FORMS, an extension of HAL, attempts to bridge this gap. It supports the HAL concept of data + links, but introduces another element, *$$_templates$$*. -*$$_templates$$* make it possible to show all the operations possible as the attributes needed for each operation. +It then appeared in a psychology paper in 1988: + +[verse, The Design|Psychology of Everyday Things (Norman)] +...the term affordance refers to the perceived and actual properties of the thing, primarily those fundamental properties +that determine just how the thing could possibly be used. (pg 9) + +Finally, it can be found in none other than one of Roy Fielding's presentations on hypermedia in 2008: + +[verse, Slide presention on REST (Fielding)] +When I say Hypertext, I mean the simultaneous presentation of information and controls such that the information becomes +the affordance through which the user obtains choices and selects actions (slide #50). + +In all these situations, "affordance" refers to the available actions provided by the surrounding environment. In the +context of REST, these are actions detailed by the hypermedia. With a HAL document, you are provided very simple affordances. +The links are shown but nothing else about them. What you can do with the links and what it takes to interact with them +is not detailed. + +We can use content negotation to discover what HTTP verbs are supported. And we can take a shot at supplying properties +based on existing data records. But the beauty of REST is that by not having a single megaspec, you can adjust and adapt +by adopting new mediatypes. HAL has been well received, but perhaps there is something better? + +HAL-FORMS, an extension of HAL, attempts to bridge this gap. It supports HAL's lightweight of concept of data and links, +but introduces another element, *_templates*. *_templates* make it possible to show all the operations possible as well as +the attributes needed for each operation. + +The following bits of code show how to use Spring HATEOAS's Affordances API to produce HAL-FORMS documents. == Defining Your Domain -This example takes off where Basics and API Evolution end: an employee payroll system. Only this time, you will create hypermedia-driven operations in the form of *templates*. +This example takes off where Basics and API Evolution end: an employee payroll system. -For starters, here is the basic definition: +Here is the basic definition: [source,java] ---- -Data +@Data @Entity @NoArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor -class Employee implements Identifiable { +class Employee { @Id @GeneratedValue private Long id; @@ -52,10 +78,6 @@ class Employee implements Identifiable { this.lastName = lastName; this.role = role; } - - public Optional getId() { - return Optional.ofNullable(this.id); - } } ---- @@ -69,95 +91,9 @@ interface EmployeeRepository extends CrudRepository { } ---- -From here, we need to build a resource assembler. +This barebones repository provides standard Spring Data CRUD operations. -First of all, we need to extend `SimpleIdentifiableResourceAssembler` and hook it to `Employee`: - -[source,java] ----- -@Component -class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { - - /** - * Link the {@link Employee} domain type to the {@link EmployeeController} using this - * {@link SimpleIdentifiableResourceAssembler} in order to generate both {@link org.springframework.hateoas.Resource} - * and {@link org.springframework.hateoas.Resources}. - */ - EmployeeResourceAssembler() { - super(EmployeeController.class); - } - - ... -} ----- - -This links the domain type of `Employee` to the Spring MVC controller (you'll build further down) `EmployeeController`. This makes it possible to -build links. - -Next you need to define the links for a single resource `Employee` (denoted by Spring HATEOAS's `Resource`): - -[source,java] ----- -@Component -class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { - - ... - - /** - * Define links to add to every {@link Resource}. - * - * @param resource - */ - @Override - protected void addLinks(Resource resource) { - - resource.getContent().getId() - .ifPresent(id -> resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel() - .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))))); - - resource.add(getCollectionLinkBuilder().withRel(this.getRelProvider().getCollectionResourceRelFor(this.getResourceType()))); - } - ... -} ----- - -A link is built by looking up the `getCollectionLinkBuilder()` to find the collection name (using Spring HATEOAS's `RelProvider` to turn `Employee` into `employees`), -followed by a slash ("/"), and the content of the `Resource`. Because this resource implements `Identifiable`, Spring -HATEOAS know how to get the `id` of the resource. This URI is converted into a Spring HATEOAS *self* `Link` through `withSelfRel()`. - -When it comes to HAL documents, this is all we need. HAL is based on data combined with simple links. - -Affordances is about chaining related links together to support richer mediatypes. In this case, we have HAL-FORMS support. This means -we can connect the *GET* link to its related *PUT* link using the `andAffordance(afford(methodOn(...))`. - -The `methodOn()` API works just like the other examples show. But the `afford()` operation, based on web-specific technology (in this -case Spring MVC), is able to look up details about the endpoint and flesh out the *_templates* section of a HAL-FORMS document. - -Similarly, you need to link the aggregate *GET* link to the corresponding *POST* link. See below: - -[source,java] ----- -@Component -class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { - - ... - /** - * Define links to add to {@link Resources} collection. - * - * @param resources - */ - @Override - protected void addLinks(Resources> resources) { - resources.add(getCollectionLinkBuilder().withSelfRel() - .andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))); - } -} ----- - -This code uses the same `getCollectionBuilder()` to point to the collection (`employees`) and connect to the controller's `newEmployee` -Spring MVC method. - -So you want to round this out by defining the controller. +Next, you define a REST controller: [source,java] ---- @@ -165,24 +101,21 @@ So you want to round this out by defining the controller. class EmployeeController { private final EmployeeRepository repository; - private final EmployeeResourceAssembler assembler; - - EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) { + EmployeeController(EmployeeRepository repository) { this.repository = repository; - this.assembler = assembler; } ... } ---- -For starters, you can declare a controller like this: +This controller has these characteristics: * `@RestController` makes the entire controller render responses as direct JSON and not rendered templates. -* Injects `EmployeeRepository` and `EmployeeResourceAssembler` through constructor injection. +* Injects `EmployeeRepository` through constructor injection. -Next, you need to define endpoints for the aggregate collection: +Next, you need to define Spring MVC endpoints for the aggregate collection: [source,java] ---- @@ -193,17 +126,32 @@ class EmployeeController { @GetMapping("/employees") ResponseEntity>> findAll() { - return ResponseEntity.ok( - assembler.toResources(repository.findAll())); + + List> employeeResources = StreamSupport.stream(repository.findAll().spliterator(), false) + .map(employee -> new Resource<>(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 Resources<>(employeeResources, + linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel() + .andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))))); } @PostMapping("/employees") ResponseEntity newEmployee(@RequestBody Employee employee) { - return repository.save(employee).getId() - .map(this::findOne) - .map(HttpEntity::getBody) - .flatMap(ResourceSupport::getId) + Employee savedEmployee = repository.save(employee); + + return new Resource<>(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") + ).getId() .map(Link::getHref) .map(href -> { try { @@ -220,16 +168,117 @@ class EmployeeController { } ---- -This fragment of the controller shows: +Look at these controller details: -* A *GET* call for the aggregate collection is defined. It uses the repository's `findAll()` method and transforms it into a `Resources>` -using the `EmployeeResourceAssembler`. -* A *POST* call for creating new employees is also defined, on the same URI. `@RequestBody` tells Spring MVC to deserialize the request body into an `Employee` object, -which is then sent through the repository's `save()` operation. From there, you grab the `Optional` *id* +* A *GET* call for the aggregate collection is laid out. It uses the repository's `findAll()` method and transforms it +into a `Resources>`. +* A *POST* call for creating new employees is also defined, on the same URI. `@RequestBody` tells Spring MVC to deserialize +the request body into an `Employee` object, which is then sent through the repository's `save()` operation. From there, +it's wrapped as a `Resource` with links added to itself and to the aggregate root. Finally a `Location` response header +* Buried in both endpoints is the new `.andAffordance()` API. Instead of `linkTo()`, you instead use the `afford()` API +to show related information. -The premise is that the *POST* endpoint is related to the *GET* endpoint. In other words, the URI at `/employees` services a *GET* call while _also affording_ a *POST* call. +Affordances is about chaining related links together to support richer mediatypes. In this case, we have HAL-FORMS support. This means +we can connect the *GET* link to its related *POST* link using the `andAffordance(afford(methodOn(...))`. A given link can +also connect to multiple affordances. That's why this example also shows linking to the `deleteEmployee` endpoint as well. -To get this operational, you must do one additional step--reconfigure hypermedia. By default, Spring Boot sets things up for HAL. To switch to HAL-FORMS, you need to create this: +The `methodOn()` API works just like the other examples show. But the `afford()` operation, based on web-specific technology (in this +case Spring MVC), is able to look up details about the endpoint and flesh out the *_templates* section of a HAL-FORMS document. + +The premise is that the *POST* endpoint and the *DELETE* endpoint are related to the *GET* endpoint. In other words, the +URI at `/employees` services a *GET* call while _also affording_ a *POST* call and a *DELETE* call. And with the Affordances +API, it captures the important details found in the related Spring MVC endpoint. When fetching a list of employees, there +are two sets of links, the links for each individual entry along with the aggregate links. + +In the aggregate links, you can see a *self* link to the collection, but connected, i.e. afforded to the `newEmployee()` +endpoint. For each individual employee in the collection, there is a *self* link to itself along with an affordance to +the `updateEmployee()` endpoint, that you'll define next. + +Check it out below: + +[source,java] +---- +@RestController +class EmployeeController { + + ... + + @GetMapping("/employees/{id}") + ResponseEntity> findOne(@PathVariable long id) { + + return repository.findById(id) + .map(employee -> new Resource<>(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}") + ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable long id) { + + Employee employeeToUpdate = employee; + employeeToUpdate.setId(id); + + Employee updatedEmployee = repository.save(employeeToUpdate); + + return new Resource<>(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") + ).getId() + .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)); + } + + ... +} +---- + +This will look very similar, but focused on single item employees. + +NOTE: Are you sensing a repeat of code just seen? Like how the same links and affordances are defined here as was shown for +each individual part of the aggregate root? It's possible to refactor this code into a `ResourceAssembler` to define a single +location, but for simplicity, it has been left out of this example. + +To round out this controller, you must also code the `deleteEmployee()` operation: + +[source,java] +---- + @RestController + class EmployeeController { + + ... + + @DeleteMapping("/employees/{id}") + ResponseEntity deleteEmployee(@PathVariable long id) { + + repository.deleteById(id); + + return ResponseEntity.noContent().build(); + } + + ... +} +---- + +This operation is quite simple. It deletes based upon *id* then returns an `HTTP 204 No Content` response. + +Our controller has been made more sophisticated by linking related operations together. However, to take advantage of this, +we must shift gears and use a different hypermedia. This demands on additional step. By default, Spring Boot sets things +up for HAL. To switch to HAL-FORMS, you need to create this: [source,java] ---- @@ -289,8 +338,8 @@ There is lot packed in here: * When you use this annotation, all of Spring Boot's autoconfigured hypermedia support is disabled. You are taking over, so the rest of the code is about finding any registered `ObjectMapper` beans in the app context and registering the HAL-FORMS support through builtin callbacks. -WARNING: You currently cannot support more than one hypermedia-based mediatype as this point in time. If you try to use both `HAL` and `HAL_FORMS` in the annotation, -Spring Boot will fail to launch. +WARNING: You currently cannot support more than one hypermedia-based mediatype as this point in time. If you try to use +both `HAL` and `HAL_FORMS` in the annotation, Spring Boot will fail to launch. IMPORTANT: We are working on simplifying the means to select different *and* multiple hypermedia formats. @@ -324,8 +373,8 @@ This little database loader will: * The `CommandLineRunner` bean is executed by Spring Boot after the entire application context is up. * Inside that chunk of code, the injected `EmployeeRepository` is used to create a couple database entries. -NOTE: The database for this example is `H2`, an in-memory database that always starts up empty. If you switch to a persistent store, you probably need -to include the extra step to delete old data or you'll get multiple entries. +NOTE: The database for this example is `H2`, an in-memory database that always starts up empty. If you switch to a persistent +store, you probably need to include the extra step to delete old data or you'll get multiple entries. If you launch the application and `GET /employees`, you can expect the following HAL-FORMS result: @@ -390,56 +439,14 @@ This template data is enough information for you to generate an HTML form on a w ---- -You can also define affordances at the individual resource level. In this situation, you can start first by defining the controller methods: +Are you wondering why Spring HATEOAS doesn't simply render an HTML form straight up? There are other mediatypes designed +for this, especially XHTML. Using the Affordances API, we plan to add support in the future, allowing you to negotiate +for the format you prefer. -[source,java] ----- -@RestController -class EmployeeController { +Do you want HAL? HAL-FORMS? SIREN? XHTML? Whatever format you need, the relation between endpoints doesn't have to change. +Simply what you configure the server to render. - ... - - @GetMapping("/employees/{id}") - ResponseEntity> findOne(@PathVariable long id) { - - return repository.findById(id) - .map(assembler::toResource) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - @PutMapping("/employees/{id}") - ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable long id) { - - Employee employeeToUpdate = employee; - employeeToUpdate.setId(id); - - return repository.save(employeeToUpdate).getId() - .map(this::findOne) - .map(HttpEntity::getBody) - .flatMap(ResourceSupport::getId) - .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)); - } - - ... -} ----- - -This augments the same REST controller with a *GET* operation for an individual `Employee` and also defines the corresponding *PUT* operation -to update/edit. - -Take your team to read both flows. The key part you must define, is the corresponding `EmployeeResourceAssembler.toResource(Employee)` method. - -If you ping `/employees/1`, you can see an individual entry: +To round things out, you can also interrogate a single employee resource as shown below: [source,javascript] ---- @@ -479,6 +486,12 @@ If you ping `/employees/1`, you can see an individual entry: "required": true } ] + }, + "deleteEmployee": { + "title": null, + "method": "delete", + "contentType": "", + "properties": [] } } } @@ -486,10 +499,12 @@ If you ping `/employees/1`, you can see an individual entry: * This is very similar to what you saw before, only there is no *_embedded* element. Instead, the resource's data is at the top level. * There are two links: *self* for the canonical link to itself and *employees* to lead back to the aggregate root. -* The method of this template is *put* instead of *post*, indicating this is for updates. -* All the properties are listed, being the same as shown at the aggregate root. +* The method of the default template is *put* instead of *post*, indicating this is for updates. +** All the properties are listed, being the same as shown at the aggregate root. +* There is a second template, *deleteEmployee* with a method of *delete*. It has no properties meaning all we need is the +URI to delete an existing employee. -This information could _also_ be used on your web site to generate update forms: +This information could easily be used on your web site to generate update forms: [source,html] ---- @@ -502,16 +517,26 @@ This information could _also_ be used on your web site to generate update forms: ---- -This is just one example of an update form. +You could also craft another form based on the `deleteEmployee` template: -NOTE: `method="put"` isn't exactly valid HTML5. Either you can handle that in your code, or you have some sort of filter like Spring MVC's -`HiddenHttpMethodFilter` that lets you construct it as `
`, which converts a *POST* into a *PUT* before -invoking the code. +[source,html] +---- + + +
+---- + +These are just a couple ways to render forms based on the hypermedia's templates. + +NOTE: `method="put"` and `method="delete"` aren't exactly valid HTML5. Either you can handle that in your code, or you +have some sort of filter like Spring MVC's `HiddenHttpMethodFilter` that lets you construct it as +`
`, which converts a *POST* into a *PUT* before invoking the code. IMPORTANT: With HAL-FORMS, there is no URI in the template itself. It's presumed to operate on the *self* link. -With the Affordances API, you can link related methods. And with HAL-FORMS support, it's possible to turn those relationships into automated -bits of HTML to enhance the user experience without having to inject domain knowledge into the client layer. +With the Affordances API, you can link related methods. And with HAL-FORMS support, it's possible to turn those +relationships into automated bits of HTML to enhance the user experience without having to inject domain knowledge into +the client layer. -And that's a key part of REST--reducing the amount of domain knowledge found in the client, allowing the client to more easily adapt to -changes on the server. \ No newline at end of file +And that's a key part of REST--reducing the amount of domain knowledge needed in the client. But instead pushing relevant +forms straight out to the end user, the client can more easily adapt to changes on the server. \ No newline at end of file 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 259c13b..03b63ce 100644 --- a/affordances/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/affordances/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -20,14 +20,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.Optional; - import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; -import org.springframework.hateoas.Identifiable; - /** * Domain object representing a company employee. Project Lombok keeps actual code at a minimum. * @@ -48,7 +44,7 @@ import org.springframework.hateoas.Identifiable; @Entity @NoArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor -class Employee implements Identifiable { +class Employee { @Id @GeneratedValue private Long id; @@ -65,8 +61,4 @@ class Employee implements Identifiable { this.lastName = lastName; this.role = role; } - - public Optional getId() { - return Optional.ofNullable(this.id); - } } diff --git a/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeController.java b/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeController.java index 24abd88..51cbff8 100644 --- a/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeController.java +++ b/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeController.java @@ -15,15 +15,19 @@ */ package org.springframework.hateoas.examples; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; + import java.net.URI; import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; -import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -38,36 +42,40 @@ import org.springframework.web.bind.annotation.RestController; class EmployeeController { private final EmployeeRepository repository; - private final EmployeeResourceAssembler assembler; - EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) { + EmployeeController(EmployeeRepository repository) { this.repository = repository; - this.assembler = assembler; } @GetMapping("/employees") ResponseEntity>> findAll() { - return ResponseEntity.ok( - assembler.toResources(repository.findAll())); - } - @GetMapping("/employees/{id}") - ResponseEntity> findOne(@PathVariable long id) { + List> employeeResources = StreamSupport.stream(repository.findAll().spliterator(), false) + .map(employee -> new Resource<>(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 repository.findById(id) - .map(assembler::toResource) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + return ResponseEntity.ok(new Resources<>(employeeResources, + linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel() + .andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))))); } @PostMapping("/employees") ResponseEntity newEmployee(@RequestBody Employee employee) { - return repository.save(employee).getId() - .map(this::findOne) - .map(HttpEntity::getBody) - .flatMap(ResourceSupport::getId) + Employee savedEmployee = repository.save(employee); + + return new Resource<>(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") + ).getId() .map(Link::getHref) .map(href -> { try { @@ -80,16 +88,34 @@ class EmployeeController { .orElse(ResponseEntity.badRequest().body("Unable to create " + employee)); } + @GetMapping("/employees/{id}") + ResponseEntity> findOne(@PathVariable long id) { + + return repository.findById(id) + .map(employee -> new Resource<>(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}") ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable long id) { Employee employeeToUpdate = employee; employeeToUpdate.setId(id); - return repository.save(employeeToUpdate).getId() - .map(this::findOne) - .map(HttpEntity::getBody) - .flatMap(ResourceSupport::getId) + Employee updatedEmployee = repository.save(employeeToUpdate); + + return new Resource<>(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") + ).getId() .map(Link::getHref) .map(href -> { try { @@ -101,4 +127,12 @@ class EmployeeController { .map(uri -> ResponseEntity.noContent().location(uri).build()) .orElse(ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate)); } + + @DeleteMapping("/employees/{id}") + ResponseEntity deleteEmployee(@PathVariable long id) { + + repository.deleteById(id); + + return ResponseEntity.noContent().build(); + } } diff --git a/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java b/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java deleted file mode 100644 index f7dd5da..0000000 --- a/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.hateoas.examples; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; - -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.Resources; -import org.springframework.hateoas.SimpleIdentifiableResourceAssembler; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -@Component -class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { - - /** - * Link the {@link Employee} domain type to the {@link EmployeeController} using this - * {@link SimpleIdentifiableResourceAssembler} in order to generate both {@link org.springframework.hateoas.Resource} - * and {@link org.springframework.hateoas.Resources}. - */ - EmployeeResourceAssembler() { - super(EmployeeController.class); - } - - /** - * Define links to add to every {@link Resource}. - * - * @param resource - */ - @Override - protected void addLinks(Resource resource) { - - resource.getContent().getId() - .ifPresent(id -> resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel() - .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))))); - - resource.add(getCollectionLinkBuilder().withRel(this.getRelProvider().getCollectionResourceRelFor(this.getResourceType()))); - } - - /** - * Define links to add to {@link Resources} collection. - * - * @param resources - */ - @Override - protected void addLinks(Resources> resources) { - resources.add(getCollectionLinkBuilder().withSelfRel() - .andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))); - } -} diff --git a/affordances/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java b/affordances/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java index d0be990..6158204 100644 --- a/affordances/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java +++ b/affordances/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java @@ -41,7 +41,7 @@ import org.springframework.test.web.servlet.MockMvc; */ @RunWith(SpringRunner.class) @WebMvcTest(EmployeeController.class) -@Import({EmployeeResourceAssembler.class, HypermediaConfiguration.class}) +@Import({HypermediaConfiguration.class}) public class EmployeeControllerTests { @Autowired