Today we will look at building a web app using Sprint Boot and React.js. We will look at a number of different tutorials out there to see if we can get a simple view of how we can do this, as there are a dazzling array of technologies and libraries and frameworks out there at the mo. Without good examples and tutorials to work with it can be difficult to make up your mind which ones to choose.
The way im going to do this of course is by looking at the existing tutorials that are out there, and use those as a guide to them come up with my own tutorial. Im going to do all this in one article so that we dont have to jump around. Lets have a look at the key technologies we have mentioned first so that we know what we are talking about.
Why Spring Boot and React.js?
Combining Spring Boot and React.js should allow us to combine the robustness of Java and Spring on the backend, with the component driven client model on the front end, and allows to carve out well designed UI apps. But throughout this tutorial we will see how easy this is to do (or not!).

What is Spring Boot?
Spring Boot is part of the Spring Framework. The Spring Framework is a set of java libraries designed to support the development of Java applications by implementing and simplifying many well known design patterns, with the underlying goal of making Java development and support easier.
Spring boot extends that idea by making a certain number of assumptions about your application, and using those to enable automatic configuration to get your application up and running without endless configuration. It also includes and embedded web server so you can get applications that need an application server running out of the box almost immediately.
What Is React.js?
React.js also known simply as React, is a client user interface JavaScript library. It is used for building user interfaces and takes a component driven approach to building those UIs. The main benefits that are given for React is that it performs well and is relatively easy to learn.
React.js and Spring Data REST – Spring.io
Over at Spring.io there is a tutorial that looks at building combining Spring Boot and React.js to build a web app. Find that tutorial here.

If you dont know what Spring Data REST is, its part of Spring that exposes hypermedia driven HTTP resources. For the purposes of what we are doing, we dont really care about it too much, the tutorial uses Spring Boot and React.js, which are the bits we are focusing on for now.
Setting Up The Eclipse Maven Project
To get started, the tutorial tells us to setup start.spring.io/ with Rest Repositories, Thymeleaf, JPA and H2 as dependencies so lets do that bit first.
Lets fill in the following values at start.spring.io


Click to ‘Generate A Project’ and the project config should get downloaded.

Im using Eclipse, and like to use the eclipse maven plugin, so I unpacked the zip file and copied the resources from there into a new Eclipse java project of the same name, springbootreact1.

We get a default Spring Boot application in com.colwil.springbootreact1
package com.colwil.springbootreact1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Springbootreact1Application {
public static void main(String[] args) {
SpringApplication.run(Springbootreact1Application.class, args);
}
}
I then run the eclipse maven plugin, ‘eclipse:clean eclipse:eclipse -DdownloadSources -DdownloadJavadocs’ and end up with the following:

Setting up the Demo Objects
We need the domain object, repository, and some sample data as per the tutorial. So lets get those bits out of the way first.
First we create an Employee object
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Entity
public class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;
private Employee() {
}
public Employee(String firstName, String lastName, String description) {
this.firstName = firstName;
this.lastName = lastName;
this.description = description;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id) && Objects.equals(firstName, employee.firstName)
&& Objects.equals(lastName, employee.lastName) && Objects.equals(description, employee.description);
}
@Override
public int hashCode() {
return Objects.hash(id, firstName, lastName, description);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "Employee{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\''
+ ", description='" + description + '\'' + '}';
}
}
// end::code[]
Then we create the EmployeeRepository. This extends Spring Data RESTs CrudRepository to give us CRUD methods to use out of the box.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.data.repository.CrudRepository;
/**
* @author Greg Turnquist
*/
// tag::code[]
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
// end::code[]
Then we create a DatabaseLoader class to give us some sample data. The annotations mean that this automatically gets run and the constructor is annotated so that it has access to the EmployeeRepository that we created.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Component
public class DatabaseLoader implements CommandLineRunner {
private final EmployeeRepository repository;
@Autowired
public DatabaseLoader(EmployeeRepository repository) {
this.repository = repository;
}
@Override
public void run(String... strings) throws Exception {
this.repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
}
}
Spring Data will generate the required database statements based on the methods in the repository class that was created.
REST Application Config
The only thing we need to configure at this stage is Spring Data REST. We need to adjust its default URI so it doesnt conflict with the UI we are going to build. So we add the following property to the application.properties file already in the src/main/resources folder.
spring.data.rest.base-path=/api
For some reason the maven eclipse plugin excludes application.properties from the build path so had to modify the build path for that change to go through.
On further investigation it turns out to be related to a default in the pom.xml. So we need to switch off filtering for the src/main/resources directory as below… otherwise exclusions are added to the build path.
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
Launching The REST API
To run the application we run the main method of our application that was built for us by the Spring Initializer.
package com.colwil.springbootreact1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Springbootreact1Application {
public static void main(String[] args) {
SpringApplication.run(Springbootreact1Application.class, args);
}
}
Right click in eclipse and Run as.. java application gives the following
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.8.RELEASE)
2019-09-15 16:58:46.643 INFO 21700 --- [ main] c.c.s.Springbootreact1Application : Starting Springbootreact1Application on with PID 21700 (\springbootreact1\target\classes started by in \springbootreact1)
2019-09-15 16:58:46.656 INFO 21700 --- [ main] c.c.s.Springbootreact1Application : No active profile set, falling back to default profiles: default
2019-09-15 16:58:50.181 INFO 21700 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2019-09-15 16:58:50.330 INFO 21700 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 101ms. Found 0 repository interfaces.
2019-09-15 16:58:51.642 INFO 21700 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$71a96361] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-09-15 16:58:51.722 INFO 21700 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.hateoas.config.HateoasConfiguration' of type [org.springframework.hateoas.config.HateoasConfiguration$$EnhancerBySpringCGLIB$$f129b093] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-09-15 16:58:53.545 INFO 21700 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-09-15 16:58:53.640 INFO 21700 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-09-15 16:58:53.640 INFO 21700 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.24]
2019-09-15 16:58:54.558 INFO 21700 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-09-15 16:58:54.559 INFO 21700 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 7720 ms
2019-09-15 16:58:56.417 INFO 21700 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2019-09-15 16:58:57.579 INFO 21700 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2019-09-15 16:58:58.214 INFO 21700 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [
name: default
...]
2019-09-15 16:58:58.826 INFO 21700 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {5.3.11.Final}
2019-09-15 16:58:58.840 INFO 21700 --- [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found
2019-09-15 16:58:59.797 INFO 21700 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-09-15 16:59:00.809 INFO 21700 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2019-09-15 16:59:01.961 INFO 21700 --- [ main] o.h.t.schema.internal.SchemaCreatorImpl : HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@2e5e6fc4'
2019-09-15 16:59:01.983 INFO 21700 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-09-15 16:59:05.727 INFO 21700 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-15 16:59:06.031 WARN 21700 --- [ main] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-09-15 16:59:07.743 WARN 21700 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2019-09-15 16:59:09.620 INFO 21700 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-15 16:59:09.629 INFO 21700 --- [ main] c.c.s.Springbootreact1Application : Started Springbootreact1Application in 24.677 seconds (JVM running for 27.013)
Testing The REST Service
Testing the REST service at this stage is as simple as accessing the URL at localhost:8080/api in the browser.
{
"_links" : {
"profile" : {
"href" : "http://localhost:8080/api/profile"
}
}
}
Yehhhh we have got a JSON message back. Hmmm, but theres a problem here. We are only getting the profile links, and not getting the employees link as in the tutorial, so something somewhere has gone wrong.
The problem is that the DatabaseLoader class that implements Springs CommandLineRunner interface is not being executed. After puzzling this out, I realised its because our CommandLineRunner, Entity and Repository are not in the same package as our Spring Application class. When I setup the application via the Spring Initializer i specified ‘com.colwil’ as the package, but the code from the tutorial is using com.greglturnquist.payroll.
So what this highlights is that Spring Boot has made assumptions and applied some defaults. Naturally if you go away from those defaults Spring Boot is not going to work properly, so you need to let it know about any diversions from the defaults.
So in this case by default Spring Boot is scanning the packages that are in the hierarchy of the class annotated with @SpringBootApplication. As our classes are elsewhere, we need to tell Spring Boot where to look with the @ComponentScan(“com.greglturnquist.payroll”) for the CommandLineRunner, @EnableJpaRepositories
for the repository and the @EntityScan
for the entity. So we change our Springbootreact1Application as below, specifying the packages where the CommandLineRunner, Entity and Repository .
package com.colwil.springbootreact1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@ComponentScan("com.greglturnquist.payroll")
@EnableJpaRepositories(basePackages = { "com.greglturnquist.payroll" })
@EntityScan(basePackages = { "com.greglturnquist.payroll" })
public class Springbootreact1Application {
public static void main(String[] args) {
SpringApplication.run(Springbootreact1Application.class, args);
}
}
If we rerun our Spring Boot app and access the url again.
{
"_links" : {
"employees" : {
"href" : "http://localhost:8080/api/employees"
},
"profile" : {
"href" : "http://localhost:8080/api/profile"
}
}
}
This time we get links for both employees and profile. Much better! If we access the link that we have been given for the employees at http://localhost:8080/api/employees, we can actually see the Employee data that has been setup by the DatabaseLoader.
{
"_embedded" : {
"employees" : [ {
"firstName" : "Frodo",
"lastName" : "Baggins",
"description" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/1"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/1"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees"
},
"profile" : {
"href" : "http://localhost:8080/api/profile/employees"
}
}
}
Setting Up The Spring MVC Controller For The React UI
At this point we need to create a home for the React components to live in. We do this by creating a Spring MVC controller and defining a Thymeleaf HTML template.
The Spring MVC Controller
@Controller
public class HomeController {
@RequestMapping(value = "/")
public String index() {
return "index";
}
}
As mentioned in the tutorial, the @Controller identifies this as a Spring MVC controller, the request mapping maps slash to the index method and index is the prefix of the html page to use.
The index.html Thymeleaf template
The index.html template needs to live in src/main/resources/templates folder.
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head lang="en">
<meta charset="UTF-8"/>
<title>ReactJS + Spring Data REST</title>
<link rel="stylesheet" href="/main.css" />
</head>
<body>
<div id="react"></div>
<script src="built/bundle.js"></script>
</body>
</html>
The div with id=”react” and the bundle.js have significance and those will get investigate those once we have built and setup the front end dependencies.
Installing Node.js and React.js Using The Pom.xml
As per the tutorial, we need to use the frontend-maven-plugin to enable us to
- Install node.js and npm package manager
- Install the node.js modules configured in package.json
- run webpack to compile all the JavaScript components into a single file
So lets add the plugin config for frontend-maven-plugin to our pom, and run the build.
So the build failed because of 1 error and 1 warning
[WARNING] npm WARN saveError ENOENT: no such file or directory, open '\springbootreact1\package.json'
So npm install failed as we havent created our package.json file yet
[ERROR] Error: Cannot find module '\springbootreact1\target\node_modules\webpack\bin\webpack.js'
and webpack failed as we havent created our config for webpack, so lets create those
package.json
{
"name": "spring-data-rest-and-reactjs",
"version": "0.1.0",
"description": "Demo of ReactJS + Spring Data REST",
"repository": {
"type": "git",
"url": "git@github.com:spring-guides/tut-react-and-spring-data-rest.git"
},
"keywords": [
"rest",
"hateoas",
"spring",
"data",
"react"
],
"author": "Greg L. Turnquist",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues"
},
"homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest",
"dependencies": {
"react": "^16.5.2",
"react-dom": "^16.5.2",
"rest": "^1.3.1"
},
"scripts": {
"watch": "webpack --watch -d"
},
"devDependencies": {
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.2",
"webpack": "^4.19.1",
"webpack-cli": "^3.1.0"
}
}
webpack.config.js
var path = require('path');
module.exports = {
entry: './src/main/js/app.js',
devtool: 'sourcemaps',
cache: true,
mode: 'development',
output: {
path: __dirname,
filename: './src/main/resources/static/built/bundle.js'
},
module: {
rules: [
{
test: path.join(__dirname, '.'),
exclude: /(node_modules)/,
use: [{
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}]
}
]
}
};
Now lets rerun the maven build.
ok so this time our npm install was successful
[INFO] --- frontend-maven-plugin:1.6:npm (npm install) @ springbootreact1 ---
[INFO] Running 'npm install' in \springbootreact1
[WARNING] npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
[WARNING] npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
[ERROR]
[INFO] added 514 packages from 248 contributors and audited 6747 packages in 183.042s
[INFO] found 0 vulnerabilities
but the webpack build failed
[INFO] --- frontend-maven-plugin:1.6:webpack (webpack build) @ springbootreact1 ---
[INFO] Running 'webpack.js ' in \springbootreact1
[ERROR]
[ERROR] Insufficient number of arguments or no entry found.
[ERROR] Alternatively, run 'webpack(-cli) --help' for usage info.
[ERROR]
[INFO] Hash: 017afc956deeaf338f52
[INFO] Version: webpack 4.40.2
[INFO] Time: 411ms
[INFO] Built at: 09/17/2019 6:09:21 PM
[INFO]
[INFO] ERROR in Entry module not found: Error: Can't resolve './src/main/js/app.js' in 'C:\dev\src\neonwrk\springbootreact1'
its because we havent created the /src/main/js/app.js that defines the modules. So lets add that as per the tutorial.
const React = require('react');
const ReactDOM = require('react-dom');
const client = require('./client');
and run the build again.
So the webpack build failed again with a different error.
[INFO] --- frontend-maven-plugin:1.6:webpack (webpack build) @ springbootreact1 ---
[INFO] Running 'webpack.js ' in \springbootreact1
[INFO] Hash: 60511e1c30ef6d8e15ce
[INFO] Version: webpack 4.40.2
[INFO] Time: 13913ms
[INFO] Built at: 09/17/2019 6:37:24 PM
[INFO] Asset Size Chunks Chunk Names
[INFO] ./src/main/resources/static/built/bundle.js 1.01 MiB main [emitted] main
[INFO] ./src/main/resources/static/built/bundle.js.map 1.17 MiB main [emitted] [dev] main
[INFO] Entrypoint main = ./src/main/resources/static/built/bundle.js ./src/main/resources/static/built/bundle.js.map
[INFO] [./src/main/js/app.js] 102 bytes {main} [built]
[INFO] + 11 hidden modules
[INFO]
[INFO] ERROR in ./src/main/js/app.js
[INFO] Module not found: Error: Can't resolve './client' in '\springbootreact1\src\main\js'
[INFO] @ ./src/main/js/app.js 5:13-32
we also need to add the src/main/js/client.js which is one of the modules needed for HAL and URI Templates.
'use strict';
const rest = require('rest');
const defaultRequest = require('rest/interceptor/defaultRequest');
const mime = require('rest/interceptor/mime');
const uriTemplateInterceptor = require('./api/uriTemplateInterceptor');
const errorCode = require('rest/interceptor/errorCode');
const baseRegistry = require('rest/mime/registry');
const registry = baseRegistry.child();
registry.register('text/uri-list', require('./api/uriListConverter'));
registry.register('application/hal+json', require('rest/mime/type/application/hal'));
module.exports = rest
.wrap(mime, { registry: registry })
.wrap(uriTemplateInterceptor)
.wrap(errorCode)
.wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }});
and rerun the build, which fails again
[INFO] --- frontend-maven-plugin:1.6:webpack (webpack build) @ springbootreact1 ---
[INFO] Running 'webpack.js ' in \springbootreact1
[INFO] Hash: 4870230e01f2d1cbd4df
[INFO] Version: webpack 4.40.2
[INFO] Time: 12055ms
[INFO] Built at: 09/17/2019 7:58:21 PM
[INFO] Asset Size Chunks Chunk Names
[INFO] ./src/main/resources/static/built/bundle.js 1.16 MiB main [emitted] main
[INFO] ./src/main/resources/static/built/bundle.js.map 1.34 MiB main [emitted] [dev] main
[INFO] Entrypoint main = ./src/main/resources/static/built/bundle.js ./src/main/resources/static/built/bundle.js.map
[INFO] [0] vertx (ignored) 15 bytes {main} [built]
[INFO] [./node_modules/webpack/buildin/amd-define.js] (webpack)/buildin/amd-define.js 85 bytes {main} [built]
[INFO] [./src/main/js/app.js] 102 bytes {main} [built]
[INFO] [./src/main/js/client.js] 712 bytes {main} [built]
[INFO] + 55 hidden modules
[INFO]
[INFO] ERROR in ./src/main/js/client.js
[INFO] Module not found: Error: Can't resolve './api/uriListConverter' in '\springbootreact1\src\main\js'
[INFO] @ ./src/main/js/client.js 16:35-68
[INFO] @ ./src/main/js/app.js
[INFO]
[INFO] ERROR in ./src/main/js/client.js
[INFO] Module not found: Error: Can't resolve './api/uriTemplateInterceptor' in '\springbootreact1\src\main\js'
[INFO] @ ./src/main/js/client.js 9:29-68
[INFO] @ ./src/main/js/app.js
looks like theres some more modules missing. Looking at the tutorial these arent mentioned, but look at the github for the tutorial, /api/uriTemplateInterceptor.js and /api/uriListConverter.js are there. So lets grab those too and rebuild the project.

