diff --git a/basics/README.adoc b/basics/README.adoc new file mode 100644 index 0000000..1d4a738 --- /dev/null +++ b/basics/README.adoc @@ -0,0 +1,279 @@ += Spring HATEOAS - Basic Example + +This guides shows a core example of using Spring HATEOAS. It illustrates how to sew hypermedia into your Spring MVC application, including test. + +Start with a very simple example, a payroll system that tracks employees. + +NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. + +== Defining Your Domain + +The cornerstone of any example is the domain object: + +[source,java] +---- +@Data +@Entity +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +class Employee implements Identifiable { + + @Id @GeneratedValue + private Long id; + + private String firstName; + + private String lastName; + + private String role; + + ... +} +---- + +This domain object includes: + +* `@Data` - Lombok annotation to define a mutable value object +* `@Entity` - JPA annotation to make the object storagable in a classic SQL engine (H2 in this example) +* `@NoArgsConstructor` - Lombok annotation to create an empty constructor call to appear Jackson. +* `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios +* `@JsonIgnoreProperties(ignoreUnknown=true)` - Jackson annotation to ignore unknown attributes when deserializing JSON. + +It also implements `Identifiable`, which comes with a single method, `getId`, used to fetch the unique identifier +for a given entity. + +NOTE: Make your REST resources robust by instructing Jackson to ignore unknown attributes. That way, additional elements, +like hypermedia (HAL *_links*, HAL-Forms *_templates*, etc.) and newer attributes will be discarded. + +This means a HAL document like this: + +---- +{ + "firstName": "Frodo", + "lastName": "Baggins", + "role" : "ring bearer", + "_links" : { + "self" : { + "href" : "/employees/1" + } + } +} +---- + +...which was served up as hypermedia, can be POST'd to create a new employee. The *_links* will simply be ignored. + +== Accessing Data + +To experiment with something realistic, you need to access a real database. This example leverages H2, an embedded JPA datasource. +And while it's not a requirement for Spring HATEOAS, this example uses Spring Data JPA. + +Create a repository like this: + +[source,java] +---- +interface EmployeeRepository extends CrudRepository { +} +---- + +This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD) +operations. + +== Converting Entities to Resources + +In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as information on _how_ to +retrieve and update that information. + +Spring HATEOAS defines a generic `Resource` container that lets store any domain object (`Employee` in this example), and +add additional links. + +IMPORTANT: Spring HATEOAS's `Resource` and `Link` classes are *vendor neutral*. HAL is thrown around a lot, being the +default mediatype, but the classes you are using can be used to render any mediatype. + +A common convention is to create a factory used to convert `Employee` objects into `Resource` objects. Spring +HATEOAS provides `SimpleIdentifiableResourceAssembler` as the simplest mechanism to perform these conversions. + +[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 class has two key properties: + +* The generic type (`Employee`) declares the entity type. +* The constructor defines the Spring MVC controller (`EmployeeController`) where links are found to turn the entity into a REST resource. + +The class is flagged with Spring's `@Component` annotation, flagging it to be automatically hooked into the +application context. + + +This is half the battle. This resource assembler is used _in the controller_ to assemble REST resources, as shown in the next section. + +== Creating Links + +The following Spring MVC controller defines the application's routes, and hence is the source of links needed +in the hypermedia. + +NOTE: This guide assumes you already somewhat familiar with Spring MVC. + +[source,java] +---- +@RestController +class EmployeeController { + + private final EmployeeRepository repository; + private final EmployeeResourceAssembler assembler; + + EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) { + + this.repository = repository; + this.assembler = assembler; + } + + ... + +} +---- + +This piece of code shows how the Spring MVC controller is wired with a copy of the `EmployeeRepository` as well as a +`EmployeeResourceAssembler` and marked as a *REST controller* thanks to the `@RestController` annotation. + +To support `SimpleIdentifiableResourceAssembler`, the controller needs two things: + +* A route to the collection. By default, it assumes a pluralized, lowercased name (`Employee` -> `/employees`). +* A route to a single entity. By default it assumes the collection's URI + `/{id}`. + +The collection's route is shown below: + +[source,java] +---- + /** + * 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())); + + } +---- + +It uses the `EmployeeResourceAssembler` and it's `toResources(Employee)` method to turn a collection of +`Employee` objects into a `Resources>`. + +NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's +own collection of links, separate from the links of every member of the collection. That's why the whole +structure is `Resources>` and not `Resources`. + +To build a single resource, the `/employess/{id}` route is shown below: + +[source,java] +---- +/** + * 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))); +} +---- + +Again, the `EmployeeResourceAssembler` is used to convert a single `Employee` into a `Resource` +through its `toResource(Employee)` method. + +== Customizing the Output + +What's not shown in this example is that the `EmployeeResourceAssembler` comes with overrides. + +* `setBasePath(/* base */)` would inject a prefix into every link built in the hypermedia. +* `addLinks(Resource)` and `addLinks(Resources)` allows you to override/augment the default links assigned to every resource. +* `getCollectionLinkBuilder()` lets you override the convention of how the whole route is built up. + +== Testing Hypermedia + +Nothing is complete without testing. Thanks to Spring Boot, it's easier than ever to test a Spring MVC controller, +including the generated hypermedia. + +The following is a bare bones "slice" test case: + +[source,java] +---- +@RunWith(SpringRunner.class) +@WebMvcTest(EmployeeController.class) +@Import({EmployeeResourceAssembler.class}) +public class EmployeeControllerTests { + + @Autowired + private MockMvc mvc; + + @MockBean + private EmployeeRepository repository; + + ... +} +---- + +* `@RunWith(SpringRunner.class)` is needed to leverage Spring Boot's test annotations with JUnit. +* `@WebMvcTest(EmployeeController.class)` confines Spring Boot to only autoconfiguring Spring MVC components, and _only_ +this one controller, making it a very precise test case. +* `@Import({EmployeeResourceAssembler.class})` pulls in one extra Spring component that would be ignored by `@WebMvcTest`. +* `@Autowired MockMvc` gives us a handle on a Spring Mock tester. +* `@MockBean` flags `EmployeeRepositor` as a test collaborator. + +With this structure, we can start crafting a test case! + +[source,java] +---- +@Test +public void getShouldFetchAHalDocument() throws Exception { + + given(repository.findAll()).willReturn( + Arrays.asList( + new Employee(1L,"Frodo", "Baggins", "ring bearer"), + new Employee(2L,"Bilbo", "Baggins", "burglar"))); + + mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE + ";charset=UTF-8")) + .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) + ... +} +---- + +* At first, the test case uses Mockito's `given()` method to define the "given"s of the test. +* Next, it uses Spring Mock MVC's `mvc` to `perform()` a *GET /employees* call with an accept header of HAL's mediatype. +* As a courtesy, it uses the `.andDo(print())` to give us a complete print out of the whole thing on the console. +* Finally, it chains a whole series of assertions. +** Verify HTTP status is *200 OK*. +** Verify the response *Content-Type* header is also HAL's mediatype. +** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`. + +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 diff --git a/basics/src/main/java/org/springframework/hateoas/examples/EmployeeController.java b/basics/src/main/java/org/springframework/hateoas/examples/EmployeeController.java index ba9abcb..54f7699 100644 --- a/basics/src/main/java/org/springframework/hateoas/examples/EmployeeController.java +++ b/basics/src/main/java/org/springframework/hateoas/examples/EmployeeController.java @@ -65,7 +65,7 @@ class EmployeeController { * 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)