Skip to content

Commit 2723283

Browse files
committedFeb 12, 2018
Add 'simplified' version
1 parent d8c7f97 commit 2723283

File tree

10 files changed

+654
-1
lines changed

10 files changed

+654
-1
lines changed
 

‎README.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ We have separate folders for each of these:
1212
== Spring HATEOAS Modules
1313

1414
* link:basics[Basics] - Poke and prod at a hypermedia-powered service from inside the code as well as externally using standard tools
15+
* link:simplified[Simplified] - Use Spring HATEOAS in the simplest way possible.
1516
* link:api-evolution[API Evolution] - Upgrade an existing REST resource
1617
* link:hypermedia[Hypermedia] - Create hypermedia-driven REST resources, linking them together, and supporting older links.
1718
* link:affordances[Affordances] - Create richer hypermedia controls using more complex hypermedia formats

‎pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<parent>
4040
<groupId>org.springframework.boot</groupId>
4141
<artifactId>spring-boot-starter-parent</artifactId>
42-
<version>2.0.0.M7</version>
42+
<version>2.0.0.RC1</version>
4343
<relativePath/> <!-- lookup parent from repository -->
4444
</parent>
4545

@@ -49,6 +49,7 @@
4949
<module>api-evolution</module>
5050
<module>hypermedia</module>
5151
<module>affordances</module>
52+
<module>simplified</module>
5253
</modules>
5354

5455
<properties>