Success!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 03:27 min
[INFO] Finished at: 2019-09-17T20:08:05+01:00
[INFO] ------------------------------------------------------------------------
Building the React.js UI
So now we are ready to play with React more. As per the tutorial we want to add in a top level component.
In the App component, an array of employees is fetched from the Spring Data REST backend and stored in this component’s state data.
https://spring.io/guides/tutorials/react-and-spring-data-rest/
also in app.js
// tag::app[]
class App extends React.Component {
constructor(props) {
super(props);
this.state = {employees: []};
}
componentDidMount() {
client({method: 'GET', path: '/api/employees'}).done(response => {
this.setState({employees: response.entity._embedded.employees});
});
}
render() {
return (
<EmployeeList employees={this.state.employees}/>
)
}
}
// end::app[]
https://spring.io/guides/tutorials/react-and-spring-data-rest/
class Foo extends React.Component{…}
is the method to create a React component.componentDidMount
is the API invoked after React renders a component in the DOM.render
is the API to “draw” the component on the screen.
So what we see here is that theres a class with a constructor to setup the React component…
class App extends React.Component {
constructor(props) {
super(props);
this.state = {employees: []};
}
then theres a componentDidMount() method. Once the component is added to the DOM, the componentDidMount() method will be called.
In this case its using the Node module, rest.js, (with additional functionality added round it for processing REST Data), and calling the GET method in our Spring MVC REST to get the employees, then calling this.setState to set employees to the employees in the response message.
componentDidMount() {
client({method: 'GET', path: '/api/employees'}).done(response => {
this.setState({employees: response.entity._embedded.employees});
});
}
Heres the description of the Node rest.js module
rest.js
Just enough client, as you need it. Make HTTP requests from a browser or Node.js applying only the client features you need. Configure a client once, and share it safely throughout your application. Easily extend with interceptors that wrap the request and/or response, or MIME type converters for rich data formats.
https://www.npmjs.com/package/rest
Calling this.setState schedules a UI update, which will trigger React to check if the state has changed and call the render() method again.
render() {
return (
<EmployeeList employees={this.state.employees}/>
)
}
So you initialise your attributes in the constructor, and check for changes in componentDidMount. Keep in mind that in React, state represents data that can change, but properties represent fixed values that wont change.
Adding the rest of the details to the app.js, first we just have the variables/requires
'use strict';
// tag::vars[]
const React = require('react');
const ReactDOM = require('react-dom');
const client = require('./client');
// end::vars[]
then we have the app component that grabs the Spring MVC data REST response and sticks that in an EmployeeList component
// tag::app[]
class App extends React.Component {
constructor(props) {
super(props);
this.state = {employees: []};
}
componentDidMount() {
client({method: 'GET', path: '/api/employees'}).done(response => {
this.setState({employees: response.entity._embedded.employees});
});
}
render() {
return (
<EmployeeList employees={this.state.employees}/>
)
}
}
// end::app[]
Then we have the EmployeeList component, that with HTML formatting and also creating each Employee
// tag::employee-list[]
class EmployeeList extends React.Component{
render() {
const employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee}/>
);
return (
<table>
<tbody>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
</tr>
{employees}
</tbody>
</table>
)
}
}
// end::employee-list[]
then we have the Employee component
// tag::employee[]
class Employee extends React.Component{
render() {
return (
<tr>
<td>{this.props.employee.firstName}</td>
<td>{this.props.employee.lastName}</td>
<td>{this.props.employee.description}</td>
</tr>
)
}
}
// end::employee[]
The above sections are part of app.js. As it appears we are mixing HTML and Javascript into something called JSX. The advantage here with react is using smaller understandable components to create larger components.
The last steps is to take out components and to add them to the HTML page
ReactDOM.render(
<App />,
document.getElementById('react')
)
App is our top level component in App.js, and ‘react’ is the <DIV> element we created in our HTML page. So this applys the react components to that div element in the HTML page.
Running The Application
First step now that we have all the source code in place, is to rebuild the project in eclipse
[INFO] --- frontend-maven-plugin:1.6:install-node-and-npm (install node and npm) @ springbootreact1 ---
[INFO] Node v10.11.0 is already installed.
[INFO] NPM 6.4.1 is already installed.
[INFO]
[INFO] --- frontend-maven-plugin:1.6:npm (npm install) @ springbootreact1 ---
[INFO] Running 'npm install' in \springbootreact1
[WARNING] npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
[WARNING] npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
[ERROR]
[INFO] audited 6747 packages in 35.809s
[INFO] found 0 vulnerabilities
[INFO]
[INFO]
[INFO] --- frontend-maven-plugin:1.6:webpack (webpack build) @ springbootreact1 ---
[INFO] Running 'webpack.js ' in \springbootreact1
[INFO] Hash: 2b8cdb886f70d0b17e2c
[INFO] Version: webpack 4.40.2
[INFO] Time: 23420ms
[INFO] Built at: 09/20/2019 3:01:58 PM
[INFO] Asset Size Chunks Chunk Names
[INFO] ./src/main/resources/static/built/bundle.js 1.17 MiB main [emitted] main
[INFO] ./src/main/resources/static/built/bundle.js.map 1.35 MiB main [emitted] [dev] main
[INFO] Entrypoint main = ./src/main/resources/static/built/bundle.js ./src/main/resources/static/built/bundle.js.map
[INFO] [0] vertx (ignored) 15 bytes {main} [built]
[INFO] [./node_modules/webpack/buildin/amd-define.js] (webpack)/buildin/amd-define.js 85 bytes {main} [built]
[INFO] [./src/main/js/api/uriListConverter.js] 614 bytes {main} [built]
[INFO] [./src/main/js/api/uriTemplateInterceptor.js] 497 bytes {main} [built]
[INFO] [./src/main/js/app.js] 4.83 KiB {main} [built]
[INFO] [./src/main/js/client.js] 712 bytes {main} [built]
[INFO] + 55 hidden modules
[INFO]
[INFO] <<< maven-eclipse-plugin:2.10:eclipse (default-cli) < generate-resources @ springbootreact1 <<<
[INFO]
[INFO]
[INFO] --- maven-eclipse-plugin:2.10:eclipse (default-cli) @ springbootreact1 ---
[INFO] Using Eclipse Workspace:
[INFO] Adding default classpath container: org.eclipse.jdt.launching.JRE_CONTAINER
[INFO] Resource directory's path matches an existing source directory but "test", "filtering" or "output" were different.The resulting eclipse configuration may not accurately reflect the project configuration for src/main/resources
[INFO] Not writing settings - defaults suffice
[INFO] Wrote Eclipse project for "springbootreact1" to \springbootreact1.
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 03:05 min
[INFO] Finished at: 2019-09-20T15:02:59+01:00
[INFO] ------------------------------------------------------------------------
One other thing to make sure is that Springs application.properties is in the root of the target folder. in my eclipse for some reason the project was set to ignore that, so it wasnt getting created, which would cause the Spring MVC URL not get set properly.

