diff --git a/README.adoc b/README.adoc index 9211ca6..ef9a2da 100644 --- a/README.adoc +++ b/README.adoc @@ -13,3 +13,4 @@ We have separate folders for each of these: * link:basics[Basics] - Poke and prod at a hypermedia-powered service from inside the code as well as externally using standard tools * link:api-evolution[API Evolution] - Upgrade an existing REST resource +* link:hypermedia[Hypermedia] - Create hypermedia-driven REST resources, linking them together, and supporting older links. \ No newline at end of file diff --git a/api-evolution/README.adoc b/api-evolution/README.adoc index 0df2518..24f016e 100644 --- a/api-evolution/README.adoc +++ b/api-evolution/README.adoc @@ -3,7 +3,11 @@ This guide shows a valuable example of using Spring HATEOAS. It illustrates how to evolve your API while maintaining backward compatible. This is valuable because it reduces the need to https://www.infoq.com/articles/roy-fielding-on-versioning[version your API], a concept not suitable for REST services. -This guide also assumes you have already checked out link:../basics[Basics]. +Before proceeding, have you read these yet? + +. link:../basics[Spring HATEOAS - Basic Example] + +You may wish to read them first before reading this one. Start with a very simple example, a payroll system that tracks employees. Create a server and a client. Then, evolve the server while ensuring the original client can talk to the new one. Finally, upgrade the client and @@ -437,4 +441,6 @@ From here on, the link:new-client[client can also evolve] and take advantage of WARNING: The example code for that doesn't depict the new-client talking to the old-server. -This is but a simple example of making clients and services support each other through typical breaking changes. \ No newline at end of file +This is but a simple example of making clients and services support each other through typical breaking changes. + +For the next step in Spring HATEOAS, you may wish to read link:../hypermedia[Spring HATEOAS - Hypermedia Example]. \ No newline at end of file diff --git a/basics/README.adoc b/basics/README.adoc index 319027b..ad02f3a 100644 --- a/basics/README.adoc +++ b/basics/README.adoc @@ -276,4 +276,6 @@ public void getShouldFetchAHalDocument() throws Exception { The rest of the assertions are commented out, but you can read it in the source code. NOTE: This is not the only way to assert the results. See Spring Framework reference docs and Spring HATEOAS -test cases for more examples. \ No newline at end of file +test cases for more examples. + +For the next step in Spring HATEOAS, you may wish to read link:../api-evolution[Spring HATEOAS - API Evolution Example]. \ No newline at end of file diff --git a/hypermedia/README.adoc b/hypermedia/README.adoc new file mode 100644 index 0000000..92a4c02 --- /dev/null +++ b/hypermedia/README.adoc @@ -0,0 +1,570 @@ += Spring HATEOAS - Hypermedia Example + +This guide shows a more detailed foray into linking resources with hypermedia. It includes automated links, custom ones, +and retaining legacy links to support older clients. + +Before proceeding, have you read these yet? + +. link:../basics[Spring HATEOAS - Basic Example] +. link:../api-evolution[Spring HATEOAS - API Evolution Example] + +You may wish to read them first before reading this one. + +NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. + +== Defining Your Domain + +This example takes off where Basics and API Evolution end: an employee payroll system. Only this time, you'll introduce +a new domain: *managers*. + +You'll explore how to create create REST representations for a manager and tie it into employees. + +For starters, here is the basic definition: + +[source,java] +---- +@Data +@Entity +@NoArgsConstructor +class Manager implements Identifiable { + + @Id @GeneratedValue + private Long id; + private String name; + + /** + * To break the recursive, bi-directional interface, don't serialize {@literal employees}. + */ + @JsonIgnore + @OneToMany(mappedBy = "manager") + private List employees = new ArrayList<>(); + + Manager(String name) { + this.name = name; + } +} +---- + +This is very similar to `Employee`: + +* Uses the same `@Data` Lombok annotation to reduce boilerplate in defining a mutable value object. +* They are stored in a JPA data store using `@Entity`, `@Id`, and `@GeneatedValue`. +* Has a `@NoArgsConstructor` to support Jackson's serializers. + +But it contains a new aspect: a 1-to-many relationship with `Employee` in the form a `List`. + +This domain object initializes the field with an empty list to avoid NPEs. The JPA `@OneToMany` annotation indicates +that the relationship between `Manager` and `Employee` is stored in the database tables in the `Employee` entity's +*manager* property, i.e. the manager's primary key will be stored as a foreign key in the *EMPLOYEE* table. + +WARNING: Bi-directional relationships can be modeled in JPA, but you must carefully handle this. Jackson tends to +navigate as far as possible when serializing, so you have to tell it to stop with the `@JsonIgnore` directive. Otherwise, +it will generate a stack overflow exception when hopping Manager -> Employee -> Manager -> etc. + +A handy constructor is also added to support link:src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java[loading the database] +with sample data. + +A corresponding Spring Data JPA repository is defined: + +[source,java] +---- +interface ManagerRepository extends CrudRepository { +} +---- + +To round things out, you need to make some updates to the `Employee` domain object: + +[source,java] +---- +@Data +@Entity +@NoArgsConstructor +class Employee implements Identifiable { + + @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; + + Employee(String name, String role, Manager manager) { + + this.name = name; + this.role = role; + this.manager = manager; + } +} +---- + +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. + +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. + +With these changes in place, you can now define a `ResourceAssembler` for the `Manager`: + +[source,java] +---- +@Component +class ManagerResourceAssembler extends SimpleIdentifiableResourceAssembler { + + ManagerResourceAssembler() { + super(ManagerController.class); + } +} +---- + +If you follow the same paradigm of extending Spring HATEOAS's `SimpleIdentifiableResourceAssembler` and applying the `Manager` type, +you can easily inherit links for */managers* and */managers/{id}* + +Before we go any further, we need to define those links! + +[source,java] +---- +@RestController +class ManagerController { + + private final ManagerRepository repository; + private final ManagerResourceAssembler assembler; + + ManagerController(ManagerRepository repository, ManagerResourceAssembler assembler) { + + this.repository = repository; + this.assembler = assembler; + } + + /** + * Look up all managers, and transform them into a REST collection resource using + * {@link ManagerResourceAssembler#toResources(Iterable)}. Then return them through + * Spring Web's {@link ResponseEntity} fluent API. + * + * NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different + * default accept header, which allows XML to get requested first, so "produces" + * forces it to HAL JSON for all clients. + */ + @GetMapping(value = "/managers", produces = MediaTypes.HAL_JSON_VALUE) + ResponseEntity>> findAll() { + return ResponseEntity.ok( + assembler.toResources(repository.findAll())); + + } + + /** + * Look up a single {@link Manager} and transform it into a REST resource using + * {@link ManagerResourceAssembler#toResource(Object)}. Then return it through + * Spring Web's {@link ResponseEntity} fluent API. + * + * See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument. + * + * @param id + */ + @GetMapping(value = "/managers/{id}", produces = MediaTypes.HAL_JSON_VALUE) + ResponseEntity> findOne(@PathVariable long id) { + return ResponseEntity.ok( + assembler.toResource(repository.findOne(id))); + } +} +---- + +This controller should look familar, 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 +ensures a consistent representation. + +With the basic routes defined, you could say we have an operational REST service. But it's not fleshed out very well. To truly +power up the hypermedia and serve clients, you need to add links _between_ the relevant domain types. + +NOTE: Up until this point, we've been using the term "domain types" or "domain objects". This is lingo found in Domain Driven Design. +What you are building are *REST resources* and how the various mediatypes they are represented in. The paradigm of REST is +to construct resources that contain both data for the client to consume as well as controls to navigate to related data. + +The first link to navigate from a `Manager` resource to its related `Employee` resources would be a */managers/{id}/employees* +route: + +.ManagerController +[source,java] +---- +@RestController +class ManagerController { + + ... + + /** + * Find an {@link Employee}'s {@link Manager} based upon employee id. Turn it into a context-based link. + * + * @param id + * @return + */ + @GetMapping(value = "/managers/{id}/employees", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity>> findEmployees(@PathVariable long id) { + return ResponseEntity.ok( + assembler.toResources(repository.findByManagerId(id))); + } +} +---- + +We've added another route, but how are we getting the data? Oh yeah, we need to add another finder! + +[source,java] +---- +interface ManagerRepository extends CrudRepository { + + /** + * Navigate through the JPA relationship to find a {@link Manager} based on an {@link Employee}'s {@literal id}. + * + * @param id + * @return + */ + Manager findByEmployeesId(Long id); +} +---- + +With Spring Data, we can define a new finder _just by writing it's method signature!_ This custom finder will navigate by property +and find the first `Manager` who has an `Employee` with an *id* matching the parameter. + +NOTE: Navigation by property is analogous to writing `select MANAGER.* from MANAGER join EMPLOYEE on MANAGER.PK = EMPLOYEE.FK where EMPLOYEE.PK == :id`. +It makes it super simple to navigate over JPA relationships and find what we need. + +This newly minted route needs to be added to every `Manager` representation we render. To do that, we need to make an alteration +to `ManagerResourceAssembler`: + +[source,java] +---- +@Component +class ManagerResourceAssembler extends SimpleIdentifiableResourceAssembler { + + ... + + /** + * Retain default links provided by {@link SimpleIdentifiableResourceAssembler}, but add extra ones to each {@link Manager}. + * + * @param resource + */ + @Override + protected void addLinks(Resource resource) { + /** + * Retain default links. + */ + super.addLinks(resource); + + // Add custom link to find all managed employees + resource.add(linkTo(methodOn(EmployeeController.class).findEmployees(resource.getContent().getId())).withRel("employees")); + } + + ... +} + +---- + +`SimpleIdentifiableResourceAssembler` has methods to alter a resource representation for single items or collections. It has pre-baked +renderings to create a self link to a single item as well as a link back to the collection. In this code, you are extending that +method and invoking `super.addLinks()` in order to include those links. Then you add the link to the manager's employees you just created. + +IMPORTANT: You can either _add_ to the links defined by `SimpleIdentifiableResourceAssembler` 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 `EmployeeController`, `EmployeeRepository`, and `EmployeeResourceAssembler`. + +== 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*. +Nothing about a REST resource is confined to what the rules of 3NF written forty years ago dictates. That can simply be shortfall of certain +toolkits (but not Spring HATEOAS!) + +What if you wanted a detailed `Employee` representation that included the `Manager` details? No problem! Just model it. + +[source,java] +---- +@Value +@JsonPropertyOrder({"id", "name", "role", "manager"}) +public class EmployeeWithManager { + + @JsonIgnore + private final Employee employee; + + public Long getId() { + return this.employee.getId(); + } + + public String getName() { + return this.employee.getName(); + } + + public String getRole() { + return this.employee.getRole(); + } + + public String getManager() { + return this.employee.getManager().getName(); + } + +} +---- + +This _immutable_ value object (thanks to Lombok's `@Value` annotation) is initialized with an `Employee` object. It defines +how it gets rendered through various getter methods. It also subtly does _not_ render the `Employee` object itself. + +IMPORTANT: `Employee` and `Manager` both have a *name* field. With combined representations, there has to be agreement on how these +two fields will appear. In this case, `Employee.name` is kept and `Manager.name` is turned into *manager*. + +To support this, we can write the corresponding route in `EmployeeController`: + +[source,java] +---- +@GetMapping(value = "/employees/detailed", produces = MediaTypes.HAL_JSON_VALUE) +public ResponseEntity>> findAllDetailedEmployees() { + + return ResponseEntity.ok( + employeeWithManagerResourceAssembler.toResources( + StreamSupport.stream(repository.findAll().spliterator(), false) + .map(EmployeeWithManager::new) + .collect(Collectors.toList()))); +} + +@GetMapping(value = "/employees/{id}/detailed", produces = MediaTypes.HAL_JSON_VALUE) +public ResponseEntity> findDetailedEmployee(@PathVariable Long id) { + + Employee employee = repository.findOne(id); + + return ResponseEntity.ok( + employeeWithManagerResourceAssembler.toResource( + new EmployeeWithManager(employee))); +} +---- + +This shows both a collection of "detailed" employees as well as a single one. The collection fetches all employees, uses a Java 8 +stream to convert each `Employee` into an `EmployeeWithManager`, and wraps it into a Spring HATEOAS `Resources` collection. + +The single employee version does the corresponding transformation against a single `Employee`. + +To support building REST resources, you also need a `ResourceAssembler` for `EmployeeWithManager`. This should appear very +familiar by now: + +[source,java] +---- +@Component +class EmployeeWithManagerResourceAssembler extends SimpleResourceAssembler { + + /** + * Define links to add to every individual {@link Resource}. + * + * @param resource + */ + @Override + protected void addLinks(Resource resource) { + + 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")); + } + + /** + * Define links to add to the {@link Resources} collection. + * + * @param resources + */ + @Override + protected void addLinks(Resources> resources) { + + resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withSelfRel()); + resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); + resources.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); + resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); + } +} +---- + +This has a handful of differences from the `ResourceAssembler` objects you've built up to this point: + +* Since the routes are different than traditional */employees* and */employees/{id}*, it makes no sense to use `SimpleIdentifiableResourceAssembler`. + So instead, you want to fall back to `SimpleResourceAssembler`, in which NO links are defined out of the box. +* Because there are no defined routes, you are in full control. +** `addLinks(Resource resource)` defines links for single items +** `addLinks(Resources> resources)` defines links for collections + +In this case, single `EmployeeWithManager` items include a self link to itself, a hop to it's parallel record that only has `Employee` info known as *summary*, +and a link to the detailed collection. To avoid semantic confusion, this is called *detailedEmployees* given *employees* is the common reference to +a collection of summary `Employee` records. + +It also makes sense to add links from the other existing REST resources to this detailed `EmployeeWithManager`. + +WARNING: Even though `addLinks(Resources> resources)` gives you access to a single item's `Resource` object, + it is recommended to NOT manipulate individual item links this way. Instead, use the other method. + +Is this the _only_ way to display a detailed record? Not at all. Spring MVC supports request parameters, so it's not that difficult +to code something like this: + +[source,java] +---- +@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE) +public ResponseEntity findOne(@PathVariable long id, + @RequestParam(value = "detailed", required = false, + defaultValue = false) boolean detailed) { + + if (detailed) { + Employee employee = repository.findOne(id); + + return ResponseEntity.ok( + employeeWithManagerResourceAssembler.toResource( + new EmployeeWithManager(employee))); + } else { + return ResponseEntity.ok( + assembler.toResource(repository.findOne(id))); + } +} +---- + +This type of solution allows serving two different representations from the same URI based on an optional `?detailed=true` +parameter. + +There are tradeoffs either way, but this option lends itself to supporting existing routes that you may already have. + +To find the other places where detailed `EmployeeWithManager` links have been added, inspect all the `ResourceAssembler` objects +in the example's code base. + +== Don't Forget the Root URI + +In order to "start at the top" and hop, you must include a `RootController`: + +[source,java] +---- +@RestController +class RootController { + + @GetMapping("/") + ResponseEntity root() { + + ResourceSupport resourceSupport = new ResourceSupport(); + + 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(ManagerController.class).findAll()).withRel("managers")); + + return ResponseEntity.ok(resourceSupport); + } + +} +---- + +Because there is no data at the top, just links, return back a `ResourceSupport` 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. + + +== Legacy Routes + +What if you started with one set of routes and migrated things to another set? This is the type of scenario that drives people screaming +to version their APIs. + +Instead of shouting "don't version APIs" from the rooftops, and appealing to the authority of Roy Fielding, it's better to see +how it's not that hard to support both old and new routes. + +For this example, assume that before the `Manager` entity and it's `ManagerController` existed, there was a `Supervisor` with a +matching `SupervisorController`. It had similar data but fewer links. A bit more RPC-like. If the original `Supervisor` entity +was gone, we can a DTO to represent the old format based on `Manager` like this: + +[source,java] +---- +/** + * Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an after + * thought. + * + * @author Greg Turnquist + */ +@Value +@JsonPropertyOrder({"id", "name", "employees"}) +class Supervisor { + + @JsonIgnore + private final Manager manager; + + public Long getId() { + return this.manager.getId(); + } + + public String getName() { + return this.manager.getName(); + } + + public List getEmployees() { + return manager.getEmployees().stream() + .map(employee -> employee.getName() + "::" + employee.getRole()) + .collect(Collectors.toList()); + } +} +---- + +This representation assumes old record had: + +* Supervisor's *id*, *name* and a somewhat sloppy display of employee's name and role. +* It's powered by the new `Manager` object, so no need to store multiple copies of data. +* The `Manager` itself is not rendered thanks to the `@JsonIgnore` annotation. + +To honor the old route (*/supervisors/{id}*), create a new controller: + +[source,java] +---- +/** + * 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 + */ +@RestController +public class SupervisorController { + + private final ManagerController controller; + + public SupervisorController(ManagerController controller) { + this.controller = controller; + } + + @GetMapping(value = "/supervisors/{id}", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity> findOne(@PathVariable Long id) { + + Resource managerResource = controller.findOne(id).getBody(); + Resource supervisorResource = new Resource<>( + new Supervisor(managerResource.getContent()), + managerResource.getLinks()); + + return ResponseEntity.ok(supervisorResource); + } +} +---- + +In this example, the assumption is that there was a route for individual supervisors, but not a link for a collection. +This controller has that route, and serves up a `Resource` record. But instead of fetching the data directly, + 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 +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 +then copied into that `Resource` object. + +Hence, this controller will respond to calls for */supervisors/{id}*, but provide links onto the new system should the client +want to gracefully start migrating. + +IMPORTANT: This example also assumes the clients can handle new links as long as the legacy ones are also there. For +a different scenario, that assumption can be adjusted. + +With this amount of linking between related objects and DTOs, it's easy to see how Spring HATEOAS can be used to model +a link-driven API. And with the flexible nature of REST, more links can be added in the future along with additional representations. +As long as the existing links are maintained, clients can have a much easier path of migration. diff --git a/hypermedia/pom.xml b/hypermedia/pom.xml new file mode 100644 index 0000000..276907b --- /dev/null +++ b/hypermedia/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + spring-hateoas-examples-hypermedia + Spring HATEOAS - Examples - Hypermedia + jar + + + org.springframework.hateoas.examples + spring-hateoas-examples + 1.0.0.BUILD-SNAPSHOT + + + + + org.springframework.hateoas.examples + commons + 1.0.0.BUILD-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java new file mode 100644 index 0000000..c815056 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java @@ -0,0 +1,56 @@ +/* + * 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 java.util.Arrays; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @author Greg Turnquist + */ +@Component +class DatabaseLoader { + + @Bean + CommandLineRunner initDatabase(EmployeeRepository employeeRepository, ManagerRepository managerRepository) { + return args -> { + /* + * Gather Gandalf's team + */ + Manager gandalf = managerRepository.save(new Manager("Gandalf")); + + Employee frodo = employeeRepository.save(new Employee("Frodo", "ring bearer", gandalf)); + Employee bilbo = employeeRepository.save(new Employee("Bilbo", "burglar", gandalf)); + + gandalf.setEmployees(Arrays.asList(frodo, bilbo)); + managerRepository.save(gandalf); + + /* + * Put together Saruman's team + */ + Manager saruman = managerRepository.save(new Manager("Saruman")); + + Employee sam = employeeRepository.save(new Employee("Sam", "gardener", saruman)); + + saruman.setEmployees(Arrays.asList(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 new file mode 100644 index 0000000..1d04e89 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -0,0 +1,56 @@ +/* + * 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 javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToOne; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.springframework.hateoas.Identifiable; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * @author Greg Turnquist + */ +@Data +@Entity +@NoArgsConstructor +class Employee implements Identifiable { + + @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; + + Employee(String name, String role, Manager manager) { + + this.name = name; + this.role = role; + this.manager = manager; + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeController.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeController.java new file mode 100644 index 0000000..bd65c0c --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeController.java @@ -0,0 +1,109 @@ +/* + * 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 java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +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; + +/** + * @author Greg Turnquist + */ +@RestController +class EmployeeController { + + private final EmployeeRepository repository; + private final EmployeeResourceAssembler assembler; + private final EmployeeWithManagerResourceAssembler employeeWithManagerResourceAssembler; + + EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler, + EmployeeWithManagerResourceAssembler employeeWithManagerResourceAssembler) { + + this.repository = repository; + this.assembler = assembler; + this.employeeWithManagerResourceAssembler = employeeWithManagerResourceAssembler; + } + + /** + * Look up all employees, and transform them into a REST collection resource using + * {@link EmployeeResourceAssembler#toResources(Iterable)}. Then return them through + * Spring Web's {@link ResponseEntity} fluent API. + * + * NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different + * default accept header, which allows XML to get requested first, so "produces" + * forces it to HAL JSON for all clients. + */ + @GetMapping(value = "/employees", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity>> findAll() { + return ResponseEntity.ok( + assembler.toResources(repository.findAll())); + + } + + /** + * Look up a single {@link Employee} and transform it into a REST resource using + * {@link EmployeeResourceAssembler#toResource(Object)}. Then return it through + * Spring Web's {@link ResponseEntity} fluent API. + * + * See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument. + * + * @param id + */ + @GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity> findOne(@PathVariable long id) { + return ResponseEntity.ok( + assembler.toResource(repository.findOne(id))); + } + + /** + * Find an {@link Employee}'s {@link Manager} based upon employee id. Turn it into a context-based link. + * + * @param id + * @return + */ + @GetMapping(value = "/managers/{id}/employees", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity>> findEmployees(@PathVariable long id) { + return ResponseEntity.ok( + assembler.toResources(repository.findByManagerId(id))); + } + + @GetMapping(value = "/employees/detailed", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity>> findAllDetailedEmployees() { + + return ResponseEntity.ok( + employeeWithManagerResourceAssembler.toResources( + StreamSupport.stream(repository.findAll().spliterator(), false) + .map(EmployeeWithManager::new) + .collect(Collectors.toList()))); + } + + @GetMapping(value = "/employees/{id}/detailed", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity> findDetailedEmployee(@PathVariable Long id) { + + Employee employee = repository.findOne(id); + + return ResponseEntity.ok( + employeeWithManagerResourceAssembler.toResource( + new EmployeeWithManager(employee))); + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java new file mode 100644 index 0000000..fbae800 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java @@ -0,0 +1,29 @@ +/* + * 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 java.util.List; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Greg Turnquist + */ +interface EmployeeRepository extends CrudRepository { + + List findByManagerId(Long id); + +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java new file mode 100644 index 0000000..29ef64e --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java @@ -0,0 +1,72 @@ +/* + * 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 { + + EmployeeResourceAssembler() { + super(EmployeeController.class); + } + + /** + * Define links to add to every {@link Resource}. + * + * @param resource + */ + @Override + protected void addLinks(Resource resource) { + + /** + * Add some custom links to the default ones provided. + * + * NOTE: To replace default links, don't invoke {@literal super.addLinks()}. + */ + super.addLinks(resource); + + // Add additional links + resource.add(linkTo(methodOn(ManagerController.class).findManager(resource.getContent().getId())).withRel("manager")); + resource.add(linkTo(methodOn(EmployeeController.class).findDetailedEmployee(resource.getContent().getId())).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(resource.getContent().getId())).withRel("supervisor")); + } + + /** + * Define links to add to {@link Resources} collection. + * + * @param resources + */ + @Override + protected void addLinks(Resources> resources) { + + super.addLinks(resources); + + resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); + resources.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); + resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManager.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManager.java new file mode 100644 index 0000000..119a25e --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManager.java @@ -0,0 +1,51 @@ +/* + * 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 lombok.Value; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * Class defined purely for hosting an "detailed" point of view for REST. + * + * @author Greg Turnquist + */ +@Value +@JsonPropertyOrder({"id", "name", "role", "manager"}) +public class EmployeeWithManager { + + @JsonIgnore + private final Employee employee; + + public Long getId() { + return this.employee.getId(); + } + + public String getName() { + return this.employee.getName(); + } + + public String getRole() { + return this.employee.getRole(); + } + + public String getManager() { + return this.employee.getManager().getName(); + } + +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManagerResourceAssembler.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManagerResourceAssembler.java new file mode 100644 index 0000000..8dc71b1 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManagerResourceAssembler.java @@ -0,0 +1,57 @@ +/* + * 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.SimpleResourceAssembler; +import org.springframework.stereotype.Component; + +/** + * @author Greg Turnquist + */ +@Component +class EmployeeWithManagerResourceAssembler extends SimpleResourceAssembler { + + /** + * Define links to add to every individual {@link Resource}. + * + * @param resource + */ + @Override + protected void addLinks(Resource resource) { + + 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")); + } + + /** + * Define links to add to the {@link Resources} collection. + * + * @param resources + */ + @Override + protected void addLinks(Resources> resources) { + + resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withSelfRel()); + resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); + resources.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); + resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/Manager.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/Manager.java new file mode 100644 index 0000000..fb34f98 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/Manager.java @@ -0,0 +1,55 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.springframework.hateoas.Identifiable; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * @author Greg Turnquist + */ +@Data +@Entity +@NoArgsConstructor +class Manager implements Identifiable { + + @Id @GeneratedValue + private Long id; + private String name; + + /** + * To break the recursive, bi-directional interface, don't serialize {@literal employees}. + */ + @JsonIgnore + @OneToMany(mappedBy = "manager") + private List employees = new ArrayList<>(); + + Manager(String name) { + this.name = name; + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerController.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerController.java new file mode 100644 index 0000000..019a303 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerController.java @@ -0,0 +1,83 @@ +/* + * 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 org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +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; + +/** + * @author Greg Turnquist + */ +@RestController +class ManagerController { + + private final ManagerRepository repository; + private final ManagerResourceAssembler assembler; + + ManagerController(ManagerRepository repository, ManagerResourceAssembler assembler) { + + this.repository = repository; + this.assembler = assembler; + } + + /** + * Look up all managers, and transform them into a REST collection resource using + * {@link ManagerResourceAssembler#toResources(Iterable)}. Then return them through + * Spring Web's {@link ResponseEntity} fluent API. + * + * NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different + * default accept header, which allows XML to get requested first, so "produces" + * forces it to HAL JSON for all clients. + */ + @GetMapping(value = "/managers", produces = MediaTypes.HAL_JSON_VALUE) + ResponseEntity>> findAll() { + return ResponseEntity.ok( + assembler.toResources(repository.findAll())); + + } + + /** + * Look up a single {@link Manager} and transform it into a REST resource using + * {@link ManagerResourceAssembler#toResource(Object)}. Then return it through + * Spring Web's {@link ResponseEntity} fluent API. + * + * See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument. + * + * @param id + */ + @GetMapping(value = "/managers/{id}", produces = MediaTypes.HAL_JSON_VALUE) + ResponseEntity> findOne(@PathVariable long id) { + return ResponseEntity.ok( + assembler.toResource(repository.findOne(id))); + } + + /** + * Find an {@link Employee}'s {@link Manager} based upon employee id. Turn it into a context-based link. + * + * @param id + * @return + */ + @GetMapping(value = "/employees/{id}/manager", produces = MediaTypes.HAL_JSON_VALUE) + ResponseEntity> findManager(@PathVariable long id) { + return ResponseEntity.ok( + assembler.toResource(repository.findByEmployeesId(id))); + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerRepository.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerRepository.java new file mode 100644 index 0000000..cc931f0 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerRepository.java @@ -0,0 +1,32 @@ +/* + * 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 org.springframework.data.repository.CrudRepository; + +/** + * @author Greg Turnquist + */ +interface ManagerRepository extends CrudRepository { + + /** + * Navigate through the JPA relationship to find a {@link Manager} based on an {@link Employee}'s {@literal id}. + * + * @param id + * @return + */ + Manager findByEmployeesId(Long id); +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerResourceAssembler.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerResourceAssembler.java new file mode 100644 index 0000000..9a79ef5 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerResourceAssembler.java @@ -0,0 +1,65 @@ +/* + * 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 ManagerResourceAssembler extends SimpleIdentifiableResourceAssembler { + + ManagerResourceAssembler() { + super(ManagerController.class); + } + + /** + * Retain default links provided by {@link SimpleIdentifiableResourceAssembler}, but add extra ones to each {@link Manager}. + * + * @param resource + */ + @Override + protected void addLinks(Resource resource) { + /** + * Retain default links. + */ + super.addLinks(resource); + + // Add custom link to find all managed employees + resource.add(linkTo(methodOn(EmployeeController.class).findEmployees(resource.getContent().getId())).withRel("employees")); + } + + /** + * Retain default links for the entire collection, but add extra custom links for the {@link Manager} collection. + * + * @param resources + */ + @Override + protected void addLinks(Resources> resources) { + + super.addLinks(resources); + + resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); + resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); + resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/RootController.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/RootController.java new file mode 100644 index 0000000..9c95267 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/RootController.java @@ -0,0 +1,44 @@ +/* + * 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.ResourceSupport; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Greg Turnquist + */ +@RestController +class RootController { + + @GetMapping("/") + ResponseEntity root() { + + ResourceSupport resourceSupport = new ResourceSupport(); + + 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(ManagerController.class).findAll()).withRel("managers")); + + return ResponseEntity.ok(resourceSupport); + } + +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/SpringHateoasHypermediaApplication.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/SpringHateoasHypermediaApplication.java new file mode 100644 index 0000000..5baa78a --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/SpringHateoasHypermediaApplication.java @@ -0,0 +1,30 @@ +/* + * 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 org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Greg Turnquist + */ +@SpringBootApplication +public class SpringHateoasHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringHateoasHypermediaApplication.class, args); + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java new file mode 100644 index 0000000..2c4b294 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java @@ -0,0 +1,52 @@ +/* + * 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 java.util.List; +import java.util.stream.Collectors; + +import lombok.Value; + +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. + * + * @author Greg Turnquist + */ +@Value +@JsonPropertyOrder({"id", "name", "employees"}) +class Supervisor { + + @JsonIgnore + private final Manager manager; + + public Long getId() { + return this.manager.getId(); + } + + public String getName() { + return this.manager.getName(); + } + + public List getEmployees() { + return manager.getEmployees().stream() + .map(employee -> employee.getName() + "::" + employee.getRole()) + .collect(Collectors.toList()); + } +} diff --git a/hypermedia/src/main/java/org/springframework/hateoas/examples/SupervisorController.java b/hypermedia/src/main/java/org/springframework/hateoas/examples/SupervisorController.java new file mode 100644 index 0000000..e2bdb21 --- /dev/null +++ b/hypermedia/src/main/java/org/springframework/hateoas/examples/SupervisorController.java @@ -0,0 +1,50 @@ +/* + * 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 org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.Resource; +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; + +/** + * 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 + */ +@RestController +public class SupervisorController { + + private final ManagerController controller; + + public SupervisorController(ManagerController controller) { + this.controller = controller; + } + + @GetMapping(value = "/supervisors/{id}", produces = MediaTypes.HAL_JSON_VALUE) + public ResponseEntity> findOne(@PathVariable Long id) { + + Resource managerResource = controller.findOne(id).getBody(); + Resource supervisorResource = new Resource<>( + new Supervisor(managerResource.getContent()), + managerResource.getLinks()); + + return ResponseEntity.ok(supervisorResource); + } +} diff --git a/pom.xml b/pom.xml index 0569551..97713ed 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 - Copyright 2011 the original author or authors. + 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. @@ -47,6 +47,7 @@ commons basics api-evolution + hypermedia