‎simplified/README.adoc

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
= Spring HATEOAS - Basic Example
2+
3+
This guides shows how to add Spring HATEOAS in the simplest way possible. Like the rest of these examples, it uses a payroll system.
4+
5+
NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code.
6+
7+
== Defining Your Domain
8+
9+
The cornerstone of any example is the domain object:
10+
11+
[source,java]
12+
----
13+
@Data
14+
@Entity
15+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
16+
@AllArgsConstructor
17+
class Employee {
18+
19+
@Id @GeneratedValue
20+
private Long id;
21+
private String firstName;
22+
private String lastName;
23+
private String role;
24+
25+
...
26+
}
27+
----
28+
29+
This domain object includes:
30+
31+
* `@Data` - Lombok annotation to define a mutable value object
32+
* `@Entity` - JPA annotation to make the object storagable in a classic SQL engine (H2 in this example)
33+
* `@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.
34+
* `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios
35+
36+
== Accessing Data
37+
38+
To experiment with something realistic, you need to access a real database. This example leverages H2, an embedded JPA datasource.
39+
And while it's not a requirement for Spring HATEOAS, this example uses Spring Data JPA.
40+
41+
Create a repository like this:
42+
43+
[source,java]
44+
----
45+
interface EmployeeRepository extends CrudRepository<Employee, Long> {
46+
}
47+
----
48+
49+
This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD)
50+
operations.
51+
52+
[[converting-entities-to-resources]]
53+
== Converting Entities to Resources
54+
55+
In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as details on _how_ to
56+
retrieve and update that information.
57+
58+
Spring HATEOAS defines a generic `Resource<T>` container that lets you store any domain object (`Employee` in this example), and
59+
add additional links.
60+
61+
IMPORTANT: Spring HATEOAS's `Resource` and `Link` classes are *vendor neutral*. HAL is thrown around a lot, being the
62+
default mediatype, but these classes can be used to render any mediatype.
63+
64+
The following Spring MVC controller defines the application's routes, and hence is the source of links needed
65+
in the hypermedia.
66+
67+
NOTE: This guide assumes you already somewhat familiar with Spring MVC.
68+
69+
[source,java]
70+
----
71+
@RestController
72+
class EmployeeController {
73+
74+
private final EmployeeRepository repository;
75+
76+
EmployeeController(EmployeeRepository repository) {
77+
this.repository = repository;
78+
}
79+
80+
...
81+
}
82+
----
83+
84+
This piece of code shows how the Spring MVC controller is wired with a copy of the `EmployeeRepository` through
85+
constructor injection and marked as a *REST controller* thanks to the `@RestController` annotation.
86+
87+
The route for the https://martinfowler.com/bliki/DDD_Aggregate.html[aggregate root] is shown below:
88+
89+
[source,java]
90+
----
91+
/**
92+
* Look up all employees, and transform them into a REST collection resource.
93+
* Then return them through Spring Web's {@link ResponseEntity} fluent API.
94+
*
95+
* NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different
96+
* default accept header, which allows XML to get requested first, so "produces"
97+
* forces it to HAL JSON for all clients.
98+
*/
99+
@GetMapping(value = "/employees", produces = MediaTypes.HAL_JSON_VALUE)
100+
ResponseEntity<Resources<Resource<Employee>>> findAll() {
101+
102+
List<Resource<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
103+
.map(employee -> new Resource<>(employee,
104+
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
105+
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
106+
.collect(Collectors.toList());
107+
108+
return ResponseEntity.ok(
109+
new Resources<>(employees,
110+
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
111+
}
112+
----
113+
114+
It retrieves a collection of `Employee` objects, streams through a Java 8 spliterator, and converts them into a collection
115+
of `Resource<Employee>` objects by using Spring HATEOAS's `linkTo` and `methodOn` helpers to build links.
116+
117+
* The natural convention with REST endpoints is to serve a *self* link (denoted by the `.withSelfRel()` call).
118+
* It's also useful for any single item resource to include a link back to the aggregate (denoted by the `.withRel("employees")`).
119+
120+
The whole collection of single item resources is then wrapped in a Spring HATEOAS `Resources` type.
121+
122+
NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's
123+
own set of links, separate from the links of each member of the collection. That's why the whole
124+
structure is `Resources<Resource<Employee>>` and not `Resources<Employee>`.
125+
126+
To build a single resource, the `/employees/{id}` route is shown below:
127+
128+
[source,java]
129+
----
130+
/**
131+
* Look up a single {@link Employee} and transform it into a REST resource. Then return it through
132+
* Spring Web's {@link ResponseEntity} fluent API.
133+
*
134+
* See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument.
135+
*
136+
* @param id
137+
*/
138+
@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE)
139+
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
140+
141+
return repository.findById(id)
142+
.map(employee -> new Resource<>(employee,
143+
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
144+
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
145+
.map(ResponseEntity::ok)
146+
.orElse(ResponseEntity.notFound().build());
147+
}
148+
----
149+
150+
This code is almost identical. It fetches a single item `Employee` from the database and that wraps up into a
151+
`Resource<Employee>` object with the same links, but that's it. No need to create a `Resources` object since is NOT a
152+
collection.
153+
154+
IMPORTANT: Does this look like duplicate code found in the aggregate root? Sures it does. That's why Spring HATEOAS
155+
includes the ability to define a `ResourceAssembler`. It lets you define, in one place, all the links for a given
156+
entity type. Then you can reuse it as needed in all relevant controller methods. It's been left out of this section
157+
for the sake of simplicity.
158+
159+
== Testing Hypermedia
160+
161+
Nothing is complete without testing. Thanks to Spring Boot, it's easier than ever to test a Spring MVC controller,
162+
including the generated hypermedia.
163+
164+
The following is a bare bones "slice" test case:
165+
166+
[source,java]
167+
----
168+
@RunWith(SpringRunner.class)
169+
@WebMvcTest(EmployeeController.class)
170+
public class EmployeeControllerTests {
171+
172+
@Autowired
173+
private MockMvc mvc;
174+
175+
@MockBean
176+
private EmployeeRepository repository;
177+
178+
...
179+
}
180+
----
181+
182+
* `@RunWith(SpringRunner.class)` is needed to leverage Spring Boot's test annotations with JUnit.
183+
* `@WebMvcTest(EmployeeController.class)` confines Spring Boot to only autoconfiguring Spring MVC components, and _only_
184+
this one controller, making it a very precise test case.
185+
* `@Autowired MockMvc` gives us a handle on a Spring Mock tester.
186+
* `@MockBean` flags `EmployeeRepository` as a test collaborator, since we don't plan on talking to a real database in this test case.
187+
188+
With this structure, we can start crafting a test case!
189+
190+
[source,java]
191+
----
192+
@Test
193+
public void getShouldFetchAHalDocument() throws Exception {
194+
195+
given(repository.findAll()).willReturn(
196+
Arrays.asList(
197+
new Employee(1L,"Frodo", "Baggins", "ring bearer"),
198+
new Employee(2L,"Bilbo", "Baggins", "burglar")));
199+
200+
mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE))
201+
.andDo(print())
202+
.andExpect(status().isOk())
203+
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE))
204+
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
205+
...
206+
}
207+
----
208+
209+
* At first, the test case uses Mockito's `given()` method to define the "given"s of the test.
210+
* Next, it uses Spring Mock MVC's `mvc` to `perform()` a *GET /employees* call with an accept header of HAL's mediatype.
211+
* As a courtesy, it uses the `.andDo(print())` to give us a complete print out of the whole thing on the console.
212+
* Finally, it chains a whole series of assertions.
213+
** Verify HTTP status is *200 OK*.
214+
** Verify the response *Content-Type* header is also HAL's mediatype (with UTF-8 flavor).
215+
** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`.
216+
** And so forth...
217+
218+
The rest of the assertions are commented out, but you can read it in the source code.
219+
220+
NOTE: This is not the only way to assert the results. See Spring Framework reference docs and Spring HATEOAS
221+
test cases for more examples.
222+
223+
For the next step in Spring HATEOAS, you may wish to read link:../api-evolution[Spring HATEOAS - API Evolution Example].

‎simplified/pom.xml

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<artifactId>spring-hateoas-examples-simplified</artifactId>
8+
<name>Spring HATEOAS - Examples - Simplified</name>
9+
<packaging>jar</packaging>
10+
11+
<parent>
12+
<groupId>org.springframework.hateoas.examples</groupId>
13+
<artifactId>spring-hateoas-examples</artifactId>
14+
<version>1.0.0.BUILD-SNAPSHOT</version>
15+
</parent>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>org.springframework.hateoas.examples</groupId>
20+
<artifactId>commons</artifactId>
21+
<version>1.0.0.BUILD-SNAPSHOT</version>
22+
</dependency>
23+
</dependencies>
24+
25+
<build>
26+
<plugins>
27+
<plugin>
28+
<groupId>org.springframework.boot</groupId>
29+
<artifactId>spring-boot-maven-plugin</artifactId>
30+
</plugin>
31+
</plugins>
32+
</build>
33+
34+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.examples;
17+
18+
import org.springframework.boot.CommandLineRunner;
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.stereotype.Component;
21+
22+
/**
23+
* Pre-load some data using a Spring Boot {@link CommandLineRunner}.
24+
*
25+
* @author Greg Turnquist
26+
*/
27+
@Component
28+
class DatabaseLoader {
29+
30+
/**
31+
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run
32+
* only after the app is operational, the database will be up.
33+
*
34+
* @param repository
35+
*/
36+
@Bean
37+
CommandLineRunner init(EmployeeRepository repository) {
38+
return args -> {
39+
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
40+
repository.save(new Employee("Bilbo", "Baggins", "burglar"));
41+
};
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.examples;
17+
18+
import lombok.AccessLevel;
19+
import lombok.AllArgsConstructor;
20+
import lombok.Data;
21+
import lombok.NoArgsConstructor;
22+
23+
import javax.persistence.Entity;
24+
import javax.persistence.GeneratedValue;
25+
import javax.persistence.Id;
26+
27+
/**
28+
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum.
29+
*
30+
* {@code @Data} - Generates getters, setters, toString, hash, and equals functions
31+
* {@code @Entity} - JPA annotation to flag this class for DB persistence
32+
* {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
33+
* {@code @AllArgsConstructor} - Create a constructor with all args to support testing
34+
*
35+
* {@code @JsonIgnoreProperties(ignoreUnknow=true)}
36+
* When converting JSON to Java, ignore any unrecognized attributes. This is critical for REST because it
37+
* encourages adding new fields in later versions that won't break. It also allows things like _links to be
38+
* ignore as well, meaning HAL documents can be fetched and later posted to the server without adjustment.
39+
*
40+
*
41+
* @author Greg Turnquist
42+
*/
43+
@Data
44+
@Entity
45+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
46+
@AllArgsConstructor
47+
class Employee {
48+
49+
@Id @GeneratedValue
50+
private Long id;
51+
private String firstName;
52+
private String lastName;
53+
private String role;
54+
55+
/**
56+
* Useful constructor when id is not yet known.
57+
*
58+
* @param firstName
59+
* @param lastName
60+
* @param role
61+
*/
62+
Employee(String firstName, String lastName, String role) {
63+
64+
this.firstName = firstName;
65+
this.lastName = lastName;
66+
this.role = role;
67+
}
68+
}

0 commit comments

Comments
 (0)