Now lets run Spring Boot and confirm we can see the data.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.8.RELEASE)
2019-09-20 15:11:00.661 INFO 21092 --- [ main] c.c.s.Springbootreact1Application : Starting Springbootreact1Application with PID 21092 \springbootreact1\target\classes started by in \springbootreact1)
2019-09-20 15:11:00.673 INFO 21092 --- [ main] c.c.s.Springbootreact1Application : No active profile set, falling back to default profiles: default
2019-09-20 15:11:03.919 INFO 21092 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2019-09-20 15:11:04.258 INFO 21092 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 294ms. Found 1 repository interfaces.
2019-09-20 15:11:06.513 INFO 21092 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$314f9ff] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-09-20 15:11:06.640 INFO 21092 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.hateoas.config.HateoasConfiguration' of type [org.springframework.hateoas.config.HateoasConfiguration$$EnhancerBySpringCGLIB$$82954731] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-09-20 15:11:08.504 INFO 21092 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-09-20 15:11:08.618 INFO 21092 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-09-20 15:11:08.618 INFO 21092 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.24]
2019-09-20 15:11:11.353 INFO 21092 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-09-20 15:11:11.354 INFO 21092 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 10412 ms
2019-09-20 15:11:12.830 INFO 21092 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2019-09-20 15:11:13.617 INFO 21092 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2019-09-20 15:11:14.078 INFO 21092 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [
name: default
...]
2019-09-20 15:11:14.533 INFO 21092 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {5.3.11.Final}
2019-09-20 15:11:14.543 INFO 21092 --- [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found
2019-09-20 15:11:15.295 INFO 21092 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-09-20 15:11:16.189 INFO 21092 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2019-09-20 15:11:20.035 INFO 21092 --- [ main] o.h.t.schema.internal.SchemaCreatorImpl : HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@62ea8931'
2019-09-20 15:11:20.051 INFO 21092 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-09-20 15:11:25.947 INFO 21092 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-20 15:11:26.428 WARN 21092 --- [ main] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-09-20 15:11:26.739 INFO 21092 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page template: index
2019-09-20 15:11:30.712 INFO 21092 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-20 15:11:30.717 INFO 21092 --- [ main] c.c.s.Springbootreact1Application : Started Springbootreact1Application in 32.136 seconds (JVM running for 34.403)
Testing 123
2019-09-20 15:12:05.805 INFO 21092 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-09-20 15:12:05.806 INFO 21092 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2019-09-20 15:12:06.180 INFO 21092 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 374 ms
2019-09-20 15:12:09.236 INFO 21092 --- [nio-8080-exec-4] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory

Success! We have the data as per the tutorial. Now lets continue with the rest of the tutorial.
So now we have 1 employee, lets add another one as per the tutorial, using curl.
Heres our curl command run at the command line.
curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"
and heres the response also at the command line.
{
"firstName" : "Bilbo",
"lastName" : "Baggins",
"description" : "burglar",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/2"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/2"
}
}
}
So if we refresh the app, we should see the additional employee.
Success!

