Add 'simplified' version
This commit is contained in:
@@ -12,6 +12,7 @@ We have separate folders for each of these:
|
||||
== Spring HATEOAS Modules
|
||||
|
||||
* link:basics[Basics] - Poke and prod at a hypermedia-powered service from inside the code as well as externally using standard tools
|
||||
* link:simplified[Simplified] - Use Spring HATEOAS in the simplest way possible.
|
||||
* 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.
|
||||
* link:affordances[Affordances] - Create richer hypermedia controls using more complex hypermedia formats
|
||||
|
||||
3
pom.xml
3
pom.xml
@@ -39,7 +39,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.M7</version>
|
||||
<version>2.0.0.RC1</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<module>api-evolution</module>
|
||||
<module>hypermedia</module>
|
||||
<module>affordances</module>
|
||||
<module>simplified</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
||||
223
simplified/README.adoc
Normal file
223
simplified/README.adoc
Normal file
@@ -0,0 +1,223 @@
|
||||
= Spring HATEOAS - Basic Example
|
||||
|
||||
This guides shows how to add Spring HATEOAS in the simplest way possible. Like the rest of these examples, it uses a payroll system.
|
||||
|
||||
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(access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor
|
||||
class Employee {
|
||||
|
||||
@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(PRIVATE)` - Lombok annotation to create an empty constructor call to appease Jackson, but which is private and not usable to our app's code.
|
||||
* `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios
|
||||
|
||||
== 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<Employee, Long> {
|
||||
}
|
||||
----
|
||||
|
||||
This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD)
|
||||
operations.
|
||||
|
||||
[[converting-entities-to-resources]]
|
||||
== Converting Entities to Resources
|
||||
|
||||
In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as details on _how_ to
|
||||
retrieve and update that information.
|
||||
|
||||
Spring HATEOAS defines a generic `Resource<T>` container that lets you 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 these classes can be used to render any mediatype.
|
||||
|
||||
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;
|
||||
|
||||
EmployeeController(EmployeeRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
This piece of code shows how the Spring MVC controller is wired with a copy of the `EmployeeRepository` through
|
||||
constructor injection and marked as a *REST controller* thanks to the `@RestController` annotation.
|
||||
|
||||
The route for the https://martinfowler.com/bliki/DDD_Aggregate.html[aggregate root] is shown below:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
/**
|
||||
* Look up all employees, and transform them into a REST collection resource.
|
||||
* 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)
|
||||
ResponseEntity<Resources<Resource<Employee>>> findAll() {
|
||||
|
||||
List<Resource<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(
|
||||
new Resources<>(employees,
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
|
||||
}
|
||||
----
|
||||
|
||||
It retrieves a collection of `Employee` objects, streams through a Java 8 spliterator, and converts them into a collection
|
||||
of `Resource<Employee>` objects by using Spring HATEOAS's `linkTo` and `methodOn` helpers to build links.
|
||||
|
||||
* The natural convention with REST endpoints is to serve a *self* link (denoted by the `.withSelfRel()` call).
|
||||
* It's also useful for any single item resource to include a link back to the aggregate (denoted by the `.withRel("employees")`).
|
||||
|
||||
The whole collection of single item resources is then wrapped in a Spring HATEOAS `Resources` type.
|
||||
|
||||
NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's
|
||||
own set of links, separate from the links of each member of the collection. That's why the whole
|
||||
structure is `Resources<Resource<Employee>>` and not `Resources<Employee>`.
|
||||
|
||||
To build a single resource, the `/employees/{id}` route is shown below:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
/**
|
||||
* Look up a single {@link Employee} and transform it into a REST resource. 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)
|
||||
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
|
||||
|
||||
return repository.findById(id)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
----
|
||||
|
||||
This code is almost identical. It fetches a single item `Employee` from the database and that wraps up into a
|
||||
`Resource<Employee>` object with the same links, but that's it. No need to create a `Resources` object since is NOT a
|
||||
collection.
|
||||
|
||||
IMPORTANT: Does this look like duplicate code found in the aggregate root? Sures it does. That's why Spring HATEOAS
|
||||
includes the ability to define a `ResourceAssembler`. It lets you define, in one place, all the links for a given
|
||||
entity type. Then you can reuse it as needed in all relevant controller methods. It's been left out of this section
|
||||
for the sake of simplicity.
|
||||
|
||||
== 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)
|
||||
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.
|
||||
* `@Autowired MockMvc` gives us a handle on a Spring Mock tester.
|
||||
* `@MockBean` flags `EmployeeRepository` as a test collaborator, since we don't plan on talking to a real database in this test case.
|
||||
|
||||
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_UTF8_VALUE))
|
||||
.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 (with UTF-8 flavor).
|
||||
** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`.
|
||||
** And so forth...
|
||||
|
||||
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.
|
||||
|
||||
For the next step in Spring HATEOAS, you may wish to read link:../api-evolution[Spring HATEOAS - API Evolution Example].
|
||||
34
simplified/pom.xml
Normal file
34
simplified/pom.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-hateoas-examples-simplified</artifactId>
|
||||
<name>Spring HATEOAS - Examples - Simplified</name>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.hateoas.examples</groupId>
|
||||
<artifactId>spring-hateoas-examples</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.hateoas.examples</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -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 org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Pre-load some data using a Spring Boot {@link CommandLineRunner}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Component
|
||||
class DatabaseLoader {
|
||||
|
||||
/**
|
||||
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run
|
||||
* only after the app is operational, the database will be up.
|
||||
*
|
||||
* @param repository
|
||||
*/
|
||||
@Bean
|
||||
CommandLineRunner init(EmployeeRepository repository) {
|
||||
return args -> {
|
||||
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
|
||||
repository.save(new Employee("Bilbo", "Baggins", "burglar"));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
|
||||
/**
|
||||
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum.
|
||||
*
|
||||
* {@code @Data} - Generates getters, setters, toString, hash, and equals functions
|
||||
* {@code @Entity} - JPA annotation to flag this class for DB persistence
|
||||
* {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
|
||||
* {@code @AllArgsConstructor} - Create a constructor with all args to support testing
|
||||
*
|
||||
* {@code @JsonIgnoreProperties(ignoreUnknow=true)}
|
||||
* When converting JSON to Java, ignore any unrecognized attributes. This is critical for REST because it
|
||||
* encourages adding new fields in later versions that won't break. It also allows things like _links to be
|
||||
* ignore as well, meaning HAL documents can be fetched and later posted to the server without adjustment.
|
||||
*
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor
|
||||
class Employee {
|
||||
|
||||
@Id @GeneratedValue
|
||||
private Long id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* Useful constructor when id is not yet known.
|
||||
*
|
||||
* @param firstName
|
||||
* @param lastName
|
||||
* @param role
|
||||
*/
|
||||
Employee(String firstName, String lastName, String role) {
|
||||
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.role = role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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 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.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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Spring Web {@link RestController} used to generate a REST API.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@RestController
|
||||
class EmployeeController {
|
||||
|
||||
private final EmployeeRepository repository;
|
||||
|
||||
EmployeeController(EmployeeRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up all employees, and transform them into a REST collection resource.
|
||||
* 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)
|
||||
ResponseEntity<Resources<Resource<Employee>>> findAll() {
|
||||
|
||||
List<Resource<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(
|
||||
new Resources<>(employees,
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
|
||||
}
|
||||
|
||||
@PostMapping("/employees")
|
||||
ResponseEntity<?> newEmployee(@RequestBody Employee employee) {
|
||||
|
||||
try {
|
||||
Employee savedEmployee = repository.save(employee);
|
||||
|
||||
Resource<Employee> employeeResource = new Resource<>(savedEmployee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel());
|
||||
|
||||
return ResponseEntity
|
||||
.created(new URI(employeeResource.getRequiredLink(Link.REL_SELF).getHref()))
|
||||
.body(employeeResource);
|
||||
} catch (URISyntaxException e) {
|
||||
return ResponseEntity.badRequest().body("Unable to create " + employee);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a single {@link Employee} and transform it into a REST resource. 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)
|
||||
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
|
||||
|
||||
return repository.findById(id)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing employee then return a Location header.
|
||||
*
|
||||
* @param employee
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@PutMapping("/employees/{id}")
|
||||
ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable long id) {
|
||||
|
||||
Employee employeeToUpdate = employee;
|
||||
employeeToUpdate.setId(id);
|
||||
repository.save(employeeToUpdate);
|
||||
|
||||
Link newlyCreatedLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
|
||||
|
||||
try {
|
||||
return ResponseEntity.noContent()
|
||||
.location(new URI(newlyCreatedLink.getHref()))
|
||||
.build();
|
||||
} catch (URISyntaxException e) {
|
||||
return ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A simple Spring Data {@link CrudRepository} for storing {@link Employee}s.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
interface EmployeeRepository extends CrudRepository<Employee, Long> {
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.hateoas.core.EvoInflectorRelProvider;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class SpringHateoasSimplifiedApplication {
|
||||
|
||||
public static void main(String... args) {
|
||||
SpringApplication.run(SpringHateoasSimplifiedApplication.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format embedded collections by pluralizing the resource's type.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
EvoInflectorRelProvider relProvider() {
|
||||
return new EvoInflectorRelProvider();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.hamcrest.CoreMatchers.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
/**
|
||||
* How to test the hypermedia-based {@link EmployeeController} with everything else mocked out.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@WebMvcTest(EmployeeController.class)
|
||||
public class EmployeeControllerTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@MockBean
|
||||
private EmployeeRepository repository;
|
||||
|
||||
@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_UTF8_VALUE))
|
||||
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
|
||||
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
|
||||
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
|
||||
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
|
||||
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
|
||||
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
|
||||
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
|
||||
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
|
||||
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
|
||||
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
|
||||
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
|
||||
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
|
||||
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees")))
|
||||
.andReturn();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user