What Did We Achieve So Far?
- Created 2 React Components
- Used Spring Data REST to populate those with data from a domain object and repository
In the next part of the tutorial, they hope to get us to make this app dynamic load data, use the hypermedia controls in the app, and also allow you to edit the data.
Navigating The Application Using The HyperMedia Controls
The next stage of the tutorial is around actually using the links included in the Spring REST data to provide navigation around the app.
Updating The Code To Include Paging
Currently the EmployeeRepository extends CrudRepository that provides CRUD operations (even though we havent implemented them all in the React app).
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
The description of that in the code is
Interface for generic CRUD operations on a repository for a specific type.
But for paging we need the EmployeeRepository to extend PagingAndSortingRepository
.
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
The description of that in the code shows that it extends CrudRepository plus adds additional pagination and sorting methods.
Extension of {@link CrudRepository} to provide additional methods to retrieve entities using the pagination and
sorting abstraction.
Also at this point we add some more initialisation to the DatabaseLoader so we have some more test data.
public class DatabaseLoader implements CommandLineRunner {
private final EmployeeRepository repository;
@Autowired
public DatabaseLoader(EmployeeRepository repository) {
this.repository = repository;
}
@Override
public void run(String... strings) throws Exception {
this.repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
this.repository.save(new Employee("Bilbo", "Baggins", "burglar"));
this.repository.save(new Employee("Gandalf", "the Grey", "wizard"));
this.repository.save(new Employee("Samwise", "Gamgee", "gardener"));
this.repository.save(new Employee("Meriadoc", "Brandybuck", "pony rider"));
this.repository.save(new Employee("Peregrin", "Took", "pipe smoker"));
}
If we rerun the app and refresh we can see the additional data.

We dont see the the additional links yet, as we havent updated the front end React part of the app.
Testing The Paging In The Rest API With Curl
If we run the curl command from the command line, we should see the result of the REST API now that we have added the paging. Lets run the curl command in the same way that they do in the tutorial. The results actually come out in a different order.
curl "localhost:8080/api/employees?size=2"
{
"_embedded" : {
"employees" : [ {
"firstName" : "Frodo",
"lastName" : "Baggins",
"description" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/1"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/1"
}
}
}, {
"firstName" : "Bilbo",
"lastName" : "Baggins",
"description" : "burglar",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/2"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/2"
}
}
} ]
},
"_links" : {
"first" : {
"href" : "http://localhost:8080/api/employees?page=0&size=2"
},
"self" : {
"href" : "http://localhost:8080/api/employees{&sort}",
"templated" : true
},
"next" : {
"href" : "http://localhost:8080/api/employees?page=1&size=2"
},
"last" : {
"href" : "http://localhost:8080/api/employees?page=2&size=2"
},
"profile" : {
"href" : "http://localhost:8080/api/profile/employees"
}
},
"page" : {
"size" : 2,
"totalElements" : 6,
"totalPages" : 3,
"number" : 0
}
}
Heres what we get if we navigate to the next link http://localhost:8080/api/employees?page=1&size=2
curl "http://localhost:8080/api/employees?page=1&size=2"
{
"_embedded" : {
"employees" : [ {
"firstName" : "Gandalf",
"lastName" : "the Grey",
"description" : "wizard",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/3"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/3"
}
}
}, {
"firstName" : "Samwise",
"lastName" : "Gamgee",
"description" : "gardener",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/4"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/4"
}
}
} ]
},
"_links" : {
"first" : {
"href" : "http://localhost:8080/api/employees?page=0&size=2"
},
"prev" : {
"href" : "http://localhost:8080/api/employees?page=0&size=2"
},
"self" : {
"href" : "http://localhost:8080/api/employees{&sort}",
"templated" : true
},
"next" : {
"href" : "http://localhost:8080/api/employees?page=2&size=2"
},
"last" : {
"href" : "http://localhost:8080/api/employees?page=2&size=2"
},
"profile" : {
"href" : "http://localhost:8080/api/profile/employees"
}
},
"page" : {
"size" : 2,
"totalElements" : 6,
"totalPages" : 3,
"number" : 1
}
}
We get the next page of results and updated links for paging. At this point the section is done, so we need to update the React frontend to process the additional features we have added.
Navigating the Spring Data Rest
In the tutorial they add a new javascript file to handle the following of the URLs,
module.exports = function follow(api, rootPath, relArray) {
const root = api({
method: 'GET',
path: rootPath
});
return relArray.reduce(function(root, arrayItem) {
const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel;
return traverseNext(root, rel, arrayItem);
}, root);
function traverseNext (root, rel, arrayItem) {
return root.then(function (response) {
if (hasEmbeddedRel(response.entity, rel)) {
return response.entity._embedded[rel];
}
if(!response.entity._links) {
return [];
}
if (typeof arrayItem === 'string') {
return api({
method: 'GET',
path: response.entity._links[rel].href
});
} else {
return api({
method: 'GET',
path: response.entity._links[rel].href,
params: arrayItem.params
});
}
});
}
function hasEmbeddedRel (entity, rel) {
return entity._embedded && entity._embedded.hasOwnProperty(rel);
}
};
In app.js, the componentDidMount() method changes. Rather than calling the REST api from componentDidMount(), then using this.setState(), the tutorial users a new loadFromServer() method.
Heres is the new app.js so you can see the whole. Then lets look at each of the areas that the tutorial talks about so we are clear that we understanding it.
'use strict';
const React = require('react');
const ReactDOM = require('react-dom');
const client = require('./client');
const follow = require('./follow'); // function to hop multiple links by "rel"
const root = '/api';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {employees: [], attributes: [], pageSize: 2, links: {}};
this.updatePageSize = this.updatePageSize.bind(this);
this.onCreate = this.onCreate.bind(this);
this.onDelete = this.onDelete.bind(this);
this.onNavigate = this.onNavigate.bind(this);
}
// tag::follow-2[]
loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
return employeeCollection;
});
}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: employeeCollection.entity._links});
});
}
// end::follow-2[]
// tag::create[]
onCreate(newEmployee) {
follow(client, root, ['employees']).then(employeeCollection => {
return client({
method: 'POST',
path: employeeCollection.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
}).then(response => {
return follow(client, root, [
{rel: 'employees', params: {'size': this.state.pageSize}}]);
}).done(response => {
if (typeof response.entity._links.last !== "undefined") {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
});
}
// end::create[]
// tag::delete[]
onDelete(employee) {
client({method: 'DELETE', path: employee._links.self.href}).done(response => {
this.loadFromServer(this.state.pageSize);
});
}
// end::delete[]
// tag::navigate[]
onNavigate(navUri) {
client({method: 'GET', path: navUri}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: this.state.attributes,
pageSize: this.state.pageSize,
links: employeeCollection.entity._links
});
});
}
// end::navigate[]
// tag::update-page-size[]
updatePageSize(pageSize) {
if (pageSize !== this.state.pageSize) {
this.loadFromServer(pageSize);
}
}
// end::update-page-size[]
// tag::follow-1[]
componentDidMount() {
this.loadFromServer(this.state.pageSize);
}
// end::follow-1[]
render() {
return (
<div>
<CreateDialog attributes={this.state.attributes} onCreate={this.onCreate}/>
<EmployeeList employees={this.state.employees}
links={this.state.links}
pageSize={this.state.pageSize}
onNavigate={this.onNavigate}
onDelete={this.onDelete}
updatePageSize={this.updatePageSize}/>
</div>
)
}
}
// tag::create-dialog[]
class CreateDialog extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const newEmployee = {};
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
// clear out the dialog's inputs
this.props.attributes.forEach(attribute => {
ReactDOM.findDOMNode(this.refs[attribute]).value = '';
});
// Navigate away from the dialog to hide it.
window.location = "#";
}
render() {
const inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute} ref={attribute} className="field"/>
</p>
);
return (
<div>
<a href="#createEmployee">Create</a>
<div id="createEmployee" className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Create new employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
}
}
// end::create-dialog[]
class EmployeeList extends React.Component {
constructor(props) {
super(props);
this.handleNavFirst = this.handleNavFirst.bind(this);
this.handleNavPrev = this.handleNavPrev.bind(this);
this.handleNavNext = this.handleNavNext.bind(this);
this.handleNavLast = this.handleNavLast.bind(this);
this.handleInput = this.handleInput.bind(this);
}
// tag::handle-page-size-updates[]
handleInput(e) {
e.preventDefault();
const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value;
if (/^[0-9]+$/.test(pageSize)) {
this.props.updatePageSize(pageSize);
} else {
ReactDOM.findDOMNode(this.refs.pageSize).value =
pageSize.substring(0, pageSize.length - 1);
}
}
// end::handle-page-size-updates[]
// tag::handle-nav[]
handleNavFirst(e){
e.preventDefault();
this.props.onNavigate(this.props.links.first.href);
}
handleNavPrev(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.prev.href);
}
handleNavNext(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.next.href);
}
handleNavLast(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.last.href);
}
// end::handle-nav[]
// tag::employee-list-render[]
render() {
const employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee} onDelete={this.props.onDelete}/>
);
const navLinks = [];
if ("first" in this.props.links) {
navLinks.push(<button key="first" onClick={this.handleNavFirst}><<</button>);
}
if ("prev" in this.props.links) {
navLinks.push(<button key="prev" onClick={this.handleNavPrev}><</button>);
}
if ("next" in this.props.links) {
navLinks.push(<button key="next" onClick={this.handleNavNext}>></button>);
}
if ("last" in this.props.links) {
navLinks.push(<button key="last" onClick={this.handleNavLast}>>></button>);
}
return (
<div>
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
<table>
<tbody>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
<th></th>
</tr>
{employees}
</tbody>
</table>
<div>
{navLinks}
</div>
</div>
)
}
// end::employee-list-render[]
}
// tag::employee[]
class Employee extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete() {
this.props.onDelete(this.props.employee);
}
render() {
return (
<tr>
<td>{this.props.employee.firstName}</td>
<td>{this.props.employee.lastName}</td>
<td>{this.props.employee.description}</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
}
// end::employee[]
ReactDOM.render(
<App />,
document.getElementById('react')
)
Lets look at the loadserver() method first as thats what the tutorial talks about next.
// tag::follow-2[]
loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
return employeeCollection;
});
}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: employeeCollection.entity._links});
});
}
// end::follow-2[]
The loadServer() method first calls the follow() method first, passing the client object to make the rest calls, the starting or root URI, and then the name of the relationship to look for in the returned JSON message.
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
)
So here its marked as employees, so from the JSON message we had earlier from CURL it looks like below.
"employees" : [ {
"firstName" : "Gandalf",
"lastName" : "the Grey",
"description" : "wizard",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/3"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/3"
}
}
}, {
"firstName" : "Samwise",
"lastName" : "Gamgee",
"description" : "gardener",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/4"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/4"
}
}
} ]
Then the loadFromServer() method will first get the JSON schema information
.then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
return employeeCollection;
});
})
, and then get the employee data itself
.done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: employeeCollection.entity._links});
})
The metadata is then used in the UI.
Create New Records With The CreateDialog React Component
In the updated app.js the render() method for App is as follows
render() {
return (
<div>
<CreateDialog attributes={this.state.attributes} onCreate={this.onCreate}/>
<EmployeeList employees={this.state.employees}
links={this.state.links}
pageSize={this.state.pageSize}
onNavigate={this.onNavigate}
onDelete={this.onDelete}
updatePageSize={this.updatePageSize}/>
</div>
)
}
So you can see that it has a new component, CreateDialog, followed by the EmployeeList component. This new component is where the create part of CRUD functionality is handled in the UI. The CreateDialog class looks like this
// tag::create-dialog[]
class CreateDialog extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const newEmployee = {};
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
// clear out the dialog's inputs
this.props.attributes.forEach(attribute => {
ReactDOM.findDOMNode(this.refs[attribute]).value = '';
});
// Navigate away from the dialog to hide it.
window.location = "#";
}
render() {
const inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute} ref={attribute} className="field"/>
</p>
);
return (
<div>
<a href="#createEmployee">Create</a>
<div id="createEmployee" className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Create new employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
}
}
// end::create-dialog[]
As per the tutorial lets look at what the render part of this new component does first.
if you remember earlier we loaded the schema into
attributes: Object.keys(this.schema.properties)
So the name of each field is listed in the schema and is now in the attributes property. The first steps of the render method is to create an input field for each field in the schema, which is what the below does and stores it in inputs.
const inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute} ref={attribute} className="field"/>
</p>
);
then it returns the UI details that includes a form where the Create button is processed by the handleSubmit() method.
return (
<div>
<a href="#createEmployee">Create</a>
<div id="createEmployee" className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Create new employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
So when you fill in the values on the input form and click the Create button, the handleSubmit() method will be called to process the form.
handleSubmit(e) {
e.preventDefault();
const newEmployee = {};
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
// clear out the dialog's inputs
this.props.attributes.forEach(attribute => {
ReactDOM.findDOMNode(this.refs[attribute]).value = '';
});
// Navigate away from the dialog to hide it.
window.location = "#";
}
The handleSubmit() method will
disable any default browser behavior normally perform on submitting a form by calling the preventDefault() react method
e.preventDefault();
create a new variable, newEmployee to hold the employee details
const newEmployee = {};
loop through the input fields in the submitted form to extract the values that have been entered and save them in the newEmployee variable
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
call the onCreate() method to process to new employee
this.props.onCreate(newEmployee);
loop through the input fields and set them to blank to reset them
// clear out the dialog's inputs
this.props.attributes.forEach(attribute => {
ReactDOM.findDOMNode(this.refs[attribute]).value = '';
});
Navigates to the anchor to close the window
// Navigate away from the dialog to hide it.
window.location = "#";
If we look at the component the # has a class with associated css on it to close the window.
<a href="#" title="Close" className="close">X</a>
This is also a link in the component, so if you click close or after processing submit you invoke the close class as below.
The additional css to process the component in main.css.
input.field {
width: 90%;
}
table {
border-collapse: collapse;
}
td, th {
border: 1px solid #999;
padding: 0.5rem;
text-align: left;
}
/*Classes for creating dialogs*/
.modalDialog {
position: fixed;
font-family: Arial, Helvetica, sans-serif;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
z-index: 99999;
opacity:0;
-webkit-transition: opacity 400ms ease-in;
-moz-transition: opacity 400ms ease-in;
transition: opacity 400ms ease-in;
pointer-events: none;
}
.modalDialog:target {
opacity:1;
pointer-events: auto;
}
.modalDialog > div {
width: 400px;
position: relative;
margin: 10% auto;
padding: 5px 20px 13px 20px;
border-radius: 10px;
background: #fff;
background: -moz-linear-gradient(#fff, #999);
background: -webkit-linear-gradient(#fff, #999);
background: -o-linear-gradient(#fff, #999);
}
.close {
background: #606061;
color: #FFFFFF;
line-height: 25px;
position: absolute;
right: -12px;
text-align: center;
top: -10px;
width: 24px;
text-decoration: none;
font-weight: bold;
-webkit-border-radius: 12px;
-moz-border-radius: 12px;
border-radius: 12px;
-moz-box-shadow: 1px 1px 3px #000;
-webkit-box-shadow: 1px 1px 3px #000;
box-shadow: 1px 1px 3px #000;
}
.close:hover { background: #00d9ff; }
The App.onCreate() method is called to process the new employee
// tag::create[]
onCreate(newEmployee) {
follow(client, root, ['employees']).then(employeeCollection => {
return client({
method: 'POST',
path: employeeCollection.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
}).then(response => {
return follow(client, root, [
{rel: 'employees', params: {'size': this.state.pageSize}}]);
}).done(response => {
if (typeof response.entity._links.last !== "undefined") {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
});
}
// end::create[]
lets break down what this does
First navigates to the employees URL
follow(client, root, ['employees'])
then does a POST to process the new employee
.then(employeeCollection => {
return client({
method: 'POST',
path: employeeCollection.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
})
then it reloads the employees
.then(response => {
return follow(client, root, [
{rel: 'employees', params: {'size': this.state.pageSize}}]);
})
then navigates to the last pages if its available using the onNavigate() method.
.done(response => {
if (typeof response.entity._links.last !== "undefined") {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
});
What Is A Promise In Javascript?
As mentioned in the tutorial, Javascript libraries like react rely heavily on promises. The concept of a promise is that its a way of waiting for the result of a long running task. Given that everything in Javascript executes on a single thread… if you block that basically everything in your UI app stops working.
Promises are a way of dealing with the single UI thread issue. With a promise, you get notified when the the response/value is available, or if theres an error when the error is available. Heres an example to show the usefulness of a promise.
var hello = helloWorld();
console.log(hello); // 'hello world'
The above code is synchronous, if the helloWorld() method access some other service that hangs, the UI is now blocked until that responds or fails.
If we use a promise by using the .then() function, we will get notified when the value is fulfilled, but the rest of the code is not blocked as this is asynchronous, so other code in the UI will continue while this waits for a response
var hello = helloWorld();
hello.then(function(function helloWorld() {
console.log(hello); // 'hello world'
});
Page Navigation In The React App
In the EmployeeList component, the render() method includes links for navigation, based on whether they are available in the list. The navLinks array will contain a list of the HTML buttons, and theres a <div> containing those included in the output of the EmployeeList component.
// tag::employee-list-render[]
render() {
const employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee} onDelete={this.props.onDelete}/>
);
const navLinks = [];
if ("first" in this.props.links) {
navLinks.push(<button key="first" onClick={this.handleNavFirst}><<</button>);
}
if ("prev" in this.props.links) {
navLinks.push(<button key="prev" onClick={this.handleNavPrev}><</button>);
}
if ("next" in this.props.links) {
navLinks.push(<button key="next" onClick={this.handleNavNext}>></button>);
}
if ("last" in this.props.links) {
navLinks.push(<button key="last" onClick={this.handleNavLast}>>></button>);
}
return (
<div>
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
<table>
<tbody>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
<th></th>
</tr>
{employees}
</tbody>
</table>
<div>
{navLinks}
</div>
</div>
)
}
// end::employee-list-render[]
There are methods defined to handle the button clicks also…
// tag::handle-nav[]
handleNavFirst(e){
e.preventDefault();
this.props.onNavigate(this.props.links.first.href);
}
handleNavPrev(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.prev.href);
}
handleNavNext(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.next.href);
}
handleNavLast(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.last.href);
}
// end::handle-nav[]
These handlers call preventDefault() to stop any default behaviour for the buttons, and then call the onNavigate() method, as shown below, to navigate to the requested page.
// tag::navigate[]
onNavigate(navUri) {
client({method: 'GET', path: navUri}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: this.state.attributes,
pageSize: this.state.pageSize,
links: employeeCollection.entity._links
});
});
}
// end::navigate[]
Deleting Employees From The React UI
Lets look at how the latest version of the code in the app shows how a delete of an employee is processed. If we look at the latest version of the Employee class we can see that in the render method theres a Delete button defined
// tag::employee[]
class Employee extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete() {
this.props.onDelete(this.props.employee);
}
render() {
return (
<tr>
<td>{this.props.employee.firstName}</td>
<td>{this.props.employee.lastName}</td>
<td>{this.props.employee.description}</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
}
// end::employee[]
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
This button has an onClick event that will call this.handleDelete.
You can see in the constructor that this is bound to the this.handleDelete variable. This is because in Javascript this doesnt happen by default. So if you didnt bind, when the onClick runs, this would not be available so you would probably get an error or the onClick wouldnt work. If you looks at the React documentation it explains it and also shows other ways to achieve the same thing, but this appears to be the cleanest way.
When the button is clicked, the handleDelete() method is called then.
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete() {
this.props.onDelete(this.props.employee);
}
The handleDelete() method calls onDelete() and passes it the employee. If you are wondering where onDelete came from it was passed from the EmployeeList with the employee.
// tag::employee-list-render[]
render() {
const employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee} onDelete={this.props.onDelete}/>
);
So a link to itself, an employee, and the onDelete method/function were passed, so that this is in the props when its needed.
// tag::delete[]
onDelete(employee) {
client({method: 'DELETE', path: employee._links.self.href}).done(response => {
this.loadFromServer(this.state.pageSize);
});
}
// end::delete[]
The REST interface is called with DELETE requested, and a link to the employee, and then loadFromServer is called again to reload everything, and we end up back on the first page.
Paging On The React UI
In the tutorial it does discuss briefly re paging, and states that Spring Data Rest will automatically update the navigation links that are generated. The paging on the backend will be determined by the value passed back when we call the REST api. So in the loadFromServer() method, the size is passed as a parameter to the REST api.
loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
The rest of the code related to the paging is about is in the UI app, setting the pagesize to be passed back to the REST api.
The EmployeeList has an input field for setting the pagesize
<div>
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
<table>
The handleInput() method validates the input field then, and calls the updatePageSize() method.
// tag::handle-page-size-updates[]
handleInput(e) {
e.preventDefault();
const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value;
if (/^[0-9]+$/.test(pageSize)) {
this.props.updatePageSize(pageSize);
} else {
ReactDOM.findDOMNode(this.refs.pageSize).value =
pageSize.substring(0, pageSize.length - 1);
}
}
// end::handle-page-size-updates[]
The updatePageSize() will call the loadFromServer() method to reload the page if the pagesize has changed.
// tag::update-page-size[]
updatePageSize(pageSize) {
if (pageSize !== this.state.pageSize) {
this.loadFromServer(pageSize);
}
}
// end::update-page-size[]
Running the UI With Create and Delete Options
Here we see the resulting UI from the work we have done above when we run the app. (Dont forget to refresh your browser to load the updated code)

If we change the paging the number of rows shown updates immediately to reflect what we have entered.


If we delete a row that row disappears


If we click create, we get the new employee page popup, and we can fill in the details and create a new employee.


Updating Existing Resources With Spring Data Rest
So we have just learned how to create new employees and also to delete existing employees. But for full CRUD functionality of course we need to be able to update existing data.
The next thing that this Spring.oi Data Rest tutorial goes into is the discussion around the problem of performing updates. If I have access the list of employees, but then I go off and grab a coffee, someone else may also access the employee data from their PC, and update it. So now the data that im seeing on my screen is out of date, so if I come back to update it based on the data I have already loaded I could be wiping out the changes they have already made with my stale data.
To deal with such situations, the Javax Persistence API or JPA as its usually know, has a concept of versioning. Spring Data JPA has an implementation of JPA, and as Spring Data REST includes Spring Data JPA, we get those features included.
By adding a field to the data, that can be used to check if the data is stale, it will allow the application to reject the update if the data has already been updated. So lets look at how we put that all together.
Versioning Of Resources In The Rest Interface
To implement version, we need to create a field in our Employee domain object, and tag it so it is used for versioning. So in Employee.java the following is added
private @Version @JsonIgnore Long version;
and heres the full version of Employee.java showing the version field
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Entity
public class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;
private @Version @JsonIgnore Long version;
private Employee() {}
public Employee(String firstName, String lastName, String description) {
this.firstName = firstName;
this.lastName = lastName;
this.description = description;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id) &&
Objects.equals(firstName, employee.firstName) &&
Objects.equals(lastName, employee.lastName) &&
Objects.equals(description, employee.description) &&
Objects.equals(version, employee.version);
}
@Override
public int hashCode() {
return Objects.hash(id, firstName, lastName, description, version);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", description='" + description + '\'' +
", version=" + version +
'}';
}
}
// end::code[]
Every change in the Employee data will cause the version to be updated.
Rest and ETags
From the Spring documentation
An ETag (entity tag) is an HTTP response header returned by an HTTP/1.1 compliant web server used to determine change in content at a given URL.
https://docs.spring.io/spring/docs/3.0.0.M4/reference/html/ch15s11.html
From IETF
The “ETag” header field in a response provides the current entity-tag for the selected representation, as determined at the conclusion of handling the request. An entity-tag is an opaque validator for differentiating between multiple representations of the same resource, regardless of whether those multiple representations are due to resource state changes over time, content negotiation resulting in multiple representations being valid at the same time, or both.
https://tools.ietf.org/html/rfc7232#section-2.3
Basically an ETag is a hash of the response body that can be used to check for changes in the output. In our case we can use the Etag to ensure that we dont update our domain with stale data. When we grab an individual Employee, Spring Data Rest will automagically use the version to create an ETag for us in the response header. I have highlighted individual above, as currently we are retrieve the Employee data as a collection, which doesnt generate ETag, so we need to change the logic to retrieve the individual Employees.
So we need to modify our react code to that, so heres the updated app.js from the tutorial with the changes that are required.
'use strict';
const React = require('react');
const ReactDOM = require('react-dom');
const when = require('when');
const client = require('./client');
const follow = require('./follow'); // function to hop multiple links by "rel"
const root = '/api';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {employees: [], attributes: [], pageSize: 2, links: {}};
this.updatePageSize = this.updatePageSize.bind(this);
this.onCreate = this.onCreate.bind(this);
this.onUpdate = this.onUpdate.bind(this);
this.onDelete = this.onDelete.bind(this);
this.onNavigate = this.onNavigate.bind(this);
}
// tag::follow-2[]
loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;
});
}).then(employeeCollection => {
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
}).then(employeePromises => {
return when.all(employeePromises);
}).done(employees => {
this.setState({
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: this.links
});
});
}
// end::follow-2[]
// tag::create[]
onCreate(newEmployee) {
const self = this;
follow(client, root, ['employees']).then(response => {
return client({
method: 'POST',
path: response.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
}).then(response => {
return follow(client, root, [{rel: 'employees', params: {'size': self.state.pageSize}}]);
}).done(response => {
if (typeof response.entity._links.last !== "undefined") {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
});
}
// end::create[]
// tag::update[]
onUpdate(employee, updatedEmployee) {
client({
method: 'PUT',
path: employee.entity._links.self.href,
entity: updatedEmployee,
headers: {
'Content-Type': 'application/json',
'If-Match': employee.headers.Etag
}
}).done(response => {
this.loadFromServer(this.state.pageSize);
}, response => {
if (response.status.code === 412) {
alert('DENIED: Unable to update ' +
employee.entity._links.self.href + '. Your copy is stale.');
}
});
}
// end::update[]
// tag::delete[]
onDelete(employee) {
client({method: 'DELETE', path: employee.entity._links.self.href}).done(response => {
this.loadFromServer(this.state.pageSize);
});
}
// end::delete[]
// tag::navigate[]
onNavigate(navUri) {
client({
method: 'GET',
path: navUri
}).then(employeeCollection => {
this.links = employeeCollection.entity._links;
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
}).then(employeePromises => {
return when.all(employeePromises);
}).done(employees => {
this.setState({
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: this.state.pageSize,
links: this.links
});
});
}
// end::navigate[]
// tag::update-page-size[]
updatePageSize(pageSize) {
if (pageSize !== this.state.pageSize) {
this.loadFromServer(pageSize);
}
}
// end::update-page-size[]
// tag::follow-1[]
componentDidMount() {
this.loadFromServer(this.state.pageSize);
}
// end::follow-1[]
render() {
return (
<div>
<CreateDialog attributes={this.state.attributes} onCreate={this.onCreate}/>
<EmployeeList employees={this.state.employees}
links={this.state.links}
pageSize={this.state.pageSize}
attributes={this.state.attributes}
onNavigate={this.onNavigate}
onUpdate={this.onUpdate}
onDelete={this.onDelete}
updatePageSize={this.updatePageSize}/>
</div>
)
}
}
// tag::create-dialog[]
class CreateDialog extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const newEmployee = {};
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
this.props.attributes.forEach(attribute => {
ReactDOM.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs
});
window.location = "#";
}
render() {
const inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute} ref={attribute} className="field"/>
</p>
);
return (
<div>
<a href="#createEmployee">Create</a>
<div id="createEmployee" className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Create new employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
}
}
// end::create-dialog[]
// tag::update-dialog[]
class UpdateDialog extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const updatedEmployee = {};
this.props.attributes.forEach(attribute => {
updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onUpdate(this.props.employee, updatedEmployee);
window.location = "#";
}
render() {
const inputs = this.props.attributes.map(attribute =>
<p key={this.props.employee.entity[attribute]}>
<input type="text" placeholder={attribute}
defaultValue={this.props.employee.entity[attribute]}
ref={attribute} className="field"/>
</p>
);
const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href;
return (
<div key={this.props.employee.entity._links.self.href}>
<a href={"#" + dialogId}>Update</a>
<div id={dialogId} className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Update an employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Update</button>
</form>
</div>
</div>
</div>
)
}
};
// end::update-dialog[]
class EmployeeList extends React.Component {
constructor(props) {
super(props);
this.handleNavFirst = this.handleNavFirst.bind(this);
this.handleNavPrev = this.handleNavPrev.bind(this);
this.handleNavNext = this.handleNavNext.bind(this);
this.handleNavLast = this.handleNavLast.bind(this);
this.handleInput = this.handleInput.bind(this);
}
// tag::handle-page-size-updates[]
handleInput(e) {
e.preventDefault();
const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value;
if (/^[0-9]+$/.test(pageSize)) {
this.props.updatePageSize(pageSize);
} else {
ReactDOM.findDOMNode(this.refs.pageSize).value = pageSize.substring(0, pageSize.length - 1);
}
}
// end::handle-page-size-updates[]
// tag::handle-nav[]
handleNavFirst(e){
e.preventDefault();
this.props.onNavigate(this.props.links.first.href);
}
handleNavPrev(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.prev.href);
}
handleNavNext(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.next.href);
}
handleNavLast(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.last.href);
}
// end::handle-nav[]
// tag::employee-list-render[]
render() {
const employees = this.props.employees.map(employee =>
<Employee key={employee.entity._links.self.href}
employee={employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}
onDelete={this.props.onDelete}/>
);
const navLinks = [];
if ("first" in this.props.links) {
navLinks.push(<button key="first" onClick={this.handleNavFirst}><<</button>);
}
if ("prev" in this.props.links) {
navLinks.push(<button key="prev" onClick={this.handleNavPrev}><</button>);
}
if ("next" in this.props.links) {
navLinks.push(<button key="next" onClick={this.handleNavNext}>></button>);
}
if ("last" in this.props.links) {
navLinks.push(<button key="last" onClick={this.handleNavLast}>>></button>);
}
return (
<div>
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
<table>
<tbody>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
<th></th>
<th></th>
</tr>
{employees}
</tbody>
</table>
<div>
{navLinks}
</div>
</div>
)
}
// end::employee-list-render[]
}
// tag::employee[]
class Employee extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete() {
this.props.onDelete(this.props.employee);
}
render() {
return (
<tr>
<td>{this.props.employee.entity.firstName}</td>
<td>{this.props.employee.entity.lastName}</td>
<td>{this.props.employee.entity.description}</td>
<td>
<UpdateDialog employee={this.props.employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}/>
</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
}
// end::employee[]
ReactDOM.render(
<App />,
document.getElementById('react')
)
so lets go through whats changed in here bit by bit so we can make sure we understand whats going on
The tutorial mentions that for Spring Data Rest, the embedded data set is only meant as a preview of the data. The ETags will only be generated when we fully load the individual Employee domain, objects so we need to modify the logic to do that.
when.js
The new app.js includes a require to pull in the nodejs module, Cujo.js implementation of when(). From the Cujo.js documentation.
When.js is a rock solid, battle-tested Promises/A+ and
https://github.com/cujojs/when/blob/master/README.mdwhen()
implementation, including a complete ES6 Promise shim. It’s a powerful combination of small size, high performance, debuggability, and rich features:
Resolve arrays and hashes of promises, as well as infinite promise sequences
Execute tasks in parallel or sequentially
Transform Node-style and other callback-based APIs into promise-based APIs
When.js is one of the many stand-alone components of cujoJS, the JavaScript Architectural Toolkit.
App
The loadServer function is modified to ensure the individual employee domain is loaded. So where we previously did
// tag::follow-2[]
loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
return employeeCollection;
});
}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: employeeCollection.entity._links});
});
}
// end::follow-2[]
which would
- call follow() to get the EmployeeCollection link
- call client() to get the EmployeeCollection embedded data set
- extract the scheme
- set the state
changed to
// tag::follow-2[]
loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;
});
}).then(employeeCollection => {
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
}).then(employeePromises => {
return when.all(employeePromises);
}).done(employees => {
this.setState({
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: this.links
});
});
}
// end::follow-2[]
which will
- call follow() to get the EmployeeCollection link
- call client() to get the EmployeeCollection embedded data set
- extract the schema
- extract the employee links
- creates promises that call client() for each employee to get individual employee
- uses when.all() on the promises created above to asynchronously get all the individual employees
- once promises complete, done to set the state with the employees
The onNavigate() function is modified to in a similar to use when.all() to retrieve the individual employee and set the state
// tag::navigate[]
onNavigate(navUri) {
client({
method: 'GET',
path: navUri
}).then(employeeCollection => {
this.links = employeeCollection.entity._links;
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
}).then(employeePromises => {
return when.all(employeePromises);
}).done(employees => {
this.setState({
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: this.state.pageSize,
links: this.links
});
});
}
// end::navigate[]
Updating Employees
The render() function in the Employee class has been modified to include the UpdateDialog component. This is the component used to update existing employee records.
render() {
return (
<tr>
<td>{this.props.employee.entity.firstName}</td>
<td>{this.props.employee.entity.lastName}</td>
<td>{this.props.employee.entity.description}</td>
<td>
<UpdateDialog employee={this.props.employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}/>
</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
and heres the UpdateDialog class component
// tag::update-dialog[]
class UpdateDialog extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const updatedEmployee = {};
this.props.attributes.forEach(attribute => {
updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onUpdate(this.props.employee, updatedEmployee);
window.location = "#";
}
render() {
const inputs = this.props.attributes.map(attribute =>
<p key={this.props.employee.entity[attribute]}>
<input type="text" placeholder={attribute}
defaultValue={this.props.employee.entity[attribute]}
ref={attribute} className="field"/>
</p>
);
const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href;
return (
<div key={this.props.employee.entity._links.self.href}>
<a href={"#" + dialogId}>Update</a>
<div id={dialogId} className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Update an employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Update</button>
</form>
</div>
</div>
</div>
)
}
};
// end::update-dialog[]
The UpdateDialog is very similar to the createDialog class.
- this uses the same html and css to control the visibility of the component
- the JSON schema attributes are converted into HTML input elements, and populate with data from the relevant employee.
- The URI for the employee is used as the key, anchor and popup
- the Update submit button is pointing to the handleSubmit() function to process the user input from the component.
- this triggers the top level onUpdate() function in the App class component as below.
The render in the top level app is modified to include the attributes and the onUpdate() function.
this.onUpdate = this.onUpdate.bind(this);
The top level App class also has its constructor modified to include a bind for the onUpdate() function.
render() {
return (
<div>
<CreateDialog attributes={this.state.attributes} onCreate={this.onCreate}/>
<EmployeeList employees={this.state.employees}
links={this.state.links}
pageSize={this.state.pageSize}
attributes={this.state.attributes}
onNavigate={this.onNavigate}
onUpdate={this.onUpdate}
onDelete={this.onDelete}
updatePageSize={this.updatePageSize}/>
</div>
)
}
The render on the EmployeeList is modified to modify the key to come from the Employee link and include the onUpdate() function.
render() {
const employees = this.props.employees.map(employee =>
<Employee key={employee.entity._links.self.href}
employee={employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}
onDelete={this.props.onDelete}/>
);
HTTP Conditional Requests
We use the onUpdate() function to implement a conditional HTTP request based on checking the ETag. Spring Data REST implements HTTP conditional requests, so will check the request version, versus to stored version and return success or failure depending on the result of that check.
onUpdate(employee, updatedEmployee) {
client({
method: 'PUT',
path: employee.entity._links.self.href,
entity: updatedEmployee,
headers: {
'Content-Type': 'application/json',
'If-Match': employee.headers.Etag
}
}).done(response => {
this.loadFromServer(this.state.pageSize);
}, response => {
if (response.status.code === 412) {
alert('DENIED: Unable to update ' +
employee.entity._links.self.href + '. Your copy is stale.');
}
});
}
So if the done is successful, it will call the loadFromServer() function to reload, if it fails, it checks the code returned and if its a 412 an alert will be displayed. Lets run this now and confirm it works as expected.
First lets access the application at http://localhost:8080/. Lets change it to display more records, say 6.

Edit 1 record.

Open another tab in the browser and edit the same record.

Change the record and click Update to save it.

In the other tab, change the record and try and update it.

As the ETags dont match, we get our error alert.

Dynamic UIs With Events and Spring WebSockets
This part of the tutorial goes into dynamically updating the UI when the underlying data changes. This is useful as it enables you to ensure that the UI reflects the data that is behind it.
What Is WebSockets?
WebSockets provide a persistent connection between a client and server that both parties can use to start sending data at any time.
https://blog.teamtreehouse.com/an-introduction-to-websockets
The client establishes a WebSocket connection through a process known as the WebSocket handshake. This process starts with the client sending a regular HTTP request to the server. AnUpgrade
header is included in this request that informs the server that the client wishes to establish a WebSocket connection.
In this part of the tutorial, it uses Spring Data REST and Spring WebSockets to pass updates to the UI in realtime.
Add spring-boot-starter-websocket
To The Project
Add the dependency for spring-boot-starter-websocket to the POM and rebuild
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
[INFO] Downloading from : https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-websocket/2.1.8.RELEASE/spring-boot-starter-websocket-2.1.8.RELEASE-sources.jar
[INFO] Downloading from : https://repo.maven.apache.org/maven2/org/springframework/spring-messaging/5.1.9.RELEASE/spring-messaging-5.1.9.RELEASE-sources.jar
[INFO] Downloaded from : https://repo.maven.apache.org/maven2/org/springframework/spring-messaging/5.1.9.RELEASE/spring-messaging-5.1.9.RELEASE-sources.jar (296 kB at 1.2 MB/s)
[INFO] Downloading from : https://repo.maven.apache.org/maven2/org/springframework/spring-websocket/5.1.9.RELEASE/spring-websocket-5.1.9.RELEASE-sources.jar
[INFO] Downloaded from : https://repo.maven.apache.org/maven2/org/springframework/spring-websocket/5.1.9.RELEASE/spring-websocket-5.1.9.RELEASE-sources.jar (300 kB at 2.0 MB/s)
[INFO] Downloading from : https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-websocket/2.1.8.RELEASE/spring-boot-starter-websocket-2.1.8.RELEASE-javadoc.jar
[INFO] Downloading from : https://repo.maven.apache.org/maven2/org/springframework/spring-messaging/5.1.9.RELEASE/spring-messaging-5.1.9.RELEASE-javadoc.jar
[INFO] Downloaded from : https://repo.maven.apache.org/maven2/org/springframework/spring-messaging/5.1.9.RELEASE/spring-messaging-5.1.9.RELEASE-javadoc.jar (1.2 MB at 3.3 MB/s)
[INFO] Downloading from : https://repo.maven.apache.org/maven2/org/springframework/spring-websocket/5.1.9.RELEASE/spring-websocket-5.1.9.RELEASE-javadoc.jar
[INFO] Downloaded from : https://repo.maven.apache.org/maven2/org/springframework/spring-websocket/5.1.9.RELEASE/spring-websocket-5.1.9.RELEASE-javadoc.jar (1.1 MB at 3.0 MB/s)
Overview Of Stomp
Websockets is a low level API, just above the level of TCP. So Websockets is often used with another service such as STOMP to enable the messaging.
STOMP is a simple text-oriented messaging protocol that was originally created for scripting languages such as Ruby, Python, and Perl to connect to enterprise message brokers. It is designed to address a subset of commonly used messaging patterns. STOMP can be used over any reliable 2-way streaming network protocol such as TCP and WebSocket. Although STOMP is a text-oriented protocol, the payload of messages can be either text or binary.
https://docs.spring.io/spring-framework/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html#websocket-stomp-overview
Configuring WebSockets In Spring
We add another component class to our Spring code to config Websockets.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Component
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
static final String MESSAGE_PREFIX = "/topic";
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/payroll").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker(MESSAGE_PREFIX);
registry.setApplicationDestinationPrefixes("/app");
}
}
// end::code[]
@EnableWebSocketMessageBroker
turns on WebSocket support.- WebSocketConfiguration implements WebSocketMessageBrokerConfigurer – Defines methods for configuring message handling with simple messaging protocols (e.g. STOMP) from WebSocket clients. Typically used to customize the configuration provided via @EnableWebSocketMessageBroker.
- MESSAGE_PREFIX is the prefix you will prepend to every message’s route, and are used to filter destinations
- registerStompEndpoints() – Register STOMP endpoints mapping each to a specific URL and (optionally) enabling and configuring SockJS fallback options. In this case the endpoint is /payroll.
- configureMessageBroker() – Configure message broker options for the broker used to relay messages between server and client.
Subscribing to Spring Data REST events
Now we need to create a new component class to listen for the events we want generated by Spring Data REST and send them as messages to the UI via WebSockets.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import static com.greglturnquist.payroll.WebSocketConfiguration.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.core.annotation.HandleAfterCreate;
import org.springframework.data.rest.core.annotation.HandleAfterDelete;
import org.springframework.data.rest.core.annotation.HandleAfterSave;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
import org.springframework.hateoas.EntityLinks;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Component
@RepositoryEventHandler(Employee.class)
public class EventHandler {
private final SimpMessagingTemplate websocket;
private final EntityLinks entityLinks;
@Autowired
public EventHandler(SimpMessagingTemplate websocket, EntityLinks entityLinks) {
this.websocket = websocket;
this.entityLinks = entityLinks;
}
@HandleAfterCreate
public void newEmployee(Employee employee) {
this.websocket.convertAndSend(
MESSAGE_PREFIX + "/newEmployee", getPath(employee));
}
@HandleAfterDelete
public void deleteEmployee(Employee employee) {
this.websocket.convertAndSend(
MESSAGE_PREFIX + "/deleteEmployee", getPath(employee));
}
@HandleAfterSave
public void updateEmployee(Employee employee) {
this.websocket.convertAndSend(
MESSAGE_PREFIX + "/updateEmployee", getPath(employee));
}
/**
* Take an {@link Employee} and get the URI using Spring Data REST's {@link EntityLinks}.
*
* @param employee
*/
private String getPath(Employee employee) {
return this.entityLinks.linkForSingleResource(employee.getClass(),
employee.getId()).toUri().getPath();
}
}
// end::code[]
@RepositoryEventHandler(Employee.class)
– indicates that this class is an event handler for the Employee.class- The EventHandler constructor is annotated with @Autowired to get access to
SimpMessagingTemplate
andEntityLinks
instances from the application context. - Methods are create with the Employee class to indicate those are domain types which you want to listen for events for, and the annotation indicates when eg
@HandleAfterCreate
,@HandleAfterDelete
and@HandleAfterSave
. - this.websocket.convertAndSend() is used in each of the handler methods to send the message to all the clients via the webSocket
- The destination in this case, eg
MESSAGE_PREFIX + "/newEmployee"
, is different for each handler as this allows us to use one webSocket for everything. getPath()
uses Spring Data REST’sEntityLinks
to retrieve the path to send to the client.
Configuring A Client WebSocket Listener To Listen For Server WebSocket Events
No we need to add the logic in the UI to listen for the events being sent from the backend. First we create a module to that allows the UI to register routes and callbacks, so that the UI can receive the messages as they arrive. See websocket-listener.js below.

'use strict';
const SockJS = require('sockjs-client'); // <1>
require('stompjs'); // <2>
function register(registrations) {
const socket = SockJS('/payroll'); // <3>
const stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
registrations.forEach(function (registration) { // <4>
stompClient.subscribe(registration.route, registration.callback);
});
});
}
module.exports.register = register;
- Load sockjs-client which is the websocket like client. “SockJS tries to use native WebSockets first. If that fails it can use a variety of browser-specific transport protocols and presents them through WebSocket-like abstractions. ” – https://github.com/sockjs/sockjs-client
- Load stompjs – STOMP client for Web browsers.
- Point SockJS to our backend Spring Data REST Stomp/Websocket endpoint, /payroll.
- Subscribe each of the provided registrations to receive the events, specifying the callback to be called.
We also need to add the additonal dependencies for sockjs-client and stompjs to package.json
"dependencies": {
"react": "^16.5.2",
"react-dom": "^16.5.2",
"rest": "^1.3.1",
"sockjs-client": "^1.0.3",
"stompjs": "^2.3.3"
},
and add an alias for stompjs in webpack.config.js
cache: true,
mode: 'development',
resolve: {
alias: {
'stompjs': __dirname + '/node_modules' + '/stompjs/lib/stomp.js',
}
},
output: {
path: __dirname,
filename: './src/main/resources/static/built/bundle.js'
},
Registering And Reacting to WebSocket Events And Updating The UI State
In the react code in App.js, we need to include a require to pull in out websocket-listener that was created.
const stompClient = require('./websocket-listener');
As per the tutorial, it states that the right time to register for the WebSocket events is after the component is fully loaded in the DOM. The function componentDidMount() is called at this point.
https://reactjs.org/docs/react-component.html#componentdidmount
componentDidMount()
is invoked immediately after a component is mounted (inserted into the tree). Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
So the logic for registering the subscription is added to the componentDidMount() in the App class component.
// tag::register-handlers[]
componentDidMount() {
this.loadFromServer(this.state.pageSize);
stompClient.register([
{route: '/topic/newEmployee', callback: this.refreshAndGoToLastPage},
{route: '/topic/updateEmployee', callback: this.refreshCurrentPage},
{route: '/topic/deleteEmployee', callback: this.refreshCurrentPage}
]);
}
// end::register-handlers[]
So when a new employee is added, a /topic/newEmployee event will be received, then the refreshAndGoToLastPage() function will be called, which will call onNavigate to go to the last page and also update the state.
// tag::websocket-handlers[]
refreshAndGoToLastPage(message) {
follow(client, root, [{
rel: 'employees',
params: {size: this.state.pageSize}
}]).done(response => {
if (response.entity._links.last !== undefined) {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
})
}
When an employee is update or deleted, a /topic/updateEmployee or a /topic/deleteEmployee event will be received, then the refreshCurrentPage() function will be called, which will refresh the current page and update the state.
refreshCurrentPage(message) {
follow(client, root, [{
rel: 'employees',
params: {
size: this.state.pageSize,
page: this.state.page.number
}
}]).then(employeeCollection => {
this.links = employeeCollection.entity._links;
this.page = employeeCollection.entity.page;
return employeeCollection.entity._embedded.employees.map(employee => {
return client({
method: 'GET',
path: employee._links.self.href
})
});
}).then(employeePromises => {
return when.all(employeePromises);
}).then(employees => {
this.setState({
page: this.page,
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: this.state.pageSize,
links: this.links
});
});
}
// end::websocket-handlers[]
Now that the state is getting updated on any change, theres no need for us to update the state in multiple places when the state changes. So for onCreate() the state will be updated when refreshAndGoToLastPage() and onNavigate() is called.
So onCreate() changes from
// tag::create[]
onCreate(newEmployee) {
const self = this;
follow(client, root, ['employees']).then(response => {
return client({
method: 'POST',
path: response.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
}).then(response => {
return follow(client, root, [{rel: 'employees', params: {'size': self.state.pageSize}}]);
}).done(response => {
if (typeof response.entity._links.last !== "undefined") {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
});
}
// end::create[]
to
// tag::on-create[]
onCreate(newEmployee) {
follow(client, root, ['employees']).done(response => {
client({
method: 'POST',
path: response.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
})
}
// end::on-create[]
For onUpdate() and for onDelete() the state will be updated when refreshCurrentPage() is called.
so the done() in onUpdate() changes from
done(response => {
this.loadFromServer(this.state.pageSize);
}, response => {
if (response.status.code === 412) {
alert('DENIED: Unable to update ' +
employee.entity._links.self.href + '. Your copy is stale.');
}
});
to
done(response => {
/* Let the websocket handler update the state */
}, response => {
if (response.status.code === 412) {
alert('DENIED: Unable to update ' +
employee.entity._links.self.href + '. Your copy is stale.');
}
})
so onDelete() changes from
onDelete(employee) {
client({method: 'DELETE', path: employee.entity._links.self.href}).done(response => {
this.loadFromServer(this.state.pageSize);
});
}
to
onDelete(employee) {
client({method: 'DELETE', path: employee.entity._links.self.href});
}
So basically we dont need to call loadFromServer() as the page will get refreshed and state updated in refreshCurrentPage().
Lets Try It
If w run the application again and open the page in 2 brower tabs


Then we change the text for one employee in the first one.


And the second tab picks up the update without any intervention.

Securing the UI and the API
The next part of the Spring tutorial covers adding security to the REST API and to the UI so that only authorised users can access them with the right privileges.
Add The Spring Security Dependencies
We need to add the Spring Security dependencies for Spring Boot and Thymeleaf to the POM as below
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Extended the Model To Include A Manager For Security
So the next step in the tutorial is to create a manager object, as in the real world only a manager can access the payroll system.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import java.util.Arrays;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Entity
public class Manager {
public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
private @Id @GeneratedValue Long id;
private String name;
private @JsonIgnore String password;
private String[] roles;
public void setPassword(String password) {
this.password = PASSWORD_ENCODER.encode(password);
}
protected Manager() {}
public Manager(String name, String password, String... roles) {
this.name = name;
this.setPassword(password);
this.roles = roles;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Manager manager = (Manager) o;
return Objects.equals(id, manager.id) &&
Objects.equals(name, manager.name) &&
Objects.equals(password, manager.password) &&
Arrays.equals(roles, manager.roles);
}
@Override
public int hashCode() {
int result = Objects.hash(id, name, password);
result = 31 * result + Arrays.hashCode(roles);
return result;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public String[] getRoles() {
return roles;
}
public void setRoles(String[] roles) {
this.roles = roles;
}
@Override
public String toString() {
return "Manager{" +
"id=" + id +
", name='" + name + '\'' +
", roles=" + Arrays.toString(roles) +
'}';
}
}
// end::code[]
- BCryptPasswordEncoder and the setPassword() are used to encrypt the password field and ensure its never stored as clear text.
- @JsonIgnore is applied to the password field to ensure its not serialized
Next we create a repository for the Manager by extending the Repository interface. This one is simpler that CrudRepository, and here its used as we dont need all the CRUD methods for this simplified example.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
/**
* @author Greg Turnquist
*/
// tag::code[]
@RepositoryRestResource(exported = false)
public interface ManagerRepository extends Repository<Manager, Long> {
Manager save(Manager manager);
Manager findByName(String name);
}
// end::code[]
- @RepositoryRestResource(exported = false) – Flag indicating whether this resource is exported at all, so setting this to false ensure this is not available as a REST resource.
- Note that this is an interface, and that the methods are implemented by the underlying JPA provider thats chosen. Spring Data derives the underlying query to use based on the name of the method.
Next the Employee pojo is modified to include the manager.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Entity
public class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;
private @Version @JsonIgnore Long version;
private @ManyToOne Manager manager;
private Employee() {}
public Employee(String firstName, String lastName, String description, Manager manager) {
this.firstName = firstName;
this.lastName = lastName;
this.description = description;
this.manager = manager;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id) &&
Objects.equals(firstName, employee.firstName) &&
Objects.equals(lastName, employee.lastName) &&
Objects.equals(description, employee.description) &&
Objects.equals(version, employee.version) &&
Objects.equals(manager, employee.manager);
}
@Override
public int hashCode() {
return Objects.hash(id, firstName, lastName, description, version, manager);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Manager getManager() {
return manager;
}
public void setManager(Manager manager) {
this.manager = manager;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", description='" + description + '\'' +
", version=" + version +
", manager=" + manager +
'}';
}
}
// end::code[]
- The manage is annotated with
@ManyToOne
. – Specifies a single-valued association to another entity class that has many-to-one multiplicity.
Specifying The Spring Security Policies For Securing Employees
Currently the EmployeeRepository looks like this
// tag::code[]
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
// end::code[]
So we are using the default CRUD operations with nothing secured. To secure these to the Managers in the tutorial its changed to override the methods and adding security checks via Spring annotations with Spring Security Spel expressions so they look like this
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* @author Greg Turnquist
*/
// tag::code[]
@PreAuthorize("hasRole('ROLE_MANAGER')")
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
@Override
@PreAuthorize("#employee?.manager == null or #employee?.manager?.name == authentication?.name")
Employee save(@Param("employee") Employee employee);
@Override
@PreAuthorize("@employeeRepository.findById(#id)?.manager?.name == authentication?.name")
void deleteById(@Param("id") Long id);
@Override
@PreAuthorize("#employee?.manager?.name == authentication?.name")
void delete(@Param("employee") Employee employee);
}
// end::code[]
- @PreAuthorize(“hasRole(‘ROLE_MANAGER’)”) – so we are ensuring that they have to be a manager to be able to access the employee data
- @PreAuthorize(“#employee?.manager == null or #employee?.manager?.name == authentication?.name”) – save is only allowed if the manager is null (new employee) or the current logged-in user is the employees manager
- @PreAuthorize(“@employeeRepository.findById(#id)?.manager?.name == authentication?.name”) – delete is only allowed if we do a lookup by id and the current logged-in user is the employees manager
- @PreAuthorize(“#employee?.manager?.name == authentication?.name”) – delete is only allowed if the current logged-in user is the employees manager
Loading The Users Details
When enabling security, a common task is to load the users details for the purpose of checking authentication and authorization for the user. Spring has a UserDetailsService class to allow you to specify the link between your data and Spring security. The implementation in the tutorial is as follows
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Component
public class SpringDataJpaUserDetailsService implements UserDetailsService {
private final ManagerRepository repository;
@Autowired
public SpringDataJpaUserDetailsService(ManagerRepository repository) {
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
Manager manager = this.repository.findByName(name);
return new User(manager.getName(), manager.getPassword(),
AuthorityUtils.createAuthorityList(manager.getRoles()));
}
}
// end::code[]
- The loadByUserName() uses the findByName() lookup we specified earlier to get the Manager, thats used to generate a Spring User. Spring will then use that for the security check.
Defining The Spring Security Policy And Access Rules
Springs WebSecurityConfigurerAdapter
is used to setup the configuration of your web security. Spring boot does setup web security automatically with default settings, but you really need to switch that off and specify it yourself in most cases. Lets see the tutorials implementation for this.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private SpringDataJpaUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(Manager.PASSWORD_ENCODER);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/built/**", "/main.css").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable()
.logout()
.logoutSuccessUrl("/");
}
}
// end::code[]
- @EnableWebSecurity tells Spring that you want to turn off its automatic Web Security configuration and configure web security yourself by overriding WebSecurityConfigurerAdapter methods.
- @EnableGlobalMethodSecurity(prePostEnabled = true) – enable method security and enable pre and post method annotations.
- the configure(AuthenticationManagerBuilder auth) method is telling Spring to use our @AutoWired SpringDataJpaUserDetailsService as its authentication manager and what password encoder to use.
- The we setup the configure(HttpSecurity http) method to configure the http security as follows
Anyone can access the javascript and css static resources
.authorizeRequests()
.antMatchers("/built/**", "/main.css").permitAll()
any other request must be authenticated
.anyRequest().authenticated()
Support form based authentication, give access to the login page and send them to the “/” if successful
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
Use HTTP Basic authentication
.and()
.httpBasic()
Disable CSRF(dont do this in real apps, only for tutorials to make things easier).
.and()
.csrf().disable()
Provide logout support so that accessing the URL “/logout” will remove and clean up any logged in session, and specify the URL to switch to on successful logout.
.logout()
.logoutSuccessUrl("/");
Using Springs @RepositoryEventHandler to Add Security Details
In the tutorial it shows that you can add security details automatically. In reality this isnt specifically security related, its using Springs @RepositoryEventHandler to add in some additional logic when the domain model is changed.
In this example we want specify the manager for an employee when a new employee is created, defaulting that to be the logged in manager. In a real application there would be a seperate part of the app for creating managers, but as we dont have that we will just default it.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.core.annotation.HandleBeforeCreate;
import org.springframework.data.rest.core.annotation.HandleBeforeSave;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Component
@RepositoryEventHandler(Employee.class)
public class SpringDataRestEventHandler {
private final ManagerRepository managerRepository;
@Autowired
public SpringDataRestEventHandler(ManagerRepository managerRepository) {
this.managerRepository = managerRepository;
}
@HandleBeforeCreate
@HandleBeforeSave
public void applyUserInformationUsingSecurityContext(Employee employee) {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
Manager manager = this.managerRepository.findByName(name);
if (manager == null) {
Manager newManager = new Manager();
newManager.setName(name);
newManager.setRoles(new String[]{"ROLE_MANAGER"});
manager = this.managerRepository.save(newManager);
}
employee.setManager(manager);
}
}
// end::code[]
- @RepositoryEventHandler(Employee.class) – Defines this class as a repository event handler for the Employee class
- @HandleBeforeCreate, @HandleBeforeSave – indicates that these methods should be run if an Employee object is saved or created.
- applyUserInformationUsingSecurityContext() method will set the employees manager to the current logged in manager. if the manager doesnt exist it will create a new one first
Adding Manager Data To The Test Data
The DatabaseLoader class is changed to include the managers for the pre loaded employees. We have enabled pre authorisation on the employee repository so we need to ensure we setup the managers and roles correctly.
/*
* Copyright 2015 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
*
* https://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 com.greglturnquist.payroll;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
* @author Greg Turnquist
*/
// tag::code[]
@Component
public class DatabaseLoader implements CommandLineRunner {
private final EmployeeRepository employees;
private final ManagerRepository managers;
@Autowired
public DatabaseLoader(EmployeeRepository employeeRepository,
ManagerRepository managerRepository) {
this.employees = employeeRepository;
this.managers = managerRepository;
}
@Override
public void run(String... strings) throws Exception {
Manager greg = this.managers.save(new Manager("greg", "turnquist",
"ROLE_MANAGER"));
Manager oliver = this.managers.save(new Manager("oliver", "gierke",
"ROLE_MANAGER"));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("greg", "doesn't matter",
AuthorityUtils.createAuthorityList("ROLE_MANAGER")));
this.employees.save(new Employee("Frodo", "Baggins", "ring bearer", greg));
this.employees.save(new Employee("Bilbo", "Baggins", "burglar", greg));
this.employees.save(new Employee("Gandalf", "the Grey", "wizard", greg));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("oliver", "doesn't matter",
AuthorityUtils.createAuthorityList("ROLE_MANAGER")));
this.employees.save(new Employee("Samwise", "Gamgee", "gardener", oliver));
this.employees.save(new Employee("Merry", "Brandybuck", "pony rider", oliver));
this.employees.save(new Employee("Peregrin", "Took", "pipe smoker", oliver));
SecurityContextHolder.clearContext();
}
}
// end::code[]
Checking The Secured REST Service
Running the application and calling the REST API via curl at this stage as mentioned in the tutorial highlighted a problem though.
C:\Users\>curl -v -u greg:turnquist localhost:8080/api/employees/1
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'greg'
> GET /api/employees/1 HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Z3JlZzp0dXJucXVpc3Q=
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 404
< Set-Cookie: JSESSIONID=FDF71FFFD5112FBAA2BFE716D394ADEA; Path=/; HttpOnly
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Wed, 16 Oct 2019 16:20:42 GMT
<
* Connection #0 to host localhost left intact
The problem as shown aboive is that i was getting a 404 error back from the rest interface. The problem seemed to be that there was no employee being setup with an id of 1, for some reason the id is now starting with 3?? If I change the curl command to pull up the employee with an id of 3 it works fine?
C:\Users>curl -v -u greg:turnquist localhost:8080/api/employees/3
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'greg'
> GET /api/employees/3 HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Z3JlZzp0dXJucXVpc3Q=
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 200
< Set-Cookie: JSESSIONID=4E560B6CFCC782370556D07FA659BD0E; Path=/; HttpOnly
< ETag: "0"
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 16 Oct 2019 16:23:36 GMT
<
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"description" : "ring bearer",
"manager" : {
"name" : "greg",
"roles" : [ "ROLE_MANAGER" ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/3"
},
"employee" : {
"href" : "http://localhost:8080/api/employees/3"
}
}
}* Connection #0 to host localhost left intact
Looking at this you can see the eTag and can also see the manager details for the employee but not the password as we specified @JsonIgnore for that.
Add The Manager Details To The React UI
The app.js react UI code is updated to include details of the manager and the security changes.
'use strict';
const React = require('react');
const ReactDOM = require('react-dom');
const when = require('when');
const client = require('./client');
const follow = require('./follow'); // function to hop multiple links by "rel"
const stompClient = require('./websocket-listener');
const root = '/api';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {employees: [], attributes: [], page: 1, pageSize: 2, links: {}
, loggedInManager: this.props.loggedInManager};
this.updatePageSize = this.updatePageSize.bind(this);
this.onCreate = this.onCreate.bind(this);
this.onUpdate = this.onUpdate.bind(this);
this.onDelete = this.onDelete.bind(this);
this.onNavigate = this.onNavigate.bind(this);
this.refreshCurrentPage = this.refreshCurrentPage.bind(this);
this.refreshAndGoToLastPage = this.refreshAndGoToLastPage.bind(this);
}
loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
// tag::json-schema-filter[]
/**
* Filter unneeded JSON Schema properties, like uri references and
* subtypes ($ref).
*/
Object.keys(schema.entity.properties).forEach(function (property) {
if (schema.entity.properties[property].hasOwnProperty('format') &&
schema.entity.properties[property].format === 'uri') {
delete schema.entity.properties[property];
}
else if (schema.entity.properties[property].hasOwnProperty('$ref')) {
delete schema.entity.properties[property];
}
});
this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;
// end::json-schema-filter[]
});
}).then(employeeCollection => {
this.page = employeeCollection.entity.page;
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
}).then(employeePromises => {
return when.all(employeePromises);
}).done(employees => {
this.setState({
page: this.page,
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: this.links
});
});
}
// tag::on-create[]
onCreate(newEmployee) {
follow(client, root, ['employees']).done(response => {
client({
method: 'POST',
path: response.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
})
}
// end::on-create[]
// tag::on-update[]
onUpdate(employee, updatedEmployee) {
if(employee.entity.manager.name === this.state.loggedInManager) {
updatedEmployee["manager"] = employee.entity.manager;
client({
method: 'PUT',
path: employee.entity._links.self.href,
entity: updatedEmployee,
headers: {
'Content-Type': 'application/json',
'If-Match': employee.headers.Etag
}
}).done(response => {
/* Let the websocket handler update the state */
}, response => {
if (response.status.code === 403) {
alert('ACCESS DENIED: You are not authorized to update ' +
employee.entity._links.self.href);
}
if (response.status.code === 412) {
alert('DENIED: Unable to update ' + employee.entity._links.self.href +
'. Your copy is stale.');
}
});
} else {
alert("You are not authorized to update");
}
}
// end::on-update[]
// tag::on-delete[]
onDelete(employee) {
client({method: 'DELETE', path: employee.entity._links.self.href}
).done(response => {/* let the websocket handle updating the UI */},
response => {
if (response.status.code === 403) {
alert('ACCESS DENIED: You are not authorized to delete ' +
employee.entity._links.self.href);
}
});
}
// end::on-delete[]
onNavigate(navUri) {
client({
method: 'GET',
path: navUri
}).then(employeeCollection => {
this.links = employeeCollection.entity._links;
this.page = employeeCollection.entity.page;
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
}).then(employeePromises => {
return when.all(employeePromises);
}).done(employees => {
this.setState({
page: this.page,
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: this.state.pageSize,
links: this.links
});
});
}
updatePageSize(pageSize) {
if (pageSize !== this.state.pageSize) {
this.loadFromServer(pageSize);
}
}
// tag::websocket-handlers[]
refreshAndGoToLastPage(message) {
follow(client, root, [{
rel: 'employees',
params: {size: this.state.pageSize}
}]).done(response => {
if (response.entity._links.last !== undefined) {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
})
}
refreshCurrentPage(message) {
follow(client, root, [{
rel: 'employees',
params: {
size: this.state.pageSize,
page: this.state.page.number
}
}]).then(employeeCollection => {
this.links = employeeCollection.entity._links;
this.page = employeeCollection.entity.page;
return employeeCollection.entity._embedded.employees.map(employee => {
return client({
method: 'GET',
path: employee._links.self.href
})
});
}).then(employeePromises => {
return when.all(employeePromises);
}).then(employees => {
this.setState({
page: this.page,
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: this.state.pageSize,
links: this.links
});
});
}
// end::websocket-handlers[]
// tag::register-handlers[]
componentDidMount() {
this.loadFromServer(this.state.pageSize);
stompClient.register([
{route: '/topic/newEmployee', callback: this.refreshAndGoToLastPage},
{route: '/topic/updateEmployee', callback: this.refreshCurrentPage},
{route: '/topic/deleteEmployee', callback: this.refreshCurrentPage}
]);
}
// end::register-handlers[]
render() {
return (
<div>
<CreateDialog attributes={this.state.attributes} onCreate={this.onCreate}/>
<EmployeeList page={this.state.page}
employees={this.state.employees}
links={this.state.links}
pageSize={this.state.pageSize}
attributes={this.state.attributes}
onNavigate={this.onNavigate}
onUpdate={this.onUpdate}
onDelete={this.onDelete}
updatePageSize={this.updatePageSize}
loggedInManager={this.state.loggedInManager}/>
</div>
)
}
}
class CreateDialog extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const newEmployee = {};
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
this.props.attributes.forEach(attribute => {
ReactDOM.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs
});
window.location = "#";
}
render() {
const inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute} ref={attribute} className="field"/>
</p>
);
return (
<div>
<a href="#createEmployee">Create</a>
<div id="createEmployee" className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Create new employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
}
}
class UpdateDialog extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const updatedEmployee = {};
this.props.attributes.forEach(attribute => {
updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onUpdate(this.props.employee, updatedEmployee);
window.location = "#";
}
render() {
const inputs = this.props.attributes.map(attribute =>
<p key={this.props.employee.entity[attribute]}>
<input type="text" placeholder={attribute}
defaultValue={this.props.employee.entity[attribute]}
ref={attribute} className="field"/>
</p>
);
const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href;
const isManagerCorrect = this.props.employee.entity.manager.name == this.props.loggedInManager;
if (isManagerCorrect === false) {
return (
<div>
<a>Not Your Employee</a>
</div>
)
} else {
return (
<div>
<a href={"#" + dialogId}>Update</a>
<div id={dialogId} className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Update an employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Update</button>
</form>
</div>
</div>
</div>
)
}
}
}
class EmployeeList extends React.Component {
constructor(props) {
super(props);
this.handleNavFirst = this.handleNavFirst.bind(this);
this.handleNavPrev = this.handleNavPrev.bind(this);
this.handleNavNext = this.handleNavNext.bind(this);
this.handleNavLast = this.handleNavLast.bind(this);
this.handleInput = this.handleInput.bind(this);
}
handleInput(e) {
e.preventDefault();
const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value;
if (/^[0-9]+$/.test(pageSize)) {
this.props.updatePageSize(pageSize);
} else {
ReactDOM.findDOMNode(this.refs.pageSize).value = pageSize.substring(0, pageSize.length - 1);
}
}
handleNavFirst(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.first.href);
}
handleNavPrev(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.prev.href);
}
handleNavNext(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.next.href);
}
handleNavLast(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.last.href);
}
render() {
const pageInfo = this.props.page.hasOwnProperty("number") ?
<h3>Employees - Page {this.props.page.number + 1} of {this.props.page.totalPages}</h3> : null;
const employees = this.props.employees.map(employee =>
<Employee key={employee.entity._links.self.href}
employee={employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}
onDelete={this.props.onDelete}
loggedInManager={this.props.loggedInManager}/>
);
const navLinks = [];
if ("first" in this.props.links) {
navLinks.push(<button key="first" onClick={this.handleNavFirst}><<</button>);
}
if ("prev" in this.props.links) {
navLinks.push(<button key="prev" onClick={this.handleNavPrev}><</button>);
}
if ("next" in this.props.links) {
navLinks.push(<button key="next" onClick={this.handleNavNext}>></button>);
}
if ("last" in this.props.links) {
navLinks.push(<button key="last" onClick={this.handleNavLast}>>></button>);
}
return (
<div>
{pageInfo}
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
<table>
<tbody>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
<th>Manager</th>
<th></th>
<th></th>
</tr>
{employees}
</tbody>
</table>
<div>
{navLinks}
</div>
</div>
)
}
}
// tag::employee[]
class Employee extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete() {
this.props.onDelete(this.props.employee);
}
render() {
return (
<tr>
<td>{this.props.employee.entity.firstName}</td>
<td>{this.props.employee.entity.lastName}</td>
<td>{this.props.employee.entity.description}</td>
<td>{this.props.employee.entity.manager.name}</td>
<td>
<UpdateDialog employee={this.props.employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}
loggedInManager={this.props.loggedInManager}/>
</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
}
// end::employee[]
ReactDOM.render(
<App loggedInManager={document.getElementById('managername').innerHTML } />,
document.getElementById('react')
)
The Employee class includes the manager name as a new column in the row, and also includes the details of the loggedInManager as a parameter.
// tag::employee[]
class Employee extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete() {
this.props.onDelete(this.props.employee);
}
render() {
return (
<tr>
<td>{this.props.employee.entity.firstName}</td>
<td>{this.props.employee.entity.lastName}</td>
<td>{this.props.employee.entity.description}</td>
<td>{this.props.employee.entity.manager.name}</td>
<td>
<UpdateDialog employee={this.props.employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}
loggedInManager={this.props.loggedInManager}/>
</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
}
// end::employee[]
The below filters out the manager details from the schema so they arent editable on the createDialog and the updateDialog.
// tag::json-schema-filter[]
/**
* Filter unneeded JSON Schema properties, like uri references and
* subtypes ($ref).
*/
Object.keys(schema.entity.properties).forEach(function (property) {
if (schema.entity.properties[property].hasOwnProperty('format') &&
schema.entity.properties[property].format === 'uri') {
delete schema.entity.properties[property];
}
else if (schema.entity.properties[property].hasOwnProperty('$ref')) {
delete schema.entity.properties[property];
}
});
this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;
// end::json-schema-filter[]
Additional logic is added to the onUpdate() method to ensure that an employee record can only be updated if authorised.
if(employee.entity.manager.name === this.state.loggedInManager) {
updatedEmployee["manager"] = employee.entity.manager;
} else {
alert("You are not authorized to update");
}
It also now reports a 403 error if it comes back from the response
if (response.status.code === 403) {
alert('ACCESS DENIED: You are not authorized to update ' +
employee.entity._links.self.href);
}
The onDelete() function now also report a 403 error if it comes back from the response if the user is not authorised.
Add Security Details To The Homepage
The final step in the tutorial is to add the manager details to the index.html homepage, and also create a form to allow the user to logout as shown below.
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head lang="en">
<meta charset="UTF-8"/>
<title>ReactJS + Spring Data REST</title>
<link rel="stylesheet" href="/main.css" />
</head>
<body>
<!-- tag::logout[] -->
<div>
Hello, <span id="managername" th:text="${#authentication.name}">user</span>.
<form th:action="@{/logout}" method="post">
<input type="submit" value="Log Out"/>
</form>
</div>
<!-- end::logout[] -->
<div id="react"></div>
<script src="built/bundle.js"></script>
</body>
</html>
Check The UI Changes
So if we run the UI now, we are presented with Spring Security’s default login page.

When you log in you get the details page with the logged in user and manager shown

If we try and update a user that we are not the manager of we cant as the update link has been removed.

If we try and delete an employee that we are not the manager of we get an error.


Conclusion
So we have come to the end of following through on this Spring Boot, Spring Data Rest and React tutorial. I feel like we have learned quite a lot about
- Setting up Spring Boot, Spring Data REST and Thymeleaf
- Adding Node.js, React,js, rest.js, webpack and babel to a Spring Boot project so we can do React UI development
- Building a REST API with Spring Boot and Spring Data REST
- Building a React.js UI and using that to grab data from a Spring Boot REST API
- Working with Spring Data REST hypermedia controls
- Building a CRUD Interface with Spring Data REST, Spring Boot and React.js UI
- Using Springs support for websockets and Spring Data REST events to build a real time event driven React.js UI
- Using Springs security model to secure the backend REST API and the front end React.js UI
So I would say this is a very good introduction to some fairly complex features, and is presented in a way that allows each task to be built on the previous ones. This is definitely a good tutorial to work through to understand the basic concepts of the above topics. This is also a good jumping off point for investigating the above topics in more